├── .eslintrc.cjs
├── .gitignore
├── LICENSE
├── README.md
├── __tests__
├── AddTable.test.tsx
├── App.test.tsx
├── NavBar.test.tsx
├── TableHeader.test.tsx
├── TestText.test.tsx
├── TestText.tsx
└── TurboNode.test.tsx
├── dist
├── assets
│ ├── FiraMono-Regular-UwpAzb3w.ttf
│ ├── index-2-ISIJB8.css
│ ├── index-80b7uBus.js
│ ├── vendors-bGEdEbVg.css
│ └── vendors-sW7PeAS8.js
├── favicon.png
└── index.html
├── express-plugin.ts
├── global_types
└── types.d.ts
├── index.html
├── package-lock.json
├── package.json
├── public
└── favicon.png
├── server
├── controller.ts
├── db.ts
├── index.ts
└── typeDefs.ts
├── src
├── App.tsx
├── assets
│ └── FiraMono-Regular.ttf
├── components
│ ├── AddColumnDialog.tsx
│ ├── AddTable.tsx
│ ├── ColumnNameNode.tsx
│ ├── DataTypeSelector.tsx
│ ├── DeleteColumnButton.tsx
│ ├── Flow.tsx
│ ├── FunctionIcon.tsx
│ ├── GenerateEdges.tsx
│ ├── GenerateNodes.tsx
│ ├── LandingPage.tsx
│ ├── NavBar.tsx
│ ├── TableHeader.tsx
│ ├── TableMenu.tsx
│ ├── TurboEdge.tsx
│ ├── TurboNode.tsx
│ └── waves.tsx
├── main.tsx
├── store.ts
├── stylesheets
│ ├── index.css
│ └── landingPage.scss
├── utilities
│ ├── queries.ts
│ └── utility.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist-ssr
12 | *.local
13 | .env
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # sql file
27 | starwars_postgres_create.sql
28 | public/migration_log.txt
29 | migration_log.txt
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SQLens
2 |
3 | SQLens is a powerful SQL database visualizer designed to simplify the complexities of managing and understanding relational databases with extensive foreign key relationships. In the world of database administration and development, keeping track of intricate relationships and optimizing queries across large datasets can be daunting. SQLens addresses these challenges head-on by providing a dynamic, graphical representation of your database schema, making it easier and more enjoyable to visualize, analyze, and manage data relationships.
4 |
5 |
6 |
7 |
8 |
9 | ## Visualize Your Data
10 |
11 |
12 |
13 |
14 |
15 | ## Getting Started
16 |
17 | ### Initial Setup
18 | 1. Fork and clone this repository.
19 | 2. Run npm install to install dependancies.
20 | 3. Run npm build to ensure any dependancy updates are reflected.
21 | 4. Run npm start to spin up a server locally, and view your data on localhost:3000
22 |
23 | ### Using the app
24 |
25 |
26 |
27 |
28 |
29 | - Simply enter in a URI string for your postgres database
30 | - Click and drag to view foreign-key relationships between tables
31 |
32 |
33 |
34 |
35 |
36 | - Add a new table using the + button on the Add New Table component
37 | - Add a column from the drop-down table to the right
38 | - In the pop-out, name the column and select its data type from the drop-down menu
39 | - Click Save to add.
40 |
41 |
42 |
43 |
44 |
45 | - To rename columns, use the pencil tool
46 | - To delete a column, simply click the trashcan, you will be asked "Are you sure?"
47 | - To delete a table, use the menu to the right
48 | - To download a migration file of any changes made, use the menu at the top left corner of the browser
49 |
50 |
51 |
52 |
53 |
54 | - SQLens ensures data integrity and operational reliability, leveraging the fact that PostgreSQL databases are ACID-compliant, which means invalid column or table names are automatically rejected by PostgreSQL and will not be saved.
55 |
56 |
57 |
58 |
59 | ## Contributing
60 |
61 | - To run the app in dev mode, simply npm run dev
62 | - To ensure updates are compatible with all functional components, run unit tests with npm run test
63 |
64 | ### Contribution Guidelines
65 |
66 | - Fork this repository
67 | - Checkout from dev to a new feature branch with the format
68 | - Test code before PR with npm run test
69 | - Make a Pull Request
70 |
71 | ### Features to Work On
72 |
73 |
74 | | Feature | Status |
75 | |---------------------------------------------------------------------------------------|-----------|
76 | | Migration to use TurboNode styling for React Flow component | ✅ |
77 | | Add, delete, edit table and column names | ✅ |
78 | | Apollo Server graphQL cacheing | ✅ |
79 | | Downloadable migration file | ✅ |
80 | | Clean way to view row data | ⏳ |
81 | | Update algorithm for best visual rendering of foreign key relationships | ⏳ |
82 | | Allow user to add/edit relationships between tables w/ GUI | ⏳ |
83 | | Build docker container for CI/CD w/ github actions | 🙏🏻 |
84 |
85 | - ✅ = Ready to use
86 | - ⏳ = In progress
87 | - 🙏🏻 = Looking for contributors
88 |
89 | ## License
90 |
91 | SQLens is free and open-source licensed under the [MIT License](https://github.com/oslabs-beta/SQLens/blob/main/LICENSE)
92 |
93 |
94 | ## Technologies Used
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | ## Contributors
109 |
110 |
111 |
112 |
113 |
114 |
115 | Alex Palazzo
116 |
117 | 🖇️
118 | 🐙
119 |
120 |
121 |
122 |
123 | Jarod Crawford
124 |
125 | 🖇️
126 | 🐙
127 |
128 |
129 |
130 |
131 | Jenny Ouk
132 |
133 | 🖇️
134 | 🐙
135 |
136 |
137 |
138 |
139 | Margaret Hatch
140 |
141 | 🖇️
142 | 🐙
143 |
144 |
145 |
146 |
147 |
148 | - 🖇️ = LinkedIn
149 | - 🐙 = Github
150 |
--------------------------------------------------------------------------------
/__tests__/AddTable.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { describe, it, expect, vi } from 'vitest';
3 | import { render, fireEvent, screen } from '@testing-library/react';
4 | import AddTable from '../src/components/AddTable';
5 | import * as store from '../src/store';
6 |
7 |
8 | describe('AddTable Component', () => {
9 | // Mock setup for store
10 | beforeEach(() => {
11 | // Reset mocks and set up new implementations
12 | store.useStore = vi.fn(() => ({
13 | fetchAndUpdateTableDetails: vi.fn(),
14 | tables: [],
15 | setTables: vi.fn(),
16 | }));
17 | });
18 |
19 | it('renders correctly with initial label', () => {
20 | const testData = { label: 'Add New Table' };
21 | render( );
22 | expect(screen.getByText('Add New Table')).toBeTruthy();
23 | });
24 |
25 | it('enters editing mode on edit button click', async () => {
26 | const testData = { label: 'Editable Table' };
27 | render( );
28 | fireEvent.click(screen.getByLabelText('edit'));
29 | expect(screen.getByRole('textbox')).toBeTruthy();
30 | });
31 |
32 |
33 | it('cancels editing mode and reverts changes on cancel button click', async () => {
34 | const testData = { label: 'Editable Table' };
35 | render( );
36 | fireEvent.click(screen.getByLabelText('edit'));
37 | fireEvent.click(screen.getByLabelText('cancel'));
38 | expect(screen.queryByRole('textbox')).not.toBeTruthy();
39 | // Since getByText fails if the element is not found, using queryByText to check absence
40 | expect(screen.queryByText('Editable Table')).toBeTruthy();
41 | });
42 |
43 | // Add more tests to cover the check (confirm) button functionality and its interaction with the mock fetch
44 | });
45 |
--------------------------------------------------------------------------------
/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | import { describe, it } from 'vitest';
2 | import { render, screen } from '@testing-library/react';
3 | import assert from 'assert';
4 | import App from '../src/App';
5 | import { MockedProvider } from '@apollo/client/testing';
6 | import { MemoryRouter } from 'react-router-dom';
7 | import { GET_TABLE_NAMES } from '../src/utilities/queries';
8 |
9 | const mocks = [
10 | {
11 | request: {
12 | query: GET_TABLE_NAMES,
13 | },
14 | result: {
15 | data: {
16 | getTableNames: [
17 | {
18 | name: "Table1",
19 | columns: ["column1", "column2"],
20 | foreignKeys: [
21 | {
22 | columnName: "column1",
23 | foreignTableName: "ForeignTable",
24 | foreignColumnName: "foreignColumn1",
25 | },
26 | ],
27 | },
28 | ],
29 | },
30 | },
31 | },
32 | ];
33 |
34 | describe('App Routing to Flow', () => {
35 | it('renders the Flow component when navigating to /flow', async () => {
36 | render(
37 |
38 | } />
39 |
40 | );
41 |
42 | // Wait for the element to appear in the DOM
43 | const flowContainer = await screen.findByTestId('flow-container');
44 | // Assert that the flow container is rendered
45 | assert.ok(flowContainer);
46 | });
47 | });
48 |
49 | describe('App Routing to Landing Page', () => {
50 | it('renders the Flow component when navigating to /flow', async () => {
51 | render(
52 |
53 | } />
54 |
55 | );
56 |
57 | // Wait for the element to appear in the DOM
58 | const flowContainer = await screen.findByTestId('outer-container');
59 | // Assert that the flow container is rendered
60 | assert.ok(flowContainer);
61 | });
62 | });
--------------------------------------------------------------------------------
/__tests__/NavBar.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent } from '@testing-library/react';
3 | import NavBar from '../src/components/NavBar';
4 | import { describe, it, expect } from 'vitest';
5 | import { MemoryRouter } from 'react-router-dom';
6 |
7 | describe('NavBar Component', () => {
8 | //renders navbar nested in memory router since navbar uses useNavigate
9 | beforeEach(() => {
10 | render( );
11 | });
12 | it('renders NavBar Component with button and logo', async () => {
13 | const logo = await screen.getByText('SQL');
14 | expect(logo).toBeTruthy();
15 |
16 | const menu = await screen.getByLabelText('open drawer');
17 | expect(menu).toBeTruthy();
18 | });
19 | it('renders NavBar Menu when clicked', async () => {
20 | fireEvent.click(screen.getByLabelText('open drawer'));
21 | expect(screen.getByText('Download Migration File')).toBeTruthy();
22 | expect(screen.getByText('Logout')).toBeTruthy();
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/__tests__/TableHeader.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent } from '@testing-library/react';
3 | import TableHeader from '../src/components/TableHeader';
4 | import { describe, it, expect } from 'vitest';
5 |
6 | describe('TableHeader Component', () => {
7 | it('renders TableHeader label and button', async () => {
8 | const mockData = {
9 | label: 'Test Label',
10 | };
11 | render( );
12 |
13 | // Test rendering of initial label
14 | const label = await screen.getByText('Test Label');
15 | expect(label).toBeTruthy();
16 | });
17 |
18 | it('Renders Table Menu button and expands Table Menu when clicked', async () => {
19 | const mockData = {
20 | label: 'Test Label',
21 | };
22 | render( );
23 |
24 | // Test clicking more button
25 | const buttons = await screen.getAllByLabelText('expandTableMenu');
26 | fireEvent.click(buttons[0]);
27 |
28 | const edit = await screen.getByText('Edit Table Name');
29 | expect(edit).toBeTruthy();
30 | const add = await screen.getByText('Add Column');
31 | expect(add).toBeTruthy();
32 | const deleteBtn = await screen.getByText('Delete Table');
33 | expect(deleteBtn).toBeTruthy();
34 | });
35 |
36 | it('Replaces label with text field when edit table is selected and removes text box after edit is canceled', async () => {
37 | const mockData = {
38 | label: 'Test Label',
39 | };
40 | render( );
41 |
42 | // Clicking expand button
43 | const buttons = await screen.getAllByLabelText('expandTableMenu');
44 | fireEvent.click(buttons[0]);
45 |
46 | const edit = await screen.getByText('Edit Table Name');
47 | fireEvent.click(edit);
48 |
49 | expect(screen.getByRole('textbox')).toBeTruthy();
50 |
51 | const cancel = await screen.getAllByLabelText('cancel');
52 | fireEvent.click(cancel[0]);
53 |
54 | const label = await screen.getAllByText('Test Label');
55 | expect(label).toBeTruthy();
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/__tests__/TestText.test.tsx:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { render, screen } from '@testing-library/react';
3 | import TestText from './TestText';
4 | import React from 'react';
5 |
6 | describe('App', () => {
7 | it('Vite to be in document', () => {
8 | render( );
9 | expect(screen.getByText('TestText')).toBeTruthy();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/__tests__/TestText.tsx:
--------------------------------------------------------------------------------
1 | import App from './App.tsx'
2 | import React from 'react'
3 |
4 | // This file exists for test suite setup purposes only
5 | // it has no functionality to the app
6 | function TestText() {
7 |
8 | return (
9 | <>
10 |
11 |
TestText
12 |
13 | >
14 | )
15 | }
16 |
17 | export default TestText
--------------------------------------------------------------------------------
/__tests__/TurboNode.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { describe, it, expect } from 'vitest';
3 | import { render, screen } from '@testing-library/react';
4 | import TurboNode, { TurboNodeData } from '../src/components/TurboNode';
5 |
6 | describe('TurboNode Component', () => {
7 | // tests normal turbo node table header
8 | it('renders TurboNode component with TableHeader when label is not "Add New Table"', async () => {
9 | const mockData: TurboNodeData = {
10 | label: 'Test Label',
11 | };
12 |
13 | render( );
14 | const label = await screen.getByText('Test Label');
15 | expect(label).toBeTruthy();
16 | });
17 |
18 | // tests "add table" table header
19 | it('renders TurboNode component with AddTable when label is "Add New Table"', async () => {
20 | const addTableData: TurboNodeData = {
21 | label: 'Add New Table',
22 | };
23 |
24 | render( );
25 | const label = await screen.getByText('Add New Table');
26 | expect(label).toBeTruthy();
27 | });
28 | });
29 |
30 | // Alternative testing method
31 | // const { getByText } = render( );
32 | // const tableHeaderElement = getByText('Test Label');
33 | // expect(tableHeaderElement).toBeTruthy();
34 |
--------------------------------------------------------------------------------
/dist/assets/FiraMono-Regular-UwpAzb3w.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SQLens/1ac24699452953c0bc6fed2da1a9fc011cbf00db/dist/assets/FiraMono-Regular-UwpAzb3w.ttf
--------------------------------------------------------------------------------
/dist/assets/index-2-ISIJB8.css:
--------------------------------------------------------------------------------
1 | @import"https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap";@font-face{font-family:Fira Mono;font-style:normal;font-weight:400;src:local("Fira Mono"),url(/assets/FiraMono-Regular-UwpAzb3w.ttf) format("ttf")}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin:0;padding:0}body{margin:0;display:flex;place-items:center}.flow-container{display:flex;width:100vw;height:calc(100vh - 64px)}.column-name-node{display:flex;justify-content:space-between;height:39px;align-items:center;border:solid 1px #213547;border-radius:3px;font-family:Fira Mono,monospace;padding-left:5px}.column-label{padding:0 8px;font-family:Fira Mono,monospace}.table-name-input:focus{outline:none}.table-name-input{margin-left:-3px;height:30px;width:155px;background-color:var(--bg-color);color:var(--text-color);border:2px solid #2a8af6;border:2px solid #e92a67;border-radius:4px;font-family:Fira mono}.react-flow{--bg-color: rgb(17, 17, 17);--text-color: rgb(243, 244, 246);--node-border-radius: 10px;--node-box-shadow: 10px 0 15px rgba(42, 138, 246, .3), -10px 0 15px rgba(233, 42, 103, .3);background-color:var(--bg-color);color:var(--text-color)}.react-flow__node-turbo{border-radius:var(--node-border-radius);display:flex;height:70px;min-width:150px;font-family:Fira Mono,monospace;font-weight:500;letter-spacing:-.2px;box-shadow:var(--node-box-shadow)}.react-flow__node-turbo .wrapper{overflow:hidden;display:flex;padding:2px;position:relative;border-radius:var(--node-border-radius);flex-grow:1}.gradient:before{content:"";position:absolute;height:800px;width:800px;background:conic-gradient(from -160deg at 50% 50%,#e92a67,#a853ba,#2a8af6,#e92a67 360deg);left:50%;top:50%;transform:translate(-50%,-50%);border-radius:100%}.react-flow__node-turbo.selected .wrapper.gradient:before{content:"";background:conic-gradient(from -160deg at 50% 50%,#e92a67,#a853ba,#2a8af6,#2a8af600 360deg);animation:spinner 4s linear infinite;transform:translate(-50%,-50%) rotate(0);z-index:-1}@keyframes spinner{to{transform:translate(-50%,-50%) rotate(-360deg)}}.react-flow__node-turbo .inner{background:var(--bg-color);padding:15px;border-radius:var(--node-border-radius);display:flex;flex-direction:row;justify-content:space-between;flex-grow:1;position:relative}.react-flow__node-turbo .icon{margin-right:8px}.react-flow__node-turbo .body{display:flex;justify-content:space-between;height:40px;justify-items:start}.react-flow__node-turbo .title{font-size:16px;margin-bottom:2px;line-height:1}.react-flow__node-turbo .subline{font-size:12px;color:#777}.react-flow__node-turbo .cloud{border-radius:100%;width:30px;height:30px;right:0;position:absolute;top:0;transform:translate(50%,-50%);display:flex;transform-origin:center center;padding:2px;overflow:hidden;box-shadow:var(--node-box-shadow);z-index:1}.react-flow__node-turbo .cloud div{background-color:var(--bg-color);flex-grow:1;border-radius:100%;display:flex;justify-content:center;align-items:center;position:relative}.react-flow__handle{opacity:0}.react-flow__handle.source{right:-10px}.react-flow__handle.target{left:-20px}.react-flow__node:focus{outline:none}.react-flow__edge .react-flow__edge-path{stroke:url(#edge-gradient);stroke-width:2;stroke-opacity:.75}.react-flow__controls button{background-color:var(--bg-color);color:var(--text-color);border:1px solid #95679e;border-bottom:none}.react-flow__controls button:hover{background-color:#252525}.react-flow__controls button:first-child{border-radius:5px 5px 0 0}.react-flow__controls button:last-child{border-bottom:1px solid #95679e;border-radius:0 0 5px 5px}.react-flow__controls button path{fill:var(--text-color)}.react-flow__attribution{background:#c8c8c833}.react-flow__attribution a{color:#95679e}.table-menu-dots{margin:-8px}body{display:flex;align-items:center;justify-content:center;min-height:100vh;overflow:hidden}.logo-container{margin-left:-5vw}.site-logo{display:flex;align-items:center;transform:translateZ(0)}[id=logo]{position:relative;flex:0 0 5.625rem;width:6.25rem;z-index:2}[id=logo] polygon{transform-origin:50%}[id=logo] circle{transform-origin:80% 80%}.site-title{position:relative;overflow:hidden;margin-left:-4.25rem;z-index:1;transform:translateZ(0);font-family:Fira Mono,monospace}.site-title-text{padding:.25rem .375rem .25rem 1.75rem;color:#e92a67;font-size:2.8125rem;font-weight:300}.site-title-text span{font-family:Fira Mono,monospace;margin-left:.015625rem;color:#646cff}.nav-title{position:relative;overflow:hidden;z-index:1;transform:translateZ(0);font-family:Fira Mono,monospace}.nav-title-text{color:#e92a67;font-size:1.25rem;font-weight:300}.nav-title-text span{font-family:Fira Mono,monospace;margin-left:.015625rem;color:#646cff}.outer-container{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:100vh;text-align:center}.input-container{width:40vw;margin-top:20px}.ocean{width:100%;position:absolute;bottom:0;left:0;background:#8157f4}.wave{background:url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/85486/wave.svg) repeat-x;position:absolute;width:6400px;top:-168px;left:0;height:198px;transform:translate(0,0,0);animation:wave 10s ease infinite;transform:translateZ(0);filter:invert(47%) sepia(95%) saturate(6399%) hue-rotate(314deg) brightness(102%) contrast(102%)}.wave.wave2{animation:swell 8s ease infinite;opacity:1;filter:invert(47%) sepia(95%) saturate(6399%) hue-rotate(244deg) brightness(102%) contrast(102%)}.wave:nth-of-type(2){top:-175px;animation:wave 7s cubic-bezier(.36,.45,.63,.53) -.125s infinite,swell 7s ease -1.25s infinite;opacity:1}.moving-shadow{--border-size: 3px;animation:shadow-move 3s linear infinite}@keyframes shadow-move{0%{box-shadow:20px 0 40px #2a8af6b3,-20px 0 40px #e92a67b3}50%{box-shadow:-20px 0 40px #2a8af6b3,20px 0 40px #e92a67b3}to{box-shadow:20px 0 40px #2a8af6b3,-20px 0 40px #e92a67b3}}@keyframes wave{0%{margin-left:0}to{margin-left:-1600px}}@keyframes swell{0%,to{transform:translateY(-20px)}50%{transform:translateY(5px)}}
2 |
--------------------------------------------------------------------------------
/dist/assets/index-80b7uBus.js:
--------------------------------------------------------------------------------
1 | import{c as e,R as a,j as t,I as n,d as l,D as s,a as r,b as o,e as i,B as c,r as d,H as m,P as u,T as h,f as p,C as b,g,h as x,u as f,A as j,i as y,k as N,M as w,l as T,m as C,n as v,F as S,S as $,O as k,o as E,p as O,q as D,s as z,t as A,v as q,w as P,x as U,y as I,z as _,E as F,G as L,J as R,K as W,L as J,N as K,Q as M,U as Y,V as B,W as G,X,Y as V}from"./vendors-sW7PeAS8.js";!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))a(e);new MutationObserver((e=>{for(const t of e)if("childList"===t.type)for(const e of t.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&a(e)})).observe(document,{childList:!0,subtree:!0})}function a(e){if(e.ep)return;e.ep=!0;const a=function(e){const a={};return e.integrity&&(a.integrity=e.integrity),e.referrerPolicy&&(a.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?a.credentials="include":"anonymous"===e.crossOrigin?a.credentials="omit":a.credentials="same-origin",a}(e);fetch(e.href,a)}}();const Z=e(((e,a)=>({tables:[],setTables:a=>e({tables:a}),searchValue:"",setSearchValue:a=>e({searchValue:a}),fetchTables:async()=>{try{const a=await async function(){const e=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n query {\n getTableNames {\n name\n columns\n foreignKeys {\n columnName\n foreignTableName\n foreignColumnName\n }\n }\n }\n "})}),a=await e.json();if(a.errors)throw console.error(a.errors),new Error("Error fetching tables");return a.data.getTableNames}();e({tables:a})}catch(a){console.error("Error fetching tables:",a)}},fetchAndUpdateTableDetails:async(t,n)=>{try{const l=await async function(e){const a=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n query GetTableDetails($tableName: String!) {\n getTableDetails(tableName: $tableName) {\n name\n columns\n foreignKeys {\n columnName\n foreignTableName\n foreignColumnName\n }\n }\n }\n ",variables:{tableName:e}})}),t=await a.json();if(t.errors)throw console.error(t.errors),new Error("Error fetching table details");return t.data.getTableDetails}(t),s=a().tables;let r;r=n?s.map((e=>e.name===n?l:e)):s.map((e=>e.name===t?l:e)),e({tables:r})}catch(l){console.error("Error fetching updated table details:",l)}}}))),H=({data:e})=>{const[d,m]=a.useState(!1),u=Z((e=>e.fetchAndUpdateTableDetails));return t.jsxs(t.Fragment,{children:[t.jsx(n,{"aria-label":"delete",size:"small",onClick:()=>{m(!0)},children:t.jsx(l,{fontSize:"inherit"})}),t.jsxs(s,{open:d,onClose:()=>m(!1),"aria-describedby":"alert-dialog-description",children:[t.jsx(r,{children:t.jsx(o,{id:"alert-dialog-description",children:"Are you sure you want to delete this column?"})}),t.jsxs(i,{children:[t.jsx(c,{onClick:()=>{m(!1)},children:"No"}),t.jsx(c,{onClick:async()=>await async function(){const a=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n mutation deleteColumn($tableName: String!, $columnName: String!){\n deleteColumn(tableName: $tableName, columnName: $columnName)\n }\n ",variables:{columnName:e.label,tableName:e.parent}})}),t=await a.json();t.errors?(console.error(t.errors[0].message),alert(t.errors[0].message)):(await u(e.parent),m(!1))}(),autoFocus:!0,children:"Yes"})]})]})]})};H.displayName="DeleteColumnButton";const Q=({data:e})=>{const[a,l]=d.useState(!1),[s,r]=d.useState(e.label),o=Z((e=>e.fetchAndUpdateTableDetails));return t.jsxs("div",{className:"column-name-node",children:[t.jsx(m,{type:"target",position:u.Left}),t.jsx(m,{type:"source",position:u.Right}),a?t.jsx("input",{type:"text",value:s,onChange:e=>{r(e.currentTarget.value)},placeholder:e.label,className:"table-name-input"}):t.jsx(h,{className:"column-label",variant:"body2",noWrap:!0,children:s}),a?t.jsxs(p,{sx:{minWidth:56},children:[t.jsx(n,{"aria-label":"edit",size:"small",onClick:async()=>{l(!1),r(s.trim().replace(/[^A-Za-z0-9_]/g,"_"));const a=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n mutation editColumn($newColumnName: String!, $columnName: String!, $tableName: String!){\n editColumn( newColumnName: $newColumnName, columnName: $columnName, tableName: $tableName)\n }\n ",variables:{newColumnName:s,columnName:e.label,tableName:e.parent}})}),t=await a.json();t.errors?(console.error(t.errors[0].message),alert(t.errors[0].message)):await o(e.parent)},children:t.jsx(b,{fontSize:"inherit"})}),t.jsx(n,{"aria-label":"cancel",size:"small",onClick:()=>{r(e.label),l(!1)},children:t.jsx(g,{fontSize:"inherit"})})]}):t.jsxs(p,{sx:{minWidth:56},children:[t.jsx(n,{"aria-label":"edit",size:"small",onClick:()=>{l(!0)},children:t.jsx(x,{fontSize:"inherit"})}),t.jsx(H,{data:e})]})]})};Q.displayName="ColumnNameNode";function ee(){const e=f(),[a,l]=d.useState(null),s=Boolean(a),r=()=>{l(null)},o=t.jsxs(w,{anchorEl:a,anchorOrigin:{vertical:"bottom",horizontal:"left"},id:"primary-search-account-menu",keepMounted:!0,open:s,onClose:r,children:[t.jsx(T,{onClick:async()=>{if(window.confirm("Do you want to download the file?"))try{const e="./migration_log.txt",a=await fetch(e),t=await a.blob(),n=document.createElement("a");n.href=URL.createObjectURL(t),n.download="migration_log.txt",n.click(),URL.revokeObjectURL(n.href)}catch(e){console.error("Error downloading file:",e)}else r()},children:"Download Migration File"}),t.jsx(T,{onClick:()=>{l(null),e("/")},children:"Logout"})]});return t.jsxs(p,{sx:{flexGrow:1},children:[t.jsx(j,{position:"static",style:{background:"rgba(0, 0, 0, .25)",boxShadow:"none"},children:t.jsxs(y,{children:[t.jsx(n,{size:"large",edge:"start",color:"inherit","aria-label":"open drawer",sx:{mr:2},onClick:e=>{l(e.currentTarget)},children:t.jsx(N,{})}),t.jsx("div",{className:"nav-title",children:t.jsxs("div",{id:"nav-text",className:"nav-title-text",children:["SQL",t.jsx("span",{children:"ens"})]})})]})}),o]})}function ae({handleAddColumnOpen:e,handleAlertOpen:a,handleEditTableName:l,anchorEl:s,handleClick:r,handleClose:o}){const i=Boolean(s),c=i?"simple-popover":void 0;return t.jsxs("div",{className:"table-menu-dots",children:[t.jsx(n,{"aria-label":"expandTableMenu",style:{color:"white"},onClick:r,children:t.jsx(C,{})}),t.jsxs(v,{id:c,open:i,anchorEl:s,onClose:o,anchorOrigin:{vertical:"bottom",horizontal:"left"},sx:{".MuiPaper-root":{backgroundColor:"rgba(200, 200, 200, 0.9)"}},children:[t.jsx(T,{onClick:l,style:{color:"black"},children:"Edit Table Name"}),t.jsx(T,{onClick:e,style:{color:"black"},children:"Add Column"}),t.jsx(T,{onClick:a,style:{color:"black"},children:"Delete Table"})]})]})}const te={PaperProps:{style:{maxHeight:224,width:250}}},ne=["bit(8)","bool","box","bytea","char(10)","cidr","circle","date","decimal (8, 2)","float4","float8","inet","int, int4","int2","int8","interval (6)","json","jsonb","line","lseg","macaddr","macaddr8","money","path","pg_lsn","pg_snapshot","point","polygon","serial2","serial4","serial8","text","time (3) without time zone","timestamp (6) without time zone","timestamptz","timetz","tsquery","tsvector","txid_snapshot","uuid","varbit(16)","varchar (255)","xml"];function le({handleDataTypeChange:e,selectedDataType:a}){return t.jsx("div",{children:t.jsx(S,{sx:{m:1,width:300,mt:3},children:t.jsxs($,{displayEmpty:!0,value:a,onChange:e,input:t.jsx(k,{}),MenuProps:te,inputProps:{"aria-label":"Without label"},children:[t.jsx(T,{disabled:!0,value:"",children:t.jsx("em",{children:"Select Data Type"})}),ne.map((e=>t.jsx(T,{value:e,children:e},e)))]})})})}function se({tableName:e,handleAddColumnClose:n,openColDialog:l}){const[o,m]=d.useState(""),[u,h]=d.useState(""),p=Z((e=>e.fetchAndUpdateTableDetails));return t.jsx(a.Fragment,{children:t.jsxs(s,{open:l,onClose:n,children:[t.jsxs(r,{children:[t.jsx(E,{children:"Add Column and Data Type"}),t.jsx(O,{autoFocus:!0,required:!0,margin:"dense",id:"columnName",name:"columnName",label:"Column Name",type:"text",variant:"standard",onChange:e=>{m(e.currentTarget.value)},fullWidth:!0}),t.jsx(le,{handleDataTypeChange:e=>{h(e.target.value)},selectedDataType:u})]}),t.jsxs(i,{children:[t.jsx(c,{onClick:n,children:"Cancel"}),t.jsx(c,{onClick:async()=>{n(),m(o.trim().replace(/[^A-Za-z0-9_]/g,"_"));const a=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n mutation addColumnToTable($tableName: String!, $columnName: String!, $dataType: String!){\n addColumnToTable( tableName: $tableName, columnName: $columnName, dataType: $dataType)\n }\n ",variables:{tableName:e,columnName:o,dataType:u}})}),t=await a.json();t.errors?(console.error(t.errors[0].message),alert(t.errors[0].message)):p(e)},children:"Save"})]})]})})}const re=({data:e})=>{const[l,m]=a.useState(!1),[u,x]=d.useState(!1),[f,j]=d.useState(e.label),[y,N]=a.useState(null),w=Z((e=>e.fetchAndUpdateTableDetails)),T=Z((e=>e.tables)),C=Z((e=>e.setTables)),v=()=>{N(null)},[S,$]=a.useState(!1),k=()=>{v(),$(!0)};return t.jsxs(p,{className:"group-node",sx:{display:"flex",flexDirection:"column",alignItems:"stretch",justifyContent:"space-between",width:1},children:[u?t.jsxs(p,{sx:{display:"flex",alignItems:"center",justifyContent:"space-between"},children:[t.jsx("input",{type:"text",value:f,onChange:e=>{j(e.currentTarget.value)},placeholder:e.label,className:"table-name-input",autoFocus:!0}),t.jsxs("div",{children:[t.jsx(n,{"aria-label":"edit",size:"small",onClick:async()=>{j(f.trim().replace(/[^A-Za-z0-9_]/g,"_"));const a=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n mutation editTableName($oldName: String!, $newName: String!){\n editTableName( oldName: $oldName, newName: $newName)\n }\n ",variables:{newName:f,oldName:e.label}})}),t=await a.json();t.errors?(j(e.label),console.error(t.errors[0].message),alert(t.errors[0].message)):(x(!1),await w(f,e.label))},children:t.jsx(b,{fontSize:"inherit"})}),t.jsx(n,{"aria-label":"cancel",size:"small",onClick:()=>{j(e.label),x(!1)},children:t.jsx(g,{fontSize:"inherit"})})]})]}):t.jsxs(p,{sx:{display:"flex",alignItems:"center",justifyContent:"space-between"},children:[t.jsx(h,{variant:"h6",noWrap:!0,sx:{flexGrow:1,maxWidth:200},children:f}),t.jsx(ae,{handleEditTableName:()=>{v(),x(!0)},anchorEl:y,handleClose:v,handleClick:e=>{N(e.currentTarget)},handleAlertOpen:()=>{m(!0)},handleAddColumnOpen:k})]}),t.jsxs(s,{open:l,onClose:()=>m(!1),"aria-describedby":"alert-dialog-description",children:[t.jsx(r,{children:t.jsx(o,{id:"alert-dialog-description",children:"Are you sure you want to delete this table?"})}),t.jsxs(i,{children:[t.jsx(c,{onClick:()=>{m(!1)},children:"No"}),t.jsx(c,{onClick:async()=>{m(!1);const a=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n mutation deleteTable($tableName: String!){\n deleteTable( tableName: $tableName)\n }\n ",variables:{tableName:e.label}})}),t=await a.json();if(t.errors)throw console.error(t.errors[0].message),alert(t.errors[0].message),new Error("Error deleting table");{const a=T.filter((e=>e.name!==f));C(a),w(e.label)}},autoFocus:!0,children:"Yes"})]})]}),t.jsx(se,{tableName:f,handleAddColumnOpen:k,openColDialog:S,handleAddColumnClose:()=>{$(!1)}})]})},oe=({data:e})=>{const[a,l]=d.useState(!1),[s,r]=d.useState(""),o=Z((e=>e.fetchAndUpdateTableDetails)),i=Z((e=>e.tables)),c=Z((e=>e.setTables));return t.jsx(p,{className:"group-node",sx:{display:"flex",flexDirection:"column",alignItems:"stretch",justifyContent:"space-between",width:1},children:a?t.jsxs(p,{sx:{display:"flex",alignItems:"center",justifyContent:"space-between"},children:[t.jsx("input",{type:"text",value:s,onChange:e=>{r(e.currentTarget.value)},placeholder:e.label,className:"table-name-input"}),t.jsxs(p,{children:[t.jsx(n,{"aria-label":"save",size:"small",onClick:async()=>{l(!1),r(s.trim().replace(/[^A-Za-z0-9_]/g,"_"));const a=await fetch("/api/graphql",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:"\n mutation addTable($tableName: String!){\n addTable( tableName: $tableName)\n }\n\n ",variables:{tableName:s}})}),t=await a.json();if(t.errors)console.error(t.errors[0].message),alert(t.errors[0].message);else{c([...i,{name:s,columns:[],foreignKeys:[]}]),r(e.label),await o(e.label)}},children:t.jsx(b,{fontSize:"inherit"})}),t.jsx(n,{"aria-label":"cancel",size:"small",onClick:()=>{l(!1)},children:t.jsx(g,{fontSize:"inherit"})})]})]}):t.jsxs(p,{sx:{display:"flex",alignItems:"center",justifyContent:"space-between"},children:[t.jsx(h,{className:"column-label",variant:"h6",noWrap:!0,children:e.label}),t.jsx(n,{"aria-label":"edit",size:"small",onClick:()=>{l(!0)},children:t.jsx(D,{fontSize:"inherit"})})]})})};function ie({data:e}){return t.jsxs(t.Fragment,{children:[t.jsx("div",{className:"cloud gradient",children:t.jsx("div",{children:t.jsx(z,{})})}),t.jsx("div",{className:"wrapper gradient",children:t.jsx("div",{className:"inner",children:"Add New Table"===e.label?t.jsx(oe,{data:e}):t.jsx(re,{data:e})})})]})}ie.displayName="TurboNode";const ce={turbo:ie,colNode:Q},de={turbo:function({id:e,sourceX:a,sourceY:n,targetX:l,targetY:s,sourcePosition:r,targetPosition:o,style:i={},markerEnd:c}){const d=a===l,m=n===s,[u]=A({sourceX:d?a+1e-4:a,sourceY:m?n+1e-4:n,sourcePosition:r,targetX:l,targetY:s,targetPosition:o});return t.jsx(t.Fragment,{children:t.jsx("path",{id:e,style:i,className:"react-flow__edge-path",d:u,markerEnd:c})})}},me={type:"turbo",markerEnd:"edge-circle"},ue=()=>{const[e,a,n]=q([]),[l,s,r]=P([]),o=Z((e=>e.tables)),i=d.useCallback((e=>s((a=>U(e,a)))),[s]);d.useEffect((()=>{if(o.length>0){const t=(e=>{const a=[];let t=0,n=0;e.forEach((e=>{n>600&&(n=0,t+=375);const l={id:`table-${e.name}`,type:"turbo",data:{label:e.name},className:"light",position:{x:t,y:n},style:{width:250,height:75+40*e.columns.length}};a.push(l);let s=60;e.columns.forEach((t=>{const n={id:`table-${e.name}-column-${t}`,data:{label:t,parent:e.name},type:"colNode",position:{x:15,y:s},parentNode:`table-${e.name}`,draggable:!1,extent:"parent",style:{width:220,height:40}};a.push(n),s+=40})),n+=150+40*e.columns.length}));const l={id:"add-table-node",type:"turbo",data:{label:"Add New Table"},position:{x:t,y:n},style:{width:250}};return a.push(l),a})(o),n=(e=>{const a=[];return e.forEach(((e,t)=>{e.foreignKeys.forEach(((n,l)=>{const s={id:`fk-${t}-${l}`,target:`table-${e.name}-column-${n.columnName}`,source:`table-${n.foreignTableName}-column-${n.foreignColumnName}`,animated:!1,style:{}};a.push(s)}))})),a})(o),l=t.map((a=>{const t=e.find((e=>e.id===a.id&&!a.id.includes("-column-")));return t?{...a,position:t.position}:a}));a(l),s(n)}}),[o,a,s]);return t.jsxs(t.Fragment,{children:[t.jsx(ee,{}),t.jsx("div",{className:"flow-container","data-testid":"flow-container",children:t.jsx(I,{nodes:e,edges:l,onNodesChange:n,onEdgesChange:r,onConnect:i,edgeTypes:de,defaultEdgeOptions:me,nodeTypes:ce,fitView:!0,deleteKeyCode:null,proOptions:{hideAttribution:!0},children:t.jsx("svg",{children:t.jsxs("defs",{children:[t.jsxs("linearGradient",{id:"edge-gradient",children:[t.jsx("stop",{offset:"0%",stopColor:"#ae53ba"}),t.jsx("stop",{offset:"100%",stopColor:"#2a8af6"})]}),t.jsx("marker",{id:"edge-circle",viewBox:"-5 -5 10 10",refX:"0",refY:"0",markerUnits:"strokeWidth",markerWidth:"10",markerHeight:"10",orient:"auto",children:t.jsx("circle",{stroke:"#2a8af6",strokeOpacity:"0.75",r:"2",cx:"0",cy:"0"})})]})})})})]})},he=()=>t.jsxs("div",{className:"ocean",children:[t.jsx("div",{className:"wave"}),t.jsx("div",{className:"wave wave2"})]}),pe=()=>{const e=d.useRef(null),a=f(),{fetchTables:l}=Z((e=>({fetchTables:e.fetchTables})));return d.useEffect((()=>{_.timeline({autoplay:!0,delay:200}).add({targets:e.current,translateY:[-100,0],opacity:[0,1],elasticity:600,duration:800}).add({targets:"#logo-hexagon",rotate:[-90,0],duration:600,elasticity:600,offset:50}).add({targets:"#logo-circle",scale:[0,1],duration:600,elasticity:600,offset:250}).add({targets:"#logo-text",translateX:["-100%",0],opacity:[0,1],duration:500,easing:"easeOutExpo",offset:500})}),[]),t.jsxs("div",{className:"outer-container","data-testid":"outer-container",children:[t.jsx("div",{className:"logo-container",children:t.jsxs("div",{className:"site-logo",children:[t.jsx("figure",{id:"logo",ref:e,children:t.jsxs("svg",{width:"100%",height:"100%",viewBox:"0 0 148 128",children:[t.jsx("defs",{children:t.jsxs("mask",{id:"circle-mask",children:[t.jsx("rect",{fill:"white",width:"100%",height:"100%"}),t.jsx("circle",{id:"logo-mask",fill:"black",cx:"120",cy:"96",r:"28"})]})}),t.jsx("polygon",{id:"logo-hexagon",fill:"#646cff",points:"64 128 8.574 96 8.574 32 64 0 119.426 32 119.426 96",mask:"url(#circle-mask)"}),t.jsx("circle",{id:"logo-circle",fill:"#e92a67",cx:"120",cy:"96",r:"20"})]})}),t.jsx("div",{className:"site-title",children:t.jsxs("div",{id:"logo-text",className:"site-title-text",children:["SQL",t.jsx("span",{children:"ens"})]})})]})}),t.jsxs("div",{className:"input-container",children:[t.jsxs("form",{style:{display:"flex",alignItems:"center"},onSubmit:async e=>{e.preventDefault();const t=e.target.input.value;try{const e=await fetch("/api/setDatabaseUri",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({databaseURI:t})}),n=await e.json();e.ok&&"Database connection updated successfully"===n.message?(await l(),a("/flow")):console.error(n.error||"Failed to update database URI")}catch(n){console.error("Error:",n)}},children:[t.jsx(O,{name:"input",type:"input",className:"moving-shadow",label:"URI String",placeholder:"Enter URI here",variant:"outlined",style:{width:"40vw",color:"#646cff",fontSize:"20px",marginTop:"5px"},InputProps:{style:{color:"#646cff"}},InputLabelProps:{style:{color:"#646cff"}}}),t.jsx(n,{style:{marginTop:"5px",color:"#646cff"},type:"submit",children:t.jsx(F,{fontSize:"large"})})]}),t.jsx("div",{style:{height:"58px",width:"40vw",margin:"50px 0"}})]}),t.jsx("div",{className:"wave-container"}),t.jsx(he,{})]})},be=L`
2 | query {
3 | getTableNames {
4 | name
5 | columns
6 | foreignKeys {
7 | columnName
8 | foreignTableName
9 | foreignColumnName
10 | }
11 | }
12 | }
13 | `;L`
14 | query GetTableData($tableName: String!) {
15 | getTableData(tableName: $tableName) {
16 | columnData
17 | }
18 | }
19 | `,L`
20 | query GetTableDetails($tableName: String!) {
21 | getTableDetails(tableName: $tableName) {
22 | name
23 | columns
24 | foreignKeys {
25 | columnName
26 | foreignTableName
27 | foreignColumnName
28 | }
29 | }
30 | }
31 | `,L`
32 | mutation AddColumnToTable(
33 | $tableName: String!
34 | $columnName: String!
35 | $dataType: String!
36 | ) {
37 | addColumnToTable(
38 | tableName: $tableName
39 | columnName: $columnName
40 | dataType: $dataType
41 | )
42 | }
43 | `,L`
44 | mutation EditTableName($oldName: String!, $newName: String!) {
45 | editTableName(oldName: $oldName, newName: $newName)
46 | }
47 | `,L`
48 | mutation DeleteTable($tableName: String!) {
49 | deleteTable(tableName: $tableName)
50 | }
51 | `,L`
52 | mutation DeleteColumn($tableName: String!, $columnName: String!) {
53 | deleteColumn(tableName: $tableName, columnName: $columnName)
54 | }
55 | `;const ge=R({palette:{mode:"dark"},typography:{fontFamily:"Fira Mono"}});function xe({router:e=Y}){const a=Z((e=>e.setTables)),{data:n,loading:l,error:s}=W(be,{onCompleted:e=>{a(e.getTableNames)}});return d.useEffect((()=>{!n||l||s||a(n.getTableNames)}),[n,l,s,a]),t.jsx(J,{theme:ge,children:t.jsx(e,{children:t.jsxs(K,{children:[t.jsx(M,{path:"/",element:t.jsx(pe,{})}),t.jsx(M,{path:"/flow",element:t.jsx(ue,{})})]})})})}const fe=new B({uri:"/api/graphql",cache:new G});X.createRoot(document.getElementById("root")).render(t.jsx(a.StrictMode,{children:t.jsx(V,{client:fe,children:t.jsx(xe,{})})}));
56 |
--------------------------------------------------------------------------------
/dist/assets/vendors-bGEdEbVg.css:
--------------------------------------------------------------------------------
1 | .react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background:#1a192b;border:1px solid white;border-radius:100%}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-default,.react-flow__node-input,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:3px;width:150px;font-size:12px;color:#222;text-align:center;border-width:1px;border-style:solid;border-color:#1a192b;background-color:#fff}.react-flow__node-default.selectable:hover,.react-flow__node-input.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:0 1px 4px 1px #00000014}.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:0 0 0 .5px #1a192b}.react-flow__node-group{background-color:#f0f0f040}.react-flow__nodesselection-rect,.react-flow__selection{background:#0059dc14;border:1px dotted rgba(0,89,220,.8)}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1;cursor:-webkit-grab;cursor:grab}.react-flow__pane.selection{cursor:pointer}.react-flow__pane.dragging{cursor:-webkit-grabbing;cursor:grabbing}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow .react-flow__edges{pointer-events:none;overflow:visible}.react-flow__edge-path,.react-flow__connection-path{stroke:#b1b1b7;stroke-width:1;fill:none}.react-flow__edge{pointer-events:visibleStroke;cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;-webkit-animation:dashdraw .5s linear infinite;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;-webkit-animation:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge:focus .react-flow__edge-path,.react-flow__edge:focus-visible .react-flow__edge-path{stroke:#555}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge-textbg{fill:#fff}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;-webkit-animation:dashdraw .5s linear infinite;animation:dashdraw .5s linear infinite}.react-flow__connectionline{z-index:1001}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:-webkit-grab;cursor:grab}.react-flow__node.dragging{cursor:-webkit-grabbing;cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:-webkit-grab;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;background-color:#333}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:-4px;transform:translate(-50%)}.react-flow__handle-top{left:50%;top:-4px;transform:translate(-50%)}.react-flow__handle-left{top:50%;left:-4px;transform:translateY(-50%)}.react-flow__handle-right{right:-4px;top:50%;transform:translateY(-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.center{left:50%;transform:translate(-50%)}.react-flow__attribution{font-size:10px;background:#ffffff80;padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@-webkit-keyframes dashdraw{0%{stroke-dashoffset:10}}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__node-default,.react-flow__node-input,.react-flow__node-output,.react-flow__node-group{border-width:1px;border-style:solid;border-color:#bbb}.react-flow__node-default.selected,.react-flow__node-default:focus,.react-flow__node-default:focus-visible,.react-flow__node-input.selected,.react-flow__node-input:focus,.react-flow__node-input:focus-visible,.react-flow__node-output.selected,.react-flow__node-output:focus,.react-flow__node-output:focus-visible,.react-flow__node-group.selected,.react-flow__node-group:focus,.react-flow__node-group:focus-visible{outline:none;border:1px solid #555}.react-flow__nodesselection-rect,.react-flow__selection{background:#9696b41a;border:1px dotted rgba(155,155,155,.8)}.react-flow__controls{box-shadow:0 0 2px 1px #00000014}.react-flow__controls-button{border:none;background:#fefefe;border-bottom:1px solid #eee;box-sizing:content-box;display:flex;justify-content:center;align-items:center;width:16px;height:16px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;padding:5px}.react-flow__controls-button:hover{background:#f4f4f4}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__minimap{background-color:#fff}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:4px;height:4px;border:1px solid #fff;border-radius:1px;background-color:#3367d9;transform:translate(-50%,-50%)}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:#3367d9;border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}
2 |
--------------------------------------------------------------------------------
/dist/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SQLens/1ac24699452953c0bc6fed2da1a9fc011cbf00db/dist/favicon.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SQLens
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/express-plugin.ts:
--------------------------------------------------------------------------------
1 | import {Request, Response, NextFunction } from 'express';
2 |
3 | export default function express(path: string) {
4 | return {
5 | name: "vite3-plugin-express",
6 | configureServer: async (server) => {
7 | server.middlewares.use(async (req: Request, res: Response, next: NextFunction) => {
8 | process.env["VITE"] = "true";
9 | try {
10 | const { app } = await server.ssrLoadModule(path);
11 | app(req, res, next);
12 | } catch (err) {
13 | console.error(err);
14 | }
15 | });
16 | },
17 | };
18 | }
--------------------------------------------------------------------------------
/global_types/types.d.ts:
--------------------------------------------------------------------------------
1 | export interface TableState {
2 | tables: Table[];
3 | searchValue: string;
4 | databaseURI?: string;
5 | setSearchValue: (searchValue: string) => void;
6 | setTables: (tables: Table[]) => void;
7 | fetchTables: () => Promise;
8 | fetchAndUpdateTableDetails: (
9 | tableName: string,
10 | oldName?: string
11 | ) => Promise;
12 | updateColumnName: (
13 | tableName: string,
14 | columnName: string,
15 | newColumnName: string
16 | ) => Promise;
17 | addColumn: (
18 | tableName: string,
19 | columnName: string,
20 | dataType: string,
21 | refTable: string,
22 | refColumn: string
23 | ) => Promise;
24 | addTable: (tableName: string) => Promise;
25 | deleteColumn: (tableName: string, columnName: string) => Promise;
26 | deleteTable: (tableName: string) => Promise;
27 | editTable: (tableName: string, newTableName: string) => Promise;
28 | }
29 |
30 | export interface ForeignKey {
31 | columnName: string;
32 | foreignTableName: string;
33 | foreignColumnName: string;
34 | }
35 |
36 | export interface Table {
37 | name: string;
38 | columns: string[];
39 | foreignKeys: ForeignKey[];
40 | }
41 |
42 | export interface Edge {
43 | id: string;
44 | source: string;
45 | target: string;
46 | type?: string;
47 | animated: boolean;
48 | style?: { stroke?: string };
49 | }
50 |
51 | export interface RowData {
52 | columnName: string;
53 | value: string | null;
54 | }
55 |
56 | export interface TableData {
57 | rowData: RowData[];
58 | }
59 |
60 | export interface Test {
61 | globals: boolean;
62 | environment: string;
63 | }
64 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SQLens
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sqlens",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --host --port 5173 --open",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "start": "tsx ./server/index.ts",
12 | "test": "vitest"
13 | },
14 | "dependencies": {
15 | "@apollo/client": "^3.9.2",
16 | "@apollo/server": "^4.10.5",
17 | "@apollo/utils.fetcher": "^3.1.0",
18 | "@emotion/react": "^11.11.3",
19 | "@emotion/styled": "^11.11.0",
20 | "@mui/icons-material": "^5.15.5",
21 | "@mui/material": "^5.15.5",
22 | "@types/jest": "^29.5.12",
23 | "animejs": "^3.2.2",
24 | "apollo-server": "^3.13.0",
25 | "apollo-server-express": "^3.13.0",
26 | "concurrently": "^8.2.2",
27 | "dotenv": "^16.3.1",
28 | "enzyme": "^3.11.0",
29 | "express": "^4.18.2",
30 | "graphql": "^16.8.1",
31 | "happy-dom": "^13.3.8",
32 | "lodash": "^4.17.21",
33 | "open": "^10.0.3",
34 | "pg": "^8.11.3",
35 | "react": "^18.2.0",
36 | "react-dom": "^18.2.0",
37 | "react-icons": "^5.0.1",
38 | "react-router-dom": "^6.21.3",
39 | "reactflow": "^11.10.1",
40 | "ts-node": "^10.9.2",
41 | "zustand": "^4.5.0"
42 | },
43 | "devDependencies": {
44 | "@cfaester/enzyme-adapter-react-18": "^0.7.1",
45 | "@testing-library/jest-dom": "^6.4.1",
46 | "@testing-library/react": "^14.2.1",
47 | "@types/animejs": "^3.1.12",
48 | "@types/compression": "^1.7.5",
49 | "@types/cors": "^2.8.17",
50 | "@types/enzyme": "^3.10.18",
51 | "@types/express": "^4.17.21",
52 | "@types/graphql": "^14.5.0",
53 | "@types/jsdom": "^21.1.6",
54 | "@types/node": "^20.11.10",
55 | "@types/pg": "^8.10.9",
56 | "@types/react": "^18.2.43",
57 | "@types/react-dom": "^18.2.17",
58 | "@typescript-eslint/eslint-plugin": "^6.14.0",
59 | "@typescript-eslint/parser": "^6.14.0",
60 | "@vitejs/plugin-react": "^4.2.1",
61 | "@vitest/ui": "^1.2.2",
62 | "eslint": "^8.55.0",
63 | "eslint-plugin-react": "^7.33.2",
64 | "eslint-plugin-react-hooks": "^4.6.0",
65 | "eslint-plugin-react-refresh": "^0.4.5",
66 | "jsdom": "^24.0.0",
67 | "sass": "^1.70.0",
68 | "terser": "^5.27.0",
69 | "tsx": "^4.7.0",
70 | "typescript": "^5.3.3",
71 | "vite": "^5.0.8",
72 | "vitest": "^1.2.2"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SQLens/1ac24699452953c0bc6fed2da1a9fc011cbf00db/public/favicon.png
--------------------------------------------------------------------------------
/server/controller.ts:
--------------------------------------------------------------------------------
1 | import { pool } from "./index";
2 | import { promises as fsPromises } from "fs";
3 | import { RowData, TableData, Table } from "../global_types/types";
4 |
5 | let migration_file = "./public/migration_log.txt";
6 | if (!process.env["VITE"]) {
7 | migration_file = "./dist/migration_log.txt"; //route need to be changed for production vs dev mode
8 | }
9 |
10 | const appendMigration = async (query: string): Promise => {
11 | await fsPromises.appendFile(migration_file, query + "\n");
12 | return;
13 | };
14 |
15 | export const resolvers = {
16 | Query: {
17 | getTableDetails: async (
18 | _: unknown,
19 | { tableName }: { tableName: string }
20 | ): Promise => {
21 | if (!pool) {
22 | throw new Error("Database connection not initialized");
23 | }
24 |
25 | try {
26 | const tableDetails: Table = {
27 | name: tableName,
28 | columns: [],
29 | foreignKeys: [],
30 | };
31 |
32 | // Fetch columns for the specified table
33 | const columnQuery = `SELECT column_name FROM information_schema.columns WHERE table_name = $1`;
34 | const columnData = await pool.query(columnQuery, [tableName]);
35 | tableDetails.columns = columnData.rows.map((row) => row.column_name);
36 |
37 | // Fetch foreign keys for the specified table
38 | const fkQuery = `
39 | SELECT
40 | kcu.column_name,
41 | ccu.table_name AS foreign_table_name,
42 | ccu.column_name AS foreign_column_name
43 | FROM
44 | information_schema.table_constraints AS tc
45 | JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
46 | JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
47 | WHERE
48 | tc.constraint_type = 'FOREIGN KEY'
49 | AND tc.table_name = $1;
50 | `;
51 | const fkData = await pool.query(fkQuery, [tableName]);
52 |
53 | // Map foreign key data to the foreignKeys array in the correct format
54 | tableDetails.foreignKeys = fkData.rows.map((fk) => ({
55 | columnName: fk.column_name,
56 | foreignTableName: fk.foreign_table_name,
57 | foreignColumnName: fk.foreign_column_name,
58 | }));
59 |
60 | return tableDetails;
61 | } catch (err) {
62 | console.error("Error in getTableDetails resolver:", err);
63 | throw new Error("Server error");
64 | }
65 | },
66 | getTableNames: async () => {
67 | if (!pool) {
68 | throw new Error("Database connection not initialized");
69 | }
70 | try {
71 | // Query to get all table names
72 | const tablesData = await pool.query(
73 | "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public'"
74 | );
75 | // tablesData.rows is an array of objects, each object has a key of tablename and a value of the table name
76 | const tableNames: string[] = tablesData.rows.map(
77 | (row) => row.tablename
78 | );
79 | // tablesWithColumns is an object with keys of table names and values of objects with keys of columns and foreignKeys
80 | const tablesWithColumns: {
81 | [key: string]: { columns: string[]; foreignKeys: unknown[] };
82 | } = {};
83 |
84 | // Query to get foreign key relations
85 | // tc = table constraints
86 | // A constraint type is a rule that is enforced on data in a table
87 | // A foreign key constraint is a constraint that references a column in another table
88 | // kcu = key column usage
89 | // A key column usage is a column that is used as a key
90 | // ccu = constraint column usage
91 | // A constraint column usage is a column that is used as a constraint
92 | // The join tables are used to get the table name, column name, foreign table name, and foreign column name
93 | // The WHERE clause is used to only get foreign key constraints
94 |
95 | const fkQuery = `
96 | SELECT
97 | tc.table_name,
98 | kcu.column_name,
99 | ccu.table_name AS foreign_table_name,
100 | ccu.column_name AS foreign_column_name
101 | FROM
102 | information_schema.table_constraints AS tc
103 | JOIN information_schema.key_column_usage AS kcu
104 | ON tc.constraint_name = kcu.constraint_name
105 | JOIN information_schema.constraint_column_usage AS ccu
106 | ON ccu.constraint_name = tc.constraint_name
107 | WHERE
108 | constraint_type = 'FOREIGN KEY';
109 | `;
110 | const fkData = await pool.query(fkQuery);
111 |
112 | // Loop to create tablesWithColumns object
113 | for (const tableName of tableNames) {
114 | const columnData = await pool.query(
115 | `SELECT column_name FROM information_schema.columns WHERE table_name = '${tableName}'`
116 | );
117 | tablesWithColumns[tableName] = {
118 | columns: columnData.rows.map((row) => row.column_name),
119 | foreignKeys: [],
120 | };
121 | }
122 |
123 | // Loop to add foreign keys to tablesWithColumns object
124 | fkData.rows.forEach((fk) => {
125 | if (tablesWithColumns[fk.table_name]) {
126 | tablesWithColumns[fk.table_name].foreignKeys.push({
127 | columnName: fk.column_name,
128 | foreignTableName: fk.foreign_table_name,
129 | foreignColumnName: fk.foreign_column_name,
130 | });
131 | }
132 | });
133 |
134 | // Convert tablesWithColumns object to array
135 | // Expected structure is an array of objects, each object has a key of name, columns, and foreignKeys
136 | const tablesArray = Object.keys(tablesWithColumns).map((tableName) => {
137 | return {
138 | name: tableName,
139 | columns: tablesWithColumns[tableName].columns,
140 | foreignKeys: tablesWithColumns[tableName].foreignKeys,
141 | };
142 | });
143 |
144 | return tablesArray;
145 | } catch (err) {
146 | console.error("Error in getTableNames resolver: ", err);
147 | throw new Error("Server error");
148 | }
149 | },
150 | // the :_ is a placeholder for the parent object which is a neccassary argument for the resolver with apollo server
151 | getTableData: async (
152 | _: unknown,
153 | { tableName }: { tableName: string }
154 | ): Promise => {
155 | if (pool !== null) {
156 | try {
157 | const tableDataQuery = `SELECT * FROM ${tableName};`;
158 | const tableDataResult = await pool.query(tableDataQuery);
159 |
160 | return tableDataResult.rows.map((row: Record) => {
161 | const rowData: RowData[] = [];
162 | for (const [key, value] of Object.entries(row)) {
163 | rowData.push({
164 | columnName: key,
165 | value:
166 | value !== null && value !== undefined
167 | ? value.toString()
168 | : null,
169 | });
170 | }
171 | return { rowData };
172 | });
173 | } catch (err) {
174 | console.error("Error in getTableData resolver: ", err);
175 | throw new Error("Server error");
176 | }
177 | }
178 | return [];
179 | },
180 | },
181 | Mutation: {
182 | addColumnToTable: async (
183 | // the :_ is a placeholder for the parent object which is a neccassary argument for the resolver with apollo server
184 | _: unknown,
185 | {
186 | tableName,
187 | columnName,
188 | dataType,
189 | refTable,
190 | refColumn,
191 | }: { tableName: string; columnName: string; dataType: string; refTable: string; refColumn: string }
192 | ) => {
193 | if (pool !== null) {
194 | try {
195 | // SQL to add a column to a table, adjust data type as needed
196 | let mutation = `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${dataType};`;
197 | if (refTable.length && refColumn.length) {
198 | mutation += `ALTER TABLE ${tableName} ADD CONSTRAINT fk_${columnName} FOREIGN KEY (${columnName}) REFERENCES ${refTable}(${refColumn})`;
199 | }
200 | await pool.query(mutation);
201 | await appendMigration(mutation);
202 | return `Column ${columnName} added to ${tableName} successfully.`;
203 | } catch (err) {
204 | console.error("Error in addColumnToTable resolver: ", err);
205 | return err;
206 | }
207 | }
208 | },
209 | editTableName: async (
210 | // the :_ is a placeholder for the parent object which is a neccassary argument for the resolver with apollo server
211 | _: unknown,
212 | { oldName, newName }: { oldName: string; newName: string }
213 | ) => {
214 | if (pool !== null) {
215 | try {
216 | // SQL to rename a table
217 | const mutation = `ALTER TABLE ${oldName} RENAME TO ${newName};`;
218 | await pool.query(mutation);
219 | await appendMigration(mutation);
220 | return `Table name changed from ${oldName} to ${newName} successfully.`;
221 | } catch (err) {
222 | console.error("Error in editTableName resolver: ", err);
223 | return err;
224 | }
225 | }
226 | },
227 | // the :_ is a placeholder for the parent object which is a neccassary argument for the resolver with apollo server
228 | deleteTable: async (_: unknown, { tableName }: { tableName: string }) => {
229 | if (pool !== null) {
230 | try {
231 | // SQL to delete a table
232 | const mutation = `DROP TABLE ${tableName};`;
233 | await pool.query(mutation);
234 | await appendMigration(mutation);
235 | return `Table ${tableName} deleted successfully.`;
236 | } catch (err) {
237 | console.error("Error in deleteTable resolver: ", err);
238 | return err;
239 | }
240 | }
241 | },
242 | deleteColumn: async (
243 | _: unknown,
244 | { columnName, tableName }: { columnName: string; tableName: string }
245 | ) => {
246 | if (pool !== null) {
247 | try {
248 | // SQL to delete a table
249 | const mutation = `ALTER TABLE ${tableName} DROP COLUMN ${columnName};`;
250 | await pool.query(mutation);
251 | await appendMigration(mutation);
252 | return `Column ${columnName} deleted successfully from ${tableName}.`;
253 | } catch (err) {
254 | console.error("Error in deleteColumn resolver: ", err);
255 | return err;
256 | }
257 | }
258 | },
259 | editColumn: async (
260 | _: unknown,
261 | {
262 | newColumnName,
263 | columnName,
264 | tableName,
265 | }: { newColumnName: string; columnName: string; tableName: string }
266 | ) => {
267 | if (pool !== null) {
268 | try {
269 | // SQL to delete a table
270 | const mutation = `ALTER TABLE ${tableName}
271 | RENAME COLUMN ${columnName} to ${newColumnName};`;
272 | await pool.query(mutation);
273 | await appendMigration(mutation);
274 | return `Column name changed to${newColumnName} from ${columnName} on ${tableName}.`;
275 | } catch (err) {
276 | console.error("Error in editColumn resolver: ", err);
277 | return err;
278 | }
279 | }
280 | },
281 | addTable: async (_: unknown, { tableName }: { tableName: string }) => {
282 | if (pool !== null) {
283 | try {
284 | // SQL to delete a table
285 | const mutation = `CREATE TABLE ${tableName} (
286 | );`;
287 | await pool.query(mutation);
288 | await appendMigration(mutation);
289 | return `Table named ${tableName} created.`;
290 | } catch (err) {
291 | console.error("Error in addTable resolver: ", err);
292 | return err;
293 | }
294 | }
295 | },
296 | },
297 | };
298 |
--------------------------------------------------------------------------------
/server/db.ts:
--------------------------------------------------------------------------------
1 | // COMMMONJS SYNTAX
2 | // import { Pool } from 'pg';
3 |
4 | // ESMODULE SYNTAX
5 | // import pkg from 'pg';
6 | // const { Pool } = pkg;
7 |
8 | // import dotenv from 'dotenv';
9 |
10 | // dotenv.config();
11 |
12 | // const pool = new Pool({
13 | // connectionString: process.env.DATABASE_URI,
14 | // });
15 |
16 | // export default pool;
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | import express, { Response, Request } from 'express';
2 | import { ApolloServer } from '@apollo/server';
3 | import { expressMiddleware } from '@apollo/server/express4';
4 | import { resolvers } from './controller';
5 | import { typeDefs } from './typeDefs';
6 | import pkg from 'pg';
7 | const { Pool } = pkg;
8 | import dotenv from 'dotenv';
9 | import open from 'open';
10 | import fs from 'fs';
11 | import cors from 'cors';
12 |
13 | dotenv.config();
14 | export let pool: pkg.Pool | null = null;
15 |
16 | const initializePool = (uri: string) => {
17 | pool = new Pool({ connectionString: uri });
18 | };
19 |
20 | export const app = express();
21 | app.use(express.json());
22 |
23 | const server = new ApolloServer({ typeDefs, resolvers });
24 |
25 | let migration_file = './public/migration_log.txt'
26 | if (!process.env['VITE']) {
27 | migration_file = './dist/migration_log.txt' //route need to be changed for production vs dev mode
28 | }
29 |
30 | async function startServer() {
31 | await server.start();
32 | app.use('/api/graphql', cors(), express.json(), expressMiddleware(server));
33 |
34 | app.get('/api/test', (_: Request, res: Response) => {
35 | res.json({ greeting: 'Hello' });
36 | });
37 |
38 | app.post('/api/setDatabaseURI', (req: Request, res: Response) => {
39 | const { databaseURI } = req.body;
40 |
41 | if (!databaseURI) {
42 | return res.status(400).json({ error: 'Database URI is required' });
43 | }
44 |
45 | initializePool(databaseURI);
46 | const currentTimestamp = new Date().toLocaleString();
47 | fs.writeFileSync(
48 | migration_file, //route needs to be changed for production vs dev mode
49 | `--\n-- Migration log\n-- Database URL: ${databaseURI}\n-- Session started ${currentTimestamp}\n--\n`
50 | );
51 | res.json({ message: 'Database connection updated successfully' });
52 | });
53 |
54 | // Conditional Express static file serving and application start
55 | if (!process.env['VITE']) {
56 | const frontendFiles = process.cwd() + '/dist';
57 | app.use(express.static(frontendFiles));
58 | app.get('/*', (_: Request, res: Response) => {
59 | res.sendFile(`${frontendFiles}/index.html`);
60 | });
61 |
62 | const PORT = process.env.PORT || 3000;
63 | app.listen(PORT, () => {
64 | console.log(`Server running on http://localhost:${PORT}`);
65 | open(`http://localhost:${PORT}`);
66 | });
67 | }
68 | }
69 |
70 | startServer().catch((error) => {
71 | console.error('Failed to start the server:', error);
72 | });
73 |
--------------------------------------------------------------------------------
/server/typeDefs.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server-express';
2 |
3 | // Defines the GraphQL schema
4 | export const typeDefs = gql`
5 | type ForeignKey {
6 | columnName: String
7 | foreignTableName: String
8 | foreignColumnName: String
9 | }
10 |
11 | type Table {
12 | name: String
13 | columns: [String]
14 | foreignKeys: [ForeignKey]
15 | }
16 |
17 | type RowData {
18 | columnData: [String]
19 | }
20 |
21 |
22 | type Query {
23 | getTableNames: [Table]
24 | getTableData(tableName: String!): [RowData]
25 | getTableDetails(tableName: String!): Table
26 | }
27 |
28 | type Mutation {
29 | addColumnToTable(tableName: String!, columnName: String!, dataType: String!, refTable: String, refColumn: String): String
30 | editTableName(oldName: String!, newName: String!): String
31 | deleteTable(tableName: String!): String
32 | deleteColumn(tableName: String!, columnName: String!): String
33 | editColumn(newColumnName: String!, columnName: String!, tableName: String!): String
34 | addTable(tableName: String!): String
35 | }
36 | `;
37 |
38 | // If getTableNames is called, the resolver will return an array of objects, each object has a key of name, columns, and foreignKeys
39 | // If getTableData is called, the resolver will return an array of objects, each object has a key of columnData
40 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import './stylesheets/index.css';
3 | import Flow from './components/Flow';
4 | import LandingPage from './components/LandingPage';
5 | import { BrowserRouter as DefaultRouter, Routes, Route } from 'react-router-dom';
6 | import { ThemeProvider, createTheme } from '@mui/material/styles';
7 | import useStore from './store';
8 | import { useQuery } from '@apollo/client';
9 | import { GET_TABLE_NAMES } from './utilities/queries';
10 |
11 | const theme = createTheme({
12 | palette: {
13 | mode: 'dark',
14 | },
15 | typography: {
16 | fontFamily: 'Fira Mono',
17 | },
18 | });
19 |
20 |
21 |
22 | function App({ router: RouterComponent = DefaultRouter }) {
23 | const setTables = useStore((state) => state.setTables);
24 | const { data, loading, error } = useQuery(GET_TABLE_NAMES, {
25 | onCompleted: (data) => {
26 | setTables(data.getTableNames);
27 | },
28 | });
29 |
30 |
31 | useEffect(() => {
32 | if (data && !loading && !error) {
33 | setTables(data.getTableNames);
34 | }
35 | }, [data, loading, error, setTables]);
36 |
37 | return (
38 |
39 |
40 |
41 | } />
42 | } />
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | export default App;
50 |
--------------------------------------------------------------------------------
/src/assets/FiraMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/SQLens/1ac24699452953c0bc6fed2da1a9fc011cbf00db/src/assets/FiraMono-Regular.ttf
--------------------------------------------------------------------------------
/src/components/AddColumnDialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Button from "@mui/material/Button";
3 | import TextField from "@mui/material/TextField";
4 | import Dialog from "@mui/material/Dialog";
5 | import DialogActions from "@mui/material/DialogActions";
6 | import DialogContent from "@mui/material/DialogContent";
7 | import DialogTitle from "@mui/material/DialogTitle";
8 | import DataTypeSelector from "./DataTypeSelector";
9 | import { SelectChangeEvent } from "@mui/material";
10 | import useStore from "../store";
11 | import { TableState, Table } from "../../global_types/types";
12 | import OutlinedInput from "@mui/material/OutlinedInput";
13 | import MenuItem from "@mui/material/MenuItem";
14 | import FormControl from "@mui/material/FormControl";
15 | import FormControlLabel from "@mui/material/FormControlLabel";
16 | import Select from "@mui/material/Select";
17 | import Checkbox from "@mui/material/Checkbox";
18 |
19 | export interface AddColumnDialogProps {
20 | tableName: string;
21 | openColDialog: boolean;
22 | handleAddColumnClose: () => void;
23 | handleAddColumnOpen: () => void;
24 | }
25 |
26 | export default function AddColumnDialog({
27 | tableName,
28 | handleAddColumnClose,
29 | openColDialog,
30 | }: AddColumnDialogProps) {
31 | const [columnName, setColumnName] = useState("");
32 | const [selectedDataType, setSelectedDataType] = useState("");
33 | const [hasForeignKey, setHasForeignKey] = useState(false);
34 | const [fkTable, setFkTable] = useState(null);
35 | const [selectedTableName, setSelectedTableName] = useState("");
36 | const [columns, setColumns] = useState([]);
37 | const [fkColumn, setFkColumn] = useState("");
38 | const tables = useStore((state: TableState) => state.tables);
39 |
40 | const addColumn = useStore((state: TableState) => state.addColumn);
41 |
42 | const handleColumnNameChange = (
43 | event: React.ChangeEvent
44 | ) => {
45 | setColumnName(event.currentTarget.value);
46 | };
47 |
48 | const handleDataTypeChange = (event: SelectChangeEvent) => {
49 | setSelectedDataType(event.target.value);
50 | };
51 |
52 | const handleSaveClick = async () => {
53 | handleAddColumnClose();
54 | setColumnName(columnName.trim().replace(/[^A-Za-z0-9_]/g, "_"));
55 | await addColumn(tableName, columnName, selectedDataType, fkTable?.name || "", fkColumn);
56 | };
57 |
58 | const handleCheckboxClick = () => {
59 | setHasForeignKey(!hasForeignKey);
60 | };
61 |
62 | const handleTableSelect = (event: SelectChangeEvent) => {
63 | setFkColumn("");
64 | setSelectedTableName(event.target.value);
65 | tables.forEach((table) => {
66 | if (table.name === event.target.value) {
67 | setFkTable(table);
68 | setColumns(table.columns);
69 | }
70 | });
71 | };
72 |
73 | const handleColumnSelect = (event: SelectChangeEvent) => {
74 | setFkColumn(event.target.value);
75 | };
76 |
77 | return (
78 |
79 |
80 |
81 | Add Column and Data Type
82 | {/* Column name input field */}
83 |
95 | {/* Column data type selector drop down menu */}
96 |
101 | {/* Checkbox and label */}
102 |
110 | }
111 | label="Add Foreign Key Constraint"
112 | />
113 | {/* Foreign key table selector drop down menu */}
114 |
115 | }
120 | disabled={!hasForeignKey}
121 | >
122 |
123 | Table
124 |
125 | {tables.map(
126 | (table: Table) =>
127 | tableName !== table.name && (
128 |
129 | {table.name}
130 |
131 | )
132 | )}
133 |
134 |
135 |
136 | }
141 | disabled={!hasForeignKey}
142 | >
143 |
144 | Columns
145 |
146 | {columns.map((column: string) => (
147 |
148 | {column}
149 |
150 | ))}
151 |
152 |
153 |
154 |
155 | Cancel
156 | Save
157 |
158 |
159 |
160 | );
161 | }
162 |
--------------------------------------------------------------------------------
/src/components/AddTable.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from "react";
3 | import { Check } from "@mui/icons-material";
4 | import { IconButton, Typography, Box } from "@mui/material";
5 | import { ImPlus } from "react-icons/im";
6 | import ClearIcon from "@mui/icons-material/Clear";
7 | import useStore from "../store";
8 | import { TableState } from "../../global_types/types";
9 | // import e from "express";
10 |
11 | const AddTable = ({
12 | data,
13 | }: {
14 | data: {
15 | label: string;
16 | };
17 | }) => {
18 | // manages the editing staus and edited label
19 | const [isEditing, setIsEditing] = useState(false);
20 | const [editedLabel, setEditedLabel] = useState("");
21 |
22 | // useStore to interact with the application's global state, fetching functions and state slices.
23 | const addTable = useStore((state: TableState) => state.addTable);
24 |
25 | //function to initiate the editing mode
26 | const handleEditClick = () => {
27 | setIsEditing(true);
28 | };
29 |
30 | // function to cancel editing, reverting changes
31 | const handleEditCancel = () => {
32 | setIsEditing(false);
33 | };
34 |
35 | // function to update state with the new label
36 | const handleInputChange = (e: React.FormEvent) => {
37 | setEditedLabel(e.currentTarget.value);
38 | };
39 |
40 | // finalize the addition of the table, sending a request to the backend and updating the global state.
41 | const handleCheckClick = async () => {
42 | setIsEditing(false);
43 | setEditedLabel(editedLabel.trim().replace(/[^A-Za-z0-9_]/g, "_"));
44 | await addTable(editedLabel);
45 | setEditedLabel(data.label); // reset the edited label to "Add new table"
46 | };
47 |
48 | // Render component, edit or view mode based on isEditing state
49 | return (
50 |
60 | {isEditing ? (
61 |
68 |
75 |
76 |
81 |
82 |
83 |
88 |
89 |
90 |
91 |
92 | ) : (
93 |
100 |
101 | {data.label}
102 |
103 |
104 |
105 |
106 |
107 |
108 | )}
109 |
110 | );
111 | };
112 |
113 | export default AddTable;
114 |
--------------------------------------------------------------------------------
/src/components/ColumnNameNode.tsx:
--------------------------------------------------------------------------------
1 | // import { memo } from 'react';
2 | import { Handle, Position } from "reactflow";
3 | import { IconButton, Typography, Box } from "@mui/material";
4 | import EditIcon from "@mui/icons-material/Edit";
5 | import DeleteColumnButton from "./DeleteColumnButton";
6 | import { useState } from "react";
7 | import { Check } from "@mui/icons-material";
8 | import ClearIcon from "@mui/icons-material/Clear";
9 | import useStore from "../store";
10 |
11 | const ColumnNameNode = ({
12 | data,
13 | }: {
14 | data: { label: string; parent: string };
15 | }) => {
16 | const [isEditing, setIsEditing] = useState(false);
17 | const [editedLabel, setEditedLabel] = useState(data.label);
18 | // const fetchAndUpdateTableDetails = useStore(
19 | // (state: TableState) => state.fetchAndUpdateTableDetails
20 | // );
21 |
22 | const updateColumnName = useStore((state) => state.updateColumnName);
23 |
24 | const handleEditClick = () => {
25 | setIsEditing(true);
26 | };
27 | const handleEditCancel = () => {
28 | setEditedLabel(data.label);
29 | setIsEditing(false);
30 | };
31 |
32 | const handleInputChange = (e: React.FormEvent) => {
33 | setEditedLabel(e.currentTarget.value);
34 | };
35 |
36 | const handleCheckClick = async () => {
37 | if (editedLabel === data.label) {
38 | setIsEditing(false);
39 | return;
40 | }
41 | const sanitizedLabel = editedLabel.trim().replace(/[^A-Za-z0-9_]/g, "_");
42 | const res = await updateColumnName(data.parent, data.label, sanitizedLabel);
43 | if (res) {
44 | setEditedLabel(sanitizedLabel);
45 | setIsEditing(false);
46 | } else {
47 | console.error("Error updating column name");
48 | setEditedLabel(data.label);
49 | setIsEditing(true);
50 | }
51 |
52 | };
53 |
54 | return (
55 |
56 | {/* These handles are currently transparent, set in the css */}
57 |
58 |
59 |
60 | {/* If editing: render text box with the label name as placeholder; Otherwise render the label name */}
61 | {isEditing ? (
62 |
69 | ) : (
70 |
71 | {editedLabel}
72 |
73 | )}
74 |
75 | {/* If editing: render check button to save; Otherwise render the edit button*/}
76 | {isEditing ? (
77 |
78 |
79 |
80 |
81 |
86 |
87 |
88 |
89 | ) : (
90 |
91 |
92 |
93 |
94 |
95 |
96 | )}
97 |
98 | );
99 | };
100 |
101 | ColumnNameNode.displayName = "ColumnNameNode";
102 |
103 | export default ColumnNameNode;
104 |
--------------------------------------------------------------------------------
/src/components/DataTypeSelector.tsx:
--------------------------------------------------------------------------------
1 | import OutlinedInput from '@mui/material/OutlinedInput';
2 | import MenuItem from '@mui/material/MenuItem';
3 | import FormControl from '@mui/material/FormControl';
4 | import Select from '@mui/material/Select';
5 | import { SelectChangeEvent } from '@mui/material/Select';
6 |
7 | interface DataTypeSelectorProps {
8 | handleDataTypeChange: (event: SelectChangeEvent) => void;
9 | selectedDataType: string;
10 | // disabled: boolean;
11 | }
12 |
13 | const ITEM_HEIGHT = 48;
14 | const ITEM_PADDING_TOP = 8;
15 | const MenuProps = {
16 | PaperProps: {
17 | style: {
18 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
19 | width: 250,
20 | },
21 | },
22 | };
23 |
24 | const dataTypes = ['bit(8)',
25 | 'bool',
26 | 'box',
27 | 'bytea',
28 | 'char(10)',
29 | 'cidr',
30 | 'circle',
31 | 'date',
32 | 'decimal (8, 2)',
33 | 'float4',
34 | 'float8',
35 | 'inet',
36 | 'int',
37 | 'int2',
38 | 'int4',
39 | 'int8',
40 | 'interval (6)',
41 | 'json',
42 | 'jsonb',
43 | 'line',
44 | 'lseg',
45 | 'macaddr',
46 | 'macaddr8',
47 | 'money',
48 | 'path',
49 | 'pg_lsn',
50 | 'pg_snapshot',
51 | 'point',
52 | 'polygon',
53 | 'serial2',
54 | 'serial4',
55 | 'serial8',
56 | 'text',
57 | 'time (3) without time zone',
58 | 'timestamp (6) without time zone',
59 | 'timestamptz',
60 | 'timetz',
61 | 'tsquery',
62 | 'tsvector',
63 | 'txid_snapshot',
64 | 'uuid',
65 | 'varbit(16)',
66 | 'varchar (255)',
67 | 'xml'];
68 |
69 | export default function DataTypeSelector({
70 | handleDataTypeChange,
71 | selectedDataType,
72 | // disabled
73 | }: DataTypeSelectorProps) {
74 |
75 | return (
76 |
77 |
78 | }
83 | MenuProps={MenuProps}
84 | inputProps={{ 'aria-label': 'Without label' }}
85 | // disabled={disabled}
86 | >
87 |
88 | Select Data Type
89 |
90 | {dataTypes.map((dataType: string) => (
91 |
92 | {dataType}
93 |
94 | ))}
95 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/DeleteColumnButton.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton, Button } from "@mui/material";
2 | import DeleteIcon from "@mui/icons-material/Delete";
3 | import React from "react";
4 | import Dialog from "@mui/material/Dialog";
5 | import DialogActions from "@mui/material/DialogActions";
6 | import DialogContent from "@mui/material/DialogContent";
7 | import DialogContentText from "@mui/material/DialogContentText";
8 | import useStore from "../store";
9 |
10 | const DeleteColumnButton = ({
11 | data,
12 | }: {
13 | data: { label: string; parent: string };
14 | }) => {
15 | const [alertOpen, setAlertOpen] = React.useState(false);
16 | const deleteColumn = useStore((state) => state.deleteColumn);
17 |
18 | const deleteCol = async function () {
19 | await deleteColumn(data.parent, data.label);
20 | setAlertOpen(false);
21 | };
22 |
23 | //click handlers
24 | const handleAlertOpen = () => {
25 | setAlertOpen(true);
26 | };
27 |
28 | const handleYesDelete = async () => await deleteCol();
29 |
30 | const handleNoDelete = () => {
31 | setAlertOpen(false);
32 | };
33 |
34 | return (
35 | <>
36 |
37 |
38 |
39 |
40 | {/** dialog for alert */}
41 | setAlertOpen(false)}
44 | aria-describedby="alert-dialog-description"
45 | >
46 |
47 |
48 | Are you sure you want to delete this column?
49 |
50 |
51 |
52 | No
53 |
54 | Yes
55 |
56 |
57 |
58 | >
59 | );
60 | };
61 |
62 | DeleteColumnButton.displayName = "DeleteColumnButton";
63 |
64 | export default DeleteColumnButton;
65 | // export default memo(CustomNode);
66 |
--------------------------------------------------------------------------------
/src/components/Flow.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect} from 'react';
2 | import ReactFlow, {
3 | addEdge,
4 | Edge,
5 | Connection,
6 | useNodesState,
7 | useEdgesState,
8 | } from 'reactflow';
9 | import ColumnNameNode from './ColumnNameNode.tsx';
10 | import 'reactflow/dist/style.css';
11 | import generateEdges from './GenerateEdges.tsx';
12 | import generateNodes from './GenerateNodes.tsx';
13 | import NavBar from './NavBar.tsx';
14 | import useStore from '../store.ts';
15 | import 'reactflow/dist/base.css';
16 | import '../stylesheets/index.css';
17 | import TurboNode from './TurboNode.tsx';
18 | import TurboEdge from './TurboEdge.tsx';
19 |
20 | // custom nodes
21 | const nodeTypes = {
22 | turbo: TurboNode,
23 | colNode: ColumnNameNode,
24 | };
25 |
26 | // custom edges
27 | const edgeTypes = {
28 | turbo: TurboEdge,
29 | };
30 |
31 | const defaultEdgeOptions = {
32 | type: 'turbo',
33 | markerEnd: 'edge-circle',
34 | };
35 |
36 | const Flow = () => {
37 | // Initialize states for nodes and edges
38 | const [nodes, setNodes, onNodesChange] = useNodesState([]);
39 | const [edges, setEdges, onEdgesChange] = useEdgesState([]);
40 |
41 |
42 | const tables = useStore((state) => state.tables);
43 |
44 | const onConnect = useCallback(
45 | (params: Edge | Connection) => setEdges((els) => addEdge(params, els)),
46 | [setEdges]
47 | );
48 |
49 | useEffect(() => {
50 | if (tables.length > 0) {
51 | const newNodes = generateNodes(tables);
52 | const newEdges = generateEdges(tables);
53 |
54 | const updatedNodes = newNodes.map(newNode => {
55 |
56 | const existingNode = nodes.find(n => n.id === newNode.id && !newNode.id.includes('-column-'));
57 | return existingNode ? { ...newNode, position: existingNode.position } : newNode;
58 | });
59 |
60 | setNodes(updatedNodes);
61 | setEdges(newEdges);
62 | }
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | }, [tables, setNodes, setEdges]);
65 |
66 | const proOptions = { hideAttribution: true };
67 | return (
68 | <>
69 |
70 |
71 |
85 |
86 | {/* */}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
104 |
111 |
112 |
113 |
114 |
115 |
116 | >
117 | );
118 | };
119 |
120 | export default Flow;
121 |
--------------------------------------------------------------------------------
/src/components/FunctionIcon.tsx:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 |
3 | function Icon() {
4 | return (
5 |
6 |
7 |
15 |
23 |
24 | );
25 | }
26 |
27 | export default Icon;
28 |
--------------------------------------------------------------------------------
/src/components/GenerateEdges.tsx:
--------------------------------------------------------------------------------
1 | import { Table, Edge } from "../../global_types/types";
2 |
3 | const generateEdges = (tables: Table[]): Edge[] => {
4 | const edges: Edge[] = [];
5 |
6 | // Iterate over each table
7 | tables.forEach((table, tIndex) => {
8 | // Iterate over each foreign key in the table
9 | table.foreignKeys.forEach((fk, fkIndex) => {
10 | // Create the edge
11 | const edge: Edge = {
12 | id: `fk-${tIndex}-${fkIndex}`,
13 | target: `table-${table.name}-column-${fk.columnName}`,
14 | source: `table-${fk.foreignTableName}-column-${fk.foreignColumnName}`,
15 | animated: false,
16 | };
17 |
18 | edges.push(edge);
19 | });
20 | });
21 |
22 | return edges;
23 | };
24 |
25 | export default generateEdges;
26 |
--------------------------------------------------------------------------------
/src/components/GenerateNodes.tsx:
--------------------------------------------------------------------------------
1 | import { Node as ReactFlowNode } from "reactflow";
2 | import { Table } from "../../global_types/types";
3 |
4 | interface ExtendedNode extends ReactFlowNode {
5 | id: string;
6 | parentNode?: string;
7 | }
8 |
9 | const generateNodes = (tables: Table[]): ExtendedNode[] => {
10 | const nodes: ExtendedNode[] = [];
11 | let layoutX: number = 0;
12 | let layoutY: number = 0;
13 |
14 | tables.forEach((table: Table): void => {
15 | //layout calcs
16 | if (layoutY > 600) {
17 | layoutY = 0;
18 | layoutX += 375;
19 | }
20 |
21 | //create group node for each table
22 | const groupNode: ExtendedNode = {
23 | id: `table-${table.name}`,
24 | type: "turbo",
25 | data: {
26 | label: table.name,
27 | },
28 | className: "light",
29 | position: { x: layoutX, y: layoutY },
30 | style: {
31 | width: 250,
32 | height: 75 + table.columns.length * 40,
33 | },
34 | };
35 | nodes.push(groupNode);
36 |
37 | //initialize column node position at 45px from top
38 | let y = 60;
39 | // iterate through columns array and create node for each column name
40 | table.columns.forEach(
41 | (
42 | column: string
43 | ): void => {
44 | const columnNode: ExtendedNode = {
45 | id: `table-${table.name}-column-${column}`,
46 | data: {
47 | label: column,
48 | parent: table.name,
49 | },
50 | type: "colNode",
51 | position: { x: 15, y: y },
52 | parentNode: `table-${table.name}`,
53 | draggable: false,
54 | extent: "parent",
55 | style: {
56 | width: 220,
57 | height: 40,
58 | },
59 | };
60 | nodes.push(columnNode);
61 | y += 40;
62 | }
63 | );
64 | layoutY += 150 + table.columns.length * 40;
65 | });
66 |
67 | // makes a custom node to allow for adding tables
68 | const addTable: ExtendedNode = {
69 | id: "add-table-node", // A unique identifier for your custom node
70 | type: "turbo", // Define a custom type if needed
71 | data: {
72 | label: "Add New Table",
73 | },
74 | position: { x: layoutX, y: layoutY }, // Define the position
75 | style: {
76 | width: 250,
77 | },
78 | };
79 | nodes.push(addTable);
80 | return nodes;
81 | };
82 |
83 | export default generateNodes;
84 |
--------------------------------------------------------------------------------
/src/components/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import anime from 'animejs';
3 | // import '../stylesheets/blobs.scss';
4 | import '../stylesheets/landingPage.scss';
5 |
6 | import TextField from '@mui/material/TextField';
7 | import IconButton from '@mui/material/IconButton';
8 | import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
9 | import OceanWaves from './waves';
10 | import { useNavigate } from 'react-router-dom';
11 | import useStore from '../store';
12 |
13 | const LandingPage = () => {
14 | const logoRef = useRef(null);
15 | const navigate = useNavigate();
16 | const { fetchTables } = useStore((state) => ({
17 | fetchTables: state.fetchTables,
18 | }));
19 |
20 | const handleSubmit = async (e: React.SyntheticEvent) => {
21 | e.preventDefault();
22 | const target = e.target as typeof e.target & {
23 | input: { value: string };
24 | }
25 | const URIString = target.input.value;
26 |
27 | try {
28 | const response = await fetch('/api/setDatabaseUri', {
29 | method: 'POST',
30 | headers: {
31 | 'Content-Type': 'application/json'
32 | },
33 | body: JSON.stringify({ databaseURI: URIString })
34 | });
35 | const data = await response.json();
36 | if (response.ok && data.message === 'Database connection updated successfully') {
37 | await fetchTables();
38 | navigate('/flow');
39 | } else {
40 | console.error(data.error || 'Failed to update database URI');
41 | }
42 | } catch (error) {
43 | console.error('Error:', error);
44 | }
45 | };
46 |
47 |
48 | useEffect(() => {
49 | const logoAnimation = anime.timeline({
50 | autoplay: true,
51 | delay: 200,
52 | });
53 |
54 | logoAnimation
55 | .add({
56 | targets: logoRef.current,
57 | translateY: [-100, 0],
58 | opacity: [0, 1],
59 | elasticity: 600,
60 | duration: 800,
61 | })
62 | .add({
63 | targets: '#logo-hexagon',
64 | rotate: [-90, 0],
65 | duration: 600,
66 | elasticity: 600,
67 | offset: 50,
68 | })
69 | .add({
70 | targets: '#logo-circle',
71 | scale: [0, 1],
72 | duration: 600,
73 | elasticity: 600,
74 | offset: 250,
75 | })
76 | .add({
77 | targets: '#logo-text',
78 | translateX: ['-100%', 0],
79 | opacity: [0, 1],
80 | duration: 500,
81 | easing: 'easeOutExpo',
82 | offset: 500,
83 | });
84 | }, []);
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
102 |
103 |
104 |
110 |
117 |
118 |
119 |
120 |
121 | SQLens
122 |
123 |
124 |
125 |
126 |
127 |
166 |
167 |
168 |
169 | );
170 | };
171 |
172 | export default LandingPage;
173 |
--------------------------------------------------------------------------------
/src/components/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import AppBar from '@mui/material/AppBar';
3 | import Box from '@mui/material/Box';
4 | import Toolbar from '@mui/material/Toolbar';
5 | import IconButton from '@mui/material/IconButton';
6 | import MenuItem from '@mui/material/MenuItem';
7 | import Menu from '@mui/material/Menu';
8 | import MenuIcon from '@mui/icons-material/Menu';
9 | import { useNavigate } from 'react-router-dom';
10 |
11 |
12 | export default function NavBar() {
13 | // navigate functionality from react-router-dom
14 | const navigate = useNavigate();
15 |
16 | // useState hook to set anchor element of table
17 | const [anchorEl, setAnchorEl] = React.useState(null);
18 | const isMenuOpen = Boolean(anchorEl);
19 |
20 | // sets anchor element when react component is clicked (onClick)
21 | const handleMenuOpen = (event: React.MouseEvent) => {
22 | setAnchorEl(event.currentTarget);
23 | };
24 |
25 | // sets anchor element to null
26 | const handleMenuClose = () => {
27 | setAnchorEl(null);
28 | };
29 |
30 | // on logout, sets anchor element to null and navigates back to langing page
31 | const logout = () => {
32 | setAnchorEl(null);
33 | navigate('/');
34 | }
35 |
36 | // click handler to confirm download of migration file
37 | const confirmDownload = async () => {
38 | // pops up browser alert menu to confirm download
39 | const downloadConfirmation = window.confirm("Do you want to download the file?");
40 |
41 | // if user confirms
42 | if (downloadConfirmation) {
43 | try {
44 | const backendFilePath = './migration_log.txt';
45 |
46 | // Fetch the file content
47 | const response = await fetch(backendFilePath);
48 | const blob = await response.blob();
49 |
50 | // Create a temporary link element and trigger download
51 | const link = document.createElement('a');
52 | link.href = URL.createObjectURL(blob);
53 | link.download = 'migration_log.txt'; //need to do this as a fetch request
54 | link.click();
55 |
56 | // Cleanup
57 | URL.revokeObjectURL(link.href);
58 | } catch (error) {
59 | console.error('Error downloading file:', error);
60 | }
61 | } else {
62 | handleMenuClose();
63 | }
64 | }
65 |
66 | const renderMenu = (
67 |
83 | );
84 |
85 | return (
86 |
87 |
91 |
92 |
100 |
101 |
102 |
103 |
104 | SQLens
105 |
106 |
107 |
108 |
109 | {renderMenu}
110 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/components/TableHeader.tsx:
--------------------------------------------------------------------------------
1 | /** This component renders the name of the table and stores many of the functions for the table menu */
2 |
3 | import React from 'react';
4 | import TableMenu from './TableMenu';
5 | import { useState } from 'react';
6 | import { Check } from '@mui/icons-material';
7 | import { IconButton, Typography, Box, Button } from '@mui/material';
8 | import ClearIcon from '@mui/icons-material/Clear';
9 | import Dialog from '@mui/material/Dialog';
10 | import DialogActions from '@mui/material/DialogActions';
11 | import DialogContent from '@mui/material/DialogContent';
12 | import DialogContentText from '@mui/material/DialogContentText';
13 | import AddColumnDialog from './AddColumnDialog';
14 | import useStore from '../store';
15 |
16 | const TableHeader = ({
17 | data,
18 | }: {
19 | data: {
20 | label: string; // the passed in label is the table name
21 | };
22 | }) => {
23 | // state of the alert dialog for deleting a table
24 | const [alertOpen, setAlertOpen] = React.useState(false);
25 | // state of whether the table name is being edited
26 | const [isEditing, setIsEditing] = useState(false);
27 | // stores the edited label while name is being edited
28 | const [editedLabel, setEditedLabel] = useState(data.label);
29 | // sets anchor element for expanded table menu
30 | const [anchorEl, setAnchorEl] = React.useState(
31 | null
32 | );
33 | // function on App-wide store to delete table
34 | const deleteTable = useStore(state => state.deleteTable);
35 | const editTableName = useStore(state => state.editTable);
36 | // const fetchAndUpdateTableDetails = useStore(state => state.fetchAndUpdateTableDetails);
37 |
38 | // click handlers for delete table dialogue. Some of these will be passed into TableMenu
39 | const handleAlertOpen = () => {
40 | setAlertOpen(true);
41 | };
42 |
43 | const handleDeleteCancel = () => {
44 | setAlertOpen(false);
45 | };
46 |
47 | const handleTableDelete = async () => {
48 | setAlertOpen(false);
49 | deleteTable(data.label);
50 | };
51 |
52 | // click handlers for expanding table menu
53 | const handleMenuClick = (event: React.MouseEvent) => {
54 | setAnchorEl(event.currentTarget);
55 | };
56 |
57 | const handleMenuClose = () => {
58 | setAnchorEl(null);
59 | };
60 |
61 | //click handlers for editing table
62 | const handleEditTableName = () => {
63 | handleMenuClose();
64 | setIsEditing(true);
65 | //use document.findElementByID to select input field and make focused or selected
66 | };
67 |
68 | const handleInputChange = (e: React.FormEvent) => {
69 | setEditedLabel(e.currentTarget.value);
70 | };
71 |
72 | const handleEditCancel = () => {
73 | setEditedLabel(data.label);
74 | setIsEditing(false);
75 | };
76 |
77 | const handleEditSubmit = async () => {
78 | setEditedLabel(editedLabel.trim().replace(/[^A-Za-z0-9_]/g, '_'));
79 | const res = await editTableName(data.label, editedLabel);
80 | if (res) {
81 | setIsEditing(false);
82 | } else {
83 | setEditedLabel(data.label);
84 | }
85 | };
86 |
87 |
88 | // click handlers for Add Column Dialog
89 | const [openColDialog, setColDialogOpen] = React.useState(false);
90 |
91 | const handleAddColumnOpen = () => {
92 | handleMenuClose();
93 | setColDialogOpen(true);
94 | };
95 |
96 | const handleAddColumnClose = () => {
97 | setColDialogOpen(false);
98 | };
99 |
100 | return (
101 |
111 | {isEditing ? (
112 |
119 |
127 |
128 |
133 |
134 |
135 |
140 |
141 |
142 |
143 |
144 | ) : (
145 |
152 |
153 | {editedLabel}
154 |
155 |
156 |
165 |
166 | )}
167 |
168 | {/** dialog for alert */}
169 | setAlertOpen(false)}
172 | aria-describedby="alert-dialog-description"
173 | >
174 |
175 |
176 | Are you sure you want to delete this table?
177 |
178 |
179 |
180 | No
181 |
182 | Yes
183 |
184 |
185 |
186 |
187 |
193 |
194 | );
195 | };
196 |
197 | export default TableHeader;
198 |
--------------------------------------------------------------------------------
/src/components/TableMenu.tsx:
--------------------------------------------------------------------------------
1 | /** This is a menu button component used on the main table node that
2 | * gives users access to editing the table name, adding a column to
3 | * the table, or deleting the table */
4 |
5 | import Popover from "@mui/material/Popover";
6 | import IconButton from "@mui/material/IconButton";
7 | import MoreVertIcon from "@mui/icons-material/MoreVert";
8 | import MenuItem from "@mui/material/MenuItem";
9 |
10 | interface TableMenuProps {
11 | handleAddColumnOpen: () => void;
12 | handleAlertOpen: () => void;
13 | handleEditTableName: () => void;
14 | anchorEl: HTMLButtonElement | null;
15 | handleClick: (event: React.MouseEvent) => void;
16 | handleClose: () => void;
17 | }
18 |
19 | export default function TableMenu({
20 | handleAddColumnOpen,
21 | handleAlertOpen,
22 | handleEditTableName,
23 | anchorEl,
24 | handleClick,
25 | handleClose,
26 | }: TableMenuProps) {
27 | // boolean for whether or not the expanded menu is open
28 | const open = Boolean(anchorEl);
29 | const id = open ? "simple-popover" : undefined;
30 |
31 | return (
32 |
33 |
38 |
39 |
40 |
56 |
57 | Edit Table Name
58 |
59 |
60 | Add Column
61 |
62 |
63 | Delete Table
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/TurboEdge.tsx:
--------------------------------------------------------------------------------
1 | /** This component renders a curved react flow "edge" (connecting line between nodes) */
2 |
3 | import { EdgeProps, getBezierPath } from 'reactflow';
4 |
5 | export default function CustomEdge({
6 | id,
7 | sourceX,
8 | sourceY,
9 | targetX,
10 | targetY,
11 | sourcePosition,
12 | targetPosition,
13 | style = {},
14 | markerEnd,
15 | }: EdgeProps) {
16 | const xEqual = sourceX === targetX;
17 | const yEqual = sourceY === targetY;
18 |
19 | const [edgePath] = getBezierPath({
20 | // Conditionally calculate sourceX and sourceY to display the gradient for a straight line
21 | sourceX: xEqual ? sourceX + 0.0001 : sourceX,
22 | sourceY: yEqual ? sourceY + 0.0001 : sourceY,
23 | sourcePosition,
24 | targetX,
25 | targetY,
26 | targetPosition,
27 | });
28 |
29 | return (
30 | <>
31 |
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/TurboNode.tsx:
--------------------------------------------------------------------------------
1 | /** This component renders our main react flow nodes */
2 |
3 | import { NodeProps } from 'reactflow';
4 | import TableHeader from './TableHeader';
5 | import { FaHand } from 'react-icons/fa6';
6 | import AddTable from './AddTable';
7 |
8 | export type TurboNodeData = {
9 | label: string;
10 | };
11 |
12 | export default function TurboNode({ data }: NodeProps) {
13 | return (
14 | <>
15 | {/* Top right icon */}
16 |
21 | {/* Main Turbo Node div with wrapping colors */}
22 |
23 |
24 | {/* Conditionally renders Add Table or Table Name header */}
25 | {data.label === 'Add New Table' ? (
26 |
27 | ) : (
28 |
29 | )}
30 |
31 |
32 | >
33 | );
34 | }
35 |
36 | TurboNode.displayName = 'TurboNode';
37 |
--------------------------------------------------------------------------------
/src/components/waves.tsx:
--------------------------------------------------------------------------------
1 | /** This component is used for a visual wave effect on the landing page footer.
2 | * see the index.css for more information on colors and animation
3 | */
4 |
5 | const OceanWaves = () => {
6 | return (
7 |
11 | );
12 | };
13 |
14 | export default OceanWaves;
15 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './stylesheets/index.css'
5 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
6 |
7 | const client = new ApolloClient({
8 | uri: '/api/graphql',
9 | cache: new InMemoryCache(),
10 | });
11 |
12 | ReactDOM.createRoot(document.getElementById('root')!).render(
13 |
14 |
15 |
16 |
17 | ,
18 | )
19 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import {
3 | getTables,
4 | getTableDetails,
5 | mutateFetch,
6 | } from "./utilities/utility.ts";
7 | import { TableState } from "../global_types/types";
8 |
9 | const useStore = create((set, get) => ({
10 | // Table state that will be used to store the tables
11 | tables: [],
12 | setTables: (tables) => set({ tables }),
13 |
14 | // may not need
15 | searchValue: "",
16 | setSearchValue: (searchValue: string) => set({ searchValue }),
17 |
18 | // Fetch all tables, columns, and foreign keys from the database. To be executed once at load time
19 | fetchTables: async () => {
20 | try {
21 | const res = await getTables();
22 | set({ tables: res });
23 | } catch (error) {
24 | console.error("Error fetching tables:", error);
25 | }
26 | },
27 |
28 | // Fetch updated table information from the database including table name, columns and foreign keys
29 | fetchAndUpdateTableDetails: async (tableName: string, oldName?: string) => {
30 | try {
31 | const updatedTableDetails = await getTableDetails(tableName);
32 | const tables = get().tables;
33 | const updatedTables = tables.map((table) =>
34 | table.name === (oldName || tableName) ? updatedTableDetails : table
35 | );
36 | set({ tables: updatedTables });
37 |
38 | } catch (error) {
39 | console.error("Error fetching updated table details:", error);
40 | }
41 | },
42 |
43 | updateColumnName: async (
44 | tableName: string,
45 | columnName: string,
46 | newColumnName: string
47 | ): Promise => {
48 | const query = `
49 | mutation editColumn($newColumnName: String!, $columnName: String!, $tableName: String!) {
50 | editColumn(newColumnName: $newColumnName, columnName: $columnName, tableName: $tableName)
51 | }
52 | `;
53 | const variables = {
54 | tableName,
55 | columnName,
56 | newColumnName,
57 | };
58 | const errMsg = "Error updating column";
59 | const success = await mutateFetch(query, variables, errMsg);
60 | if (success) {
61 | await get().fetchAndUpdateTableDetails(tableName);
62 | return true;
63 | } else return false;
64 | },
65 |
66 | addColumn: async (
67 | tableName: string,
68 | columnName: string,
69 | dataType: string,
70 | refTable: string,
71 | refColumn: string
72 | ) => {
73 | const query = `
74 | mutation addColumnToTable($tableName: String!, $columnName: String!, $dataType: String!, $refTable: String, $refColumn: String){
75 | addColumnToTable( tableName: $tableName, columnName: $columnName, dataType: $dataType, refTable: $refTable, refColumn: $refColumn)
76 | }`;
77 | const variables = {
78 | tableName,
79 | columnName,
80 | dataType,
81 | refTable,
82 | refColumn,
83 | };
84 | const errMsg = "Error adding column";
85 | const success = await mutateFetch(query, variables, errMsg);
86 | if (success) {
87 | await get().fetchAndUpdateTableDetails(tableName);
88 | }
89 | },
90 |
91 | addTable: async (tableName: string) => {
92 | const query = `
93 | mutation addTable($tableName: String!){
94 | addTable( tableName: $tableName)
95 | }
96 | `;
97 | const variables = { tableName: tableName };
98 | const errMsg = "Error adding table";
99 | const success = await mutateFetch(query, variables, errMsg);
100 | if (success) {
101 | const newTable = { name: tableName, columns: [], foreignKeys: [] };
102 | set({ tables: get().tables.concat(newTable) });
103 | await get().fetchAndUpdateTableDetails(tableName);
104 | }
105 | },
106 |
107 | deleteColumn: async (
108 | tableName: string,
109 | columnName: string
110 | ): Promise => {
111 | const query = `
112 | mutation deleteColumn($tableName: String!, $columnName: String!){
113 | deleteColumn(tableName: $tableName, columnName: $columnName)
114 | }
115 | `;
116 | const variables = { tableName, columnName };
117 | const errMsg = "Error deleting column";
118 | const success = await mutateFetch(query, variables, errMsg);
119 | if (success) {
120 | await get().fetchAndUpdateTableDetails(tableName);
121 | }
122 | },
123 |
124 | deleteTable: async (tableName: string): Promise => {
125 | const query = `mutation deleteTable($tableName: String!){
126 | deleteTable( tableName: $tableName)
127 | }`;
128 | const variables = { tableName };
129 | const errMsg = "Error deleting table";
130 | const success = await mutateFetch(query, variables, errMsg);
131 | if (success) {
132 | set({ tables: get().tables.filter((table) => table.name !== tableName) });
133 | }
134 | },
135 |
136 | editTable: async (oldName: string, newName: string): Promise => {
137 | const query = `
138 | mutation editTableName($oldName: String!, $newName: String!){
139 | editTableName( oldName: $oldName, newName: $newName)
140 | }
141 | `;
142 | const variables = { oldName, newName };
143 | const errMsg = "Error editing table name";
144 | const success = await mutateFetch(query, variables, errMsg);
145 | if (success) {
146 | await get().fetchAndUpdateTableDetails(oldName, newName);
147 | return true;
148 | }
149 | return false;
150 | },
151 | }));
152 |
153 | export default useStore;
154 |
--------------------------------------------------------------------------------
/src/stylesheets/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Fira Mono';
3 | font-style: normal;
4 | font-weight: normal;
5 | src: local('Fira Mono'), url('../assets/FiraMono-Regular.ttf') format('ttf');
6 | }
7 |
8 | :root {
9 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
10 | line-height: 1.5;
11 | font-weight: 400;
12 |
13 | color-scheme: light dark;
14 | color: rgba(255, 255, 255, 0.87);
15 | background-color: #242424;
16 |
17 | font-synthesis: none;
18 | text-rendering: optimizeLegibility;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 |
22 | margin: 0px;
23 | padding: 0px;
24 | }
25 |
26 | body {
27 | margin: 0;
28 | display: flex;
29 | place-items: center;
30 | }
31 |
32 | .flow-container {
33 | display: flex;
34 | width: 100vw;
35 | height: calc(100vh - 64px);
36 | }
37 |
38 | .column-name-node {
39 | display: flex;
40 | justify-content: space-between;
41 | height: 39px;
42 | align-items: center;
43 | border: solid 1px #213547;
44 | border-radius: 3px;
45 | font-family: 'Fira Mono', monospace;
46 | padding-left: 5px;
47 | }
48 |
49 | .column-label {
50 | padding: 0px 8px;
51 | font-family: 'Fira Mono', monospace;
52 | }
53 |
54 | .table-name-input:focus {
55 | outline: none;
56 | }
57 |
58 | .table-name-input {
59 | margin-left: -3px;
60 | height: 30px;
61 | width: 155px;
62 | background-color: var(--bg-color);
63 | color: var(--text-color);
64 | border: 2px solid #2a8af6;
65 | border: 2px solid #e92a67;
66 | border-radius: 4px;
67 | font-family: 'Fira mono';
68 | }
69 |
70 | /* EVERYTHING BELOW HERE IS THE PREFERRED STYLES, redundant styles from above can be removed ABOVE*/
71 | /* turbo flow */
72 | .react-flow {
73 | --bg-color: rgb(17, 17, 17);
74 | --text-color: rgb(243, 244, 246);
75 | --node-border-radius: 10px;
76 | --node-box-shadow: 10px 0 15px rgba(42, 138, 246, 0.3), -10px 0 15px rgba(233, 42, 103, 0.3);
77 | background-color: var(--bg-color);
78 | color: var(--text-color);
79 | }
80 |
81 | .react-flow__node-turbo {
82 | border-radius: var(--node-border-radius);
83 | display: flex;
84 | height: 70px;
85 | min-width: 150px;
86 | font-family: 'Fira Mono', monospace;
87 | font-weight: 500;
88 | letter-spacing: -0.2px;
89 | box-shadow: var(--node-box-shadow);
90 | }
91 |
92 | .react-flow__node-turbo .wrapper {
93 | overflow: hidden;
94 | display: flex;
95 | padding: 2px;
96 | position: relative;
97 | border-radius: var(--node-border-radius);
98 | flex-grow: 1;
99 | }
100 |
101 | .gradient:before {
102 | content: '';
103 | position: absolute;
104 | height: 800px;
105 | width: 800px;
106 | background: conic-gradient(
107 | from -160deg at 50% 50%,
108 | #e92a67 0deg,
109 | #a853ba 120deg,
110 | #2a8af6 240deg,
111 | #e92a67 360deg
112 | );
113 | left: 50%;
114 | top: 50%;
115 | transform: translate(-50%, -50%);
116 | border-radius: 100%;
117 | }
118 |
119 | .react-flow__node-turbo.selected .wrapper.gradient:before {
120 | content: '';
121 | background: conic-gradient(
122 | from -160deg at 50% 50%,
123 | #e92a67 0deg,
124 | #a853ba 120deg,
125 | #2a8af6 240deg,
126 | rgba(42, 138, 246, 0) 360deg
127 | );
128 | animation: spinner 4s linear infinite;
129 | transform: translate(-50%, -50%) rotate(0deg);
130 | z-index: -1;
131 | }
132 |
133 | @keyframes spinner {
134 | 100% {
135 | transform: translate(-50%, -50%) rotate(-360deg);
136 | }
137 | }
138 |
139 | .react-flow__node-turbo .inner {
140 | background: var(--bg-color);
141 | padding: 15px 15px;
142 | border-radius: var(--node-border-radius);
143 | display: flex;
144 | flex-direction: row;
145 | justify-content: space-between;
146 | flex-grow: 1;
147 | position: relative;
148 | }
149 |
150 | .react-flow__node-turbo .icon {
151 | margin-right: 8px;
152 | }
153 |
154 | .react-flow__node-turbo .body {
155 | display: flex;
156 | justify-content: space-between;
157 | height: 40px;
158 | justify-items: start;
159 | }
160 |
161 | .react-flow__node-turbo .title {
162 | font-size: 16px;
163 | margin-bottom: 2px;
164 | line-height: 1;
165 | }
166 |
167 | .react-flow__node-turbo .subline {
168 | font-size: 12px;
169 | color: #777;
170 | }
171 |
172 | .react-flow__node-turbo .cloud {
173 | border-radius: 100%;
174 | width: 30px;
175 | height: 30px;
176 | right: 0;
177 | position: absolute;
178 | top: 0;
179 | transform: translate(50%, -50%);
180 | display: flex;
181 | transform-origin: center center;
182 | padding: 2px;
183 | overflow: hidden;
184 | box-shadow: var(--node-box-shadow);
185 | z-index: 1;
186 | }
187 |
188 | .react-flow__node-turbo .cloud div {
189 | background-color: var(--bg-color);
190 | flex-grow: 1;
191 | border-radius: 100%;
192 | display: flex;
193 | justify-content: center;
194 | align-items: center;
195 | position: relative;
196 | }
197 |
198 | .react-flow__handle {
199 | opacity: 0;
200 | }
201 |
202 | .react-flow__handle.source {
203 | right: -10px;
204 | }
205 |
206 | .react-flow__handle.target {
207 | left: -20px;
208 | }
209 |
210 | .react-flow__node:focus {
211 | outline: none;
212 | }
213 |
214 | .react-flow__edge .react-flow__edge-path {
215 | stroke: url(#edge-gradient);
216 | stroke-width: 2;
217 | stroke-opacity: 0.75;
218 | }
219 |
220 | .react-flow__controls button {
221 | background-color: var(--bg-color);
222 | color: var(--text-color);
223 | border: 1px solid #95679e;
224 | border-bottom: none;
225 | }
226 |
227 | .react-flow__controls button:hover {
228 | background-color: rgb(37, 37, 37);
229 | }
230 |
231 | .react-flow__controls button:first-child {
232 | border-radius: 5px 5px 0 0;
233 | }
234 |
235 | .react-flow__controls button:last-child {
236 | border-bottom: 1px solid #95679e;
237 | border-radius: 0 0 5px 5px;
238 | }
239 |
240 | .react-flow__controls button path {
241 | fill: var(--text-color);
242 | }
243 |
244 | .react-flow__attribution {
245 | background: rgba(200, 200, 200, 0.2);
246 | }
247 |
248 | .react-flow__attribution a {
249 | color: #95679e;
250 | }
251 |
252 | .table-menu-dots {
253 | margin: -8px;
254 | }
255 |
--------------------------------------------------------------------------------
/src/stylesheets/landingPage.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 |
3 | @import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap');
4 |
5 | $ff: 'Fira Mono' , monospace;
6 | $slate: #e92a67;
7 | $blue: #646cff;
8 |
9 | @function rem($px, $base: 16) {
10 | @return #{math.div($px, $base)}rem;
11 | }
12 |
13 | body {
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | min-height: 100vh;
18 | overflow: hidden;
19 | }
20 |
21 | .logo-container{
22 | margin-left: -5vw;
23 | }
24 |
25 | .site-logo {
26 | display: flex;
27 | align-items: center;
28 | transform: translateZ(0);
29 | }
30 |
31 | [id="logo"] {
32 | position: relative;
33 | flex: 0 0 rem(90);
34 | width: rem(100);
35 | z-index: 2;
36 |
37 | polygon { transform-origin: 50% }
38 | circle { transform-origin: 80% 80% }
39 | }
40 |
41 | .site-title {
42 | position: relative;
43 | overflow: hidden;
44 | margin-left: rem(-68);
45 | z-index: 1;
46 | transform: translateZ(0);
47 | font-family: $ff;
48 | }
49 |
50 | .site-title-text {
51 | padding: rem(4) rem(6) rem(4) rem(28);
52 | color: $slate;
53 | font-size: rem(45);
54 | font-weight: 300;
55 |
56 | span {
57 | font-family: $ff;
58 | margin-left: rem(0.25);
59 | color: $blue;
60 | }
61 | }
62 |
63 |
64 | .nav-title {
65 | position: relative;
66 | overflow: hidden;
67 | z-index: 1;
68 | transform: translateZ(0);
69 | font-family: $ff;
70 | }
71 |
72 | .nav-title-text {
73 | color: $slate;
74 | font-size: rem(20);
75 | font-weight: 300;
76 |
77 | span {
78 | font-family: $ff;
79 | margin-left: rem(0.25);
80 | color: $blue;
81 | }
82 | }
83 |
84 | .outer-container {
85 | display: flex;
86 | flex-direction: column;
87 | justify-content: center;
88 | align-items: center;
89 | min-height: 100vh;
90 | text-align: center
91 | }
92 |
93 | .input-container {
94 | width: 40vw;
95 | margin-top: 20px;
96 | }
97 |
98 | .ocean {
99 | // height: 5%;
100 | width:100%;
101 | position: absolute;
102 | bottom: 0;
103 | left: 0;
104 | background: #8157f4;
105 | }
106 |
107 | .wave {
108 | background: url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/85486/wave.svg') repeat-x;
109 | position: absolute;
110 | width: 6400px;
111 | top: -168px;
112 | left: 0;
113 | height: 198px;
114 | transform: translate(0, 0, 0);
115 | animation: wave 10s ease infinite;
116 | transform: translate3d(0, 0, 0);
117 |
118 | filter: invert(47%) sepia(95%) saturate(6399%) hue-rotate(314deg) brightness(102%) contrast(102%);
119 | }
120 |
121 | .wave.wave2 {
122 | // top: -108px;
123 | animation: swell 8s ease infinite;
124 | opacity: 1;
125 | filter: invert(47%) sepia(95%) saturate(6399%) hue-rotate(244deg) brightness(102%) contrast(102%);
126 | }
127 |
128 | .wave:nth-of-type(2) {
129 | top: -175px;
130 | animation: wave 7s cubic-bezier( 0.36, 0.45, 0.63, 0.53) -.125s infinite, swell 7s ease -1.25s infinite;
131 | opacity: 1;
132 | }
133 |
134 | .moving-shadow {
135 | --border-size: 3px;
136 | animation: shadow-move 3s linear infinite;
137 | }
138 |
139 | @keyframes shadow-move {
140 | 0% {
141 | box-shadow: 20px 0 40px rgba(42, 138, 246, 0.7), -20px 0 40px rgba(233, 42, 103, 0.7);
142 | }
143 | 50% {
144 | box-shadow: -20px 0 40px rgba(42, 138, 246, 0.7), 20px 0 40px rgba(233, 42, 103, 0.7);
145 | }
146 | 100% {
147 | box-shadow: 20px 0 40px rgba(42, 138, 246, 0.7), -20px 0 40px rgba(233, 42, 103, 0.7);
148 | }
149 | }
150 |
151 | @keyframes wave {
152 | 0% { margin-left: 0; }
153 | 100% { margin-left: -1600px; }
154 | }
155 |
156 | @keyframes swell {
157 | 0%, 100% {
158 | transform: translate(0, -20px);
159 | }
160 | 50% {
161 | transform: translate(0, 5px);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/utilities/queries.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | // to fetch details from all tables
4 | export const GET_TABLE_NAMES = gql`
5 | query {
6 | getTableNames {
7 | name
8 | columns
9 | foreignKeys {
10 | columnName
11 | foreignTableName
12 | foreignColumnName
13 | }
14 | }
15 | }
16 | `;
17 |
18 | // to fetch row data from a specific table
19 | export const GET_TABLE_DATA = gql`
20 | query GetTableData($tableName: String!) {
21 | getTableData(tableName: $tableName) {
22 | columnData
23 | }
24 | }
25 | `;
26 |
27 | // to fetch the details of a specific table
28 | export const GET_TABLE_DETAILS = gql`
29 | query GetTableDetails($tableName: String!) {
30 | getTableDetails(tableName: $tableName) {
31 | name
32 | columns
33 | foreignKeys {
34 | columnName
35 | foreignTableName
36 | foreignColumnName
37 | }
38 | }
39 | }
40 | `;
41 |
42 | // to add a new column to a table
43 | export const ADD_COLUMN_TO_TABLE = gql`
44 | mutation AddColumnToTable(
45 | $tableName: String!
46 | $columnName: String!
47 | $dataType: String!
48 | $fkTable: String
49 | $fkColumn: String
50 | ) {
51 | addColumnToTable(
52 | tableName: $tableName
53 | columnName: $columnName
54 | dataType: $dataType
55 | fkTable: $fkTable
56 | fkColumn: $fkColumn
57 | )
58 | }
59 | `;
60 | // to edit the name of a table
61 | export const EDIT_TABLE_NAME = gql`
62 | mutation EditTableName($oldName: String!, $newName: String!) {
63 | editTableName(oldName: $oldName, newName: $newName)
64 | }
65 | `;
66 | // to delete a table
67 | export const DELETE_TABLE = gql`
68 | mutation DeleteTable($tableName: String!) {
69 | deleteTable(tableName: $tableName)
70 | }
71 | `;
72 |
73 | export const DELETE_COLUMN = gql`
74 | mutation DeleteColumn($tableName: String!, $columnName: String!) {
75 | deleteColumn(tableName: $tableName, columnName: $columnName)
76 | }
77 | `;
78 |
--------------------------------------------------------------------------------
/src/utilities/utility.ts:
--------------------------------------------------------------------------------
1 | export const getTables = async function () {
2 | const response = await fetch("/api/graphql", {
3 | method: "POST",
4 | headers: { "Content-Type": "application/json" },
5 | body: JSON.stringify({
6 | query: `
7 | query {
8 | getTableNames {
9 | name
10 | columns
11 | foreignKeys {
12 | columnName
13 | foreignTableName
14 | foreignColumnName
15 | }
16 | }
17 | }
18 | `,
19 | }),
20 | });
21 |
22 | const final = await response.json();
23 | if (final.errors) {
24 | console.error(final.errors);
25 | throw new Error("Error fetching tables");
26 | }
27 | return final.data.getTableNames;
28 | };
29 |
30 | export const fetchColumnData = async (tableName: string) => {
31 | const response = await fetch("/api/graphql", {
32 | method: "POST",
33 | headers: { "Content-Type": "application/json" },
34 | body: JSON.stringify({
35 | query: `
36 | query GetTableData($tableName: String!) {
37 | getTableData(tableName: $tableName) {
38 | columnData
39 | }
40 | }
41 | `,
42 | variables: { tableName },
43 | }),
44 | });
45 |
46 | const result = await response.json();
47 | if (result.errors) {
48 | console.error(result.errors);
49 | throw new Error("Error fetching column data");
50 | }
51 | return result.data.getTableData;
52 | };
53 |
54 | export const getTableDetails = async function (tableName: string) {
55 | const response = await fetch("/api/graphql", {
56 | method: "POST",
57 | headers: { "Content-Type": "application/json" },
58 | body: JSON.stringify({
59 | query: `
60 | query GetTableDetails($tableName: String!) {
61 | getTableDetails(tableName: $tableName) {
62 | name
63 | columns
64 | foreignKeys {
65 | columnName
66 | foreignTableName
67 | foreignColumnName
68 | }
69 | }
70 | }
71 | `,
72 | variables: { tableName },
73 | }),
74 | });
75 |
76 | const final = await response.json();
77 | if (final.errors) {
78 | console.error(final.errors);
79 | throw new Error("Error fetching table details");
80 | }
81 | return final.data.getTableDetails;
82 | };
83 |
84 | export const mutateFetch = async (
85 | query: string,
86 | variables: { [key: string]: string; },
87 | errMsg: string
88 | ): Promise => {
89 | try {
90 | const response = await fetch("/api/graphql", {
91 | method: "POST",
92 | headers: { "Content-Type": "application/json" },
93 | body: JSON.stringify({
94 | query,
95 | variables
96 | }),
97 | });
98 |
99 | const final = await response.json();
100 | if (final.errors) {
101 | console.error(final.errors[0].message);
102 | alert(final.errors[0].message);
103 | return false;
104 | } else {
105 | return true;
106 | }
107 | } catch (error) {
108 | console.error(`${errMsg}:`, error);
109 | return false;
110 | }
111 | };
112 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noEmit": true,
4 | "esModuleInterop": true,
5 | "target": "ES2015",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2015", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | // "noEmit": true,
17 | "jsx": "react-jsx",
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noFallthroughCasesInSwitch": true
24 | },
25 | "include": [
26 | "src",
27 | "server/controller.ts",
28 | "server/db.ts",
29 | "server/index.ts",
30 | "server/typeDefs.ts",
31 | "__tests__/App.test.tsx",
32 | "__tests__/Text.test.tsx"
33 | , "global_types/types.d.ts" ],
34 | "references": [{ "path": "./tsconfig.node.json" }]
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts","express-plugin.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite'
3 | import react from '@vitejs/plugin-react'
4 | import express from './express-plugin'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [react(), express('./server')],
9 | // server: {
10 | // proxy: {
11 | // '/api': {
12 | // target: 'http://localhost:4173',
13 | // changeOrigin: true,
14 | // }
15 | // },
16 | // },
17 | // test: {
18 | // globals: true,
19 | // environment: 'jsdom',
20 | // setupFiles: ['./__tests__/setupTests.ts'],
21 | // },
22 | build: {
23 | outDir: 'dist', // Output directory for production build
24 | minify: 'terser', // Minify JavaScript
25 | cssCodeSplit: true,
26 | rollupOptions: {
27 | output: {
28 | manualChunks(id) {
29 | if (id.includes('node_modules')) {
30 | return 'vendors'; // This will put all node_modules code into a separate chunk
31 | }
32 | }
33 | }
34 | },
35 | },
36 | })
37 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 |
3 | export default defineConfig({
4 | test: {
5 | // Specifies glob patterns to match your test files
6 | globals: true,
7 | environment: 'happy-dom',
8 | include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
9 | exclude: [
10 | '**/node_modules/**',
11 | '**/dist/**',
12 | '**/cypress/**',
13 | '**/.{idea,git,cache,output,temp}/**',
14 | '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*',
15 | ],
16 | },
17 | });
--------------------------------------------------------------------------------