├── .DS_Store
├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── README.md
├── __tests__
├── App.test.jsx
├── CodeDrawer.test.jsx
├── DataPage.test.jsx
├── DataPage.toggle.test.jsx
├── NavBar.routes.test.jsx
├── NavBar.test.jsx
├── __mocks__
│ ├── fileMock.js
│ └── reactFlowMock.js
└── api.routes.test.js
├── client
├── .DS_Store
├── App.jsx
├── assets
│ ├── .DS_Store
│ ├── GraphQL-output.png
│ ├── database-connect.png
│ ├── example-schema.json
│ ├── github-icon.png
│ ├── graphiql.png
│ ├── linkedin-icon.png
│ ├── navy-github.png
│ ├── navy-linkedin.png
│ ├── navy-twitter.png
│ ├── optimized
│ │ └── images
│ │ │ ├── navy-github.webp
│ │ │ ├── navy-linkedin.webp
│ │ │ ├── navy-twitter.webp
│ │ │ ├── white-github.webp
│ │ │ └── white-linkedin.webp
│ ├── orbit-logo-black.png
│ ├── orbit-logo-white.png
│ ├── schema-visualization.png
│ ├── white-github.png
│ └── white-linkedin.png
├── components
│ ├── BackToTop.jsx
│ ├── CodeDrawer.jsx
│ ├── CodeMirror.jsx
│ ├── ColumnHandleNode.jsx
│ ├── ColumnNode.jsx
│ ├── DemoItem.jsx
│ ├── NavBar.jsx
│ ├── TableNode.jsx
│ ├── TeamMember.jsx
│ └── URIButton.jsx
├── containers
│ ├── CodeContainer.jsx
│ ├── DBInputContainer.jsx
│ ├── DemoContainer.jsx
│ ├── DiagramContainer.jsx
│ ├── FAQ.jsx
│ ├── FlowContainer.jsx
│ ├── Footer.jsx
│ ├── FormContainer.jsx
│ └── IntroContainer.jsx
├── favicon.ico
├── index.html
├── index.js
├── pages
│ ├── DataPage.jsx
│ └── HomePage.jsx
├── state
│ ├── contexts.jsx
│ └── reducers.js
└── stylesheets
│ ├── _colors.scss
│ ├── _fonts.scss
│ ├── aboutProject.scss
│ ├── aboutUs.scss
│ ├── codeContainer.scss
│ ├── codeMirror.scss
│ ├── dataPage.scss
│ ├── demo.scss
│ ├── footer.scss
│ ├── homePage.scss
│ ├── index.scss
│ ├── navbar.scss
│ ├── uri.scss
│ └── visualizer.scss
├── jest.config.js
├── jest.setup.js
├── package-lock.json
├── package.json
├── scripts
└── optimize-assets.sh
├── server
├── GQLFactory
│ ├── helpers
│ │ ├── helperFunctions.js
│ │ ├── resolverHelpers.js
│ │ └── typeHelpers.js
│ ├── resolverFactory.js
│ ├── schemaFactory.js
│ └── typeFactory.js
├── controllers
│ ├── GQLController.js
│ └── SQLController.js
├── router.js
├── schema.js
├── secretKey.js
├── server.js
└── tableQuery.sql
└── webpack.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "useBuiltIns": "entry",
7 | "corejs": 3
8 | }
9 | ],
10 | "@babel/preset-react"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | public/
3 | build/
4 | coverage/
5 | dist/
6 | .tmp/
7 | .vscode/
8 | **/bundle.js
9 | **/*.min.js
10 | client/assets/
11 | .vercel/
12 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | node: true,
6 | es2021: true,
7 | jest: true,
8 | },
9 | parserOptions: {
10 | ecmaVersion: 'latest',
11 | sourceType: 'module',
12 | ecmaFeatures: { jsx: true },
13 | },
14 | settings: {
15 | react: { version: 'detect' },
16 | },
17 | plugins: ['react', 'react-hooks', 'prettier'],
18 | extends: [
19 | 'eslint:recommended',
20 | 'plugin:react/recommended',
21 | 'plugin:react-hooks/recommended',
22 | 'prettier',
23 | ],
24 | rules: {
25 | 'prettier/prettier': 'error',
26 | semi: ['error', 'always'],
27 | quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
28 | indent: ['error', 2, { SwitchCase: 1 }],
29 | 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
30 | 'react/prop-types': 'off',
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | lint-and-test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 |
16 | - name: Setup Node
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: '18'
20 | cache: 'npm'
21 |
22 | - name: Install dependencies
23 | run: npm install --legacy-peer-deps
24 |
25 | - name: Lint
26 | run: npm run lint
27 |
28 | - name: Test
29 | run: npm test
30 | env:
31 | NODE_ENV: test
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | public
4 | .DS_Store
5 | /build
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "tabWidth": 2,
5 | "useTabs": false,
6 | "trailingComma": "es5",
7 | "printWidth": 100,
8 | "arrowParens": "always",
9 | "endOfLine": "lf"
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.defaultFormatter": "esbenp.prettier-vscode",
4 | "eslint.format.enable": true,
5 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "json"],
6 | "editor.codeActionsOnSave": {
7 | "source.fixAll.eslint": "explicit",
8 | "source.fixAll": "explicit",
9 | "source.organizeImports": "explicit"
10 | },
11 | "files.eol": "\n"
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Orbit — GraphQL prototyping and relational database visualization
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ---
22 |
23 | ## Overview
24 |
25 | **Orbit** is an open-source developer tool for **GraphQL prototyping** and **relational database visualization**.
26 |
27 | Connect to PostgreSQL, explore your schema as an interactive diagram, generate GraphQL types, and test queries in one place.
28 |
29 | - Visual database explorer with tables, columns, and foreign-key relationships
30 | - Automatic GraphQL schema generation from database metadata
31 | - Built-in playground for running queries and checking design decisions
32 | - Useful for API design, onboarding, and REST → GraphQL migrations
33 |
34 | 🔗 [Read the introduction on Medium](https://ryan-mcdaniel.medium.com/introducing-lexiql-56401bbf8d9e)
35 | 🚀 [Accelerated by OS Labs](https://github.com/oslabs-beta/)
36 |
37 | ---
38 |
39 | ## Live Demo
40 |
41 | **[Try Orbit in the browser](https://www.orbitdev.io/)** (no setup required!)
42 |
43 | ---
44 |
45 | ## Table of Contents
46 |
47 | - [Features](#features)
48 | - [Quick Start (Local)](#quick-start-local)
49 | - [How It Works](#how-it-works)
50 | - [Example Use Cases](#example-use-cases)
51 | - [Tech Stack](#tech-stack)
52 | - [Contributing](#contributing)
53 | - [Security Note](#security-note)
54 | - [Developers](#developers)
55 | - [License](#license)
56 |
57 | ---
58 |
59 | ## Features
60 |
61 | - **Visual Database Explorer** — Interactive ER-style view of tables, columns, and relationships.
62 | - **Automatic Schema Generation** — GraphQL type definitions scaffolded from your database.
63 | - **Rapid Prototyping** — Move from connection to runnable queries in minutes.
64 | - **Onboarding & Documentation** — A clear map of the data model for new engineers.
65 | - **REST Migration Aid** — Map relational data to GraphQL types without manual boilerplate.
66 |
67 | ---
68 |
69 | ## Quick Start (Local)
70 |
71 | **Requirements**
72 |
73 | - Node.js 18+ (recommended)
74 | - Optional: access to a PostgreSQL instance (Orbit also provides a sample DB)
75 |
76 | **Install and run**
77 |
78 | ```bash
79 | git clone https://github.com/oslabs-beta/Orbit.git
80 | cd Orbit
81 | npm install
82 | npm run dev
83 | ```
84 |
85 | ```
86 | API runs on http://localhost:3000
87 | Client runs on http://localhost:8080
88 | ```
89 |
90 | ### Notes
91 |
92 | Single Page App routing is configured so direct loads and refreshes work on client routes.
93 |
94 | If port 3000 is in use, stop the existing process or change the API port via process.env.PORT.
95 |
96 | ---
97 |
98 | ## How It Works
99 |
100 | 1. **Connect to a database**
101 | - Paste a PostgreSQL connection URI, or
102 | - Use the built-in **Sample Database** to explore immediately
103 |
104 |
105 |
106 | 2. **Visualize your data model**
107 | - Interactive canvas shows tables, columns, data types, and foreign keys
108 | - Drag to reposition tables for clarity
109 |
110 |
111 |
112 | 3. **Generate a GraphQL schema**
113 | - Auto-generates `type` definitions (and scaffolding) from your DB metadata
114 | - Copy the schema into your project directly from the editor
115 |
116 |
117 |
118 | 4. **Test queries**
119 | - Use the built-in playground to compose queries and mutations
120 | - Inspect available fields and relationships as you iterate
121 |
122 |
123 |
124 | ---
125 |
126 | ## Example Use Cases
127 |
128 | - **Prototype a new API** — Start from your database and generate the initial schema.
129 | - **Onboard engineers** — Provide a visual map and a safe space to run queries.
130 | - **Migrate from REST** — Translate relational tables to GraphQL types quickly.
131 | - **Explore an unfamiliar DB** — Use the sample database to learn or demo concepts.
132 |
133 | ---
134 |
135 | ## Tech Stack
136 |
137 | - **Frontend:** React, React Flow
138 | - **Backend:** Node.js, Express, PostgreSQL
139 | - **GraphQL:** GraphQL and related tooling
140 | - **Build:** Webpack
141 | - **Infra:** Vercel Docker (optional)
142 | - **CI/CD:** GitHub Actions (lint, test, build, deploy)
143 |
144 | ---
145 |
146 | ## Contributing
147 |
148 | Contributions to Orbit are welcome! Open an issue to discuss a change, or submit a pull request with a clear description and focused commits.
149 |
150 | - Keep UI changes consistent with Orbit branding and accessibility (keyboard focus, contrast).
151 | - For new features, include brief docs or inline comments.
152 | - Avoid introducing breaking changes without discussion.
153 |
154 | ---
155 |
156 | ## Security Note
157 |
158 | Keep production credentials secure. If connecting to a live database, review the code and restrict access as appropriate. Avoid using privileged accounts during prototyping.
159 |
160 | ---
161 |
162 | ## Developers
163 |
164 | - Christopher Carney — [@Carthanial](https://github.com/Carthanial)
165 | - Stacy Learn — [@hello-stacy](https://github.com/hello-stacy)
166 | - John Li — [@john-li7](https://github.com/john-li7)
167 | - Ryan McDaniel — [@ryanmcd118](https://github.com/ryanmcd118) | [ryanmcdaniel.io](https://www.ryanmcdaniel.io/)
168 |
169 | ---
170 |
171 | ## License
172 |
173 | Licensed under the [MIT License](LICENSE).
174 |
--------------------------------------------------------------------------------
/__tests__/App.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import React from 'react';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import App from '../client/App.jsx';
5 |
6 | test('renders NavBar and shows Playground link', () => {
7 | render(
8 |
9 |
10 |
11 | );
12 |
13 | expect(screen.getByText(/Playground/i)).toBeInTheDocument();
14 | });
15 |
--------------------------------------------------------------------------------
/__tests__/CodeDrawer.test.jsx:
--------------------------------------------------------------------------------
1 | import { fireEvent, render, screen } from '@testing-library/react';
2 | import React from 'react';
3 | import CodeDrawer from '../client/components/CodeDrawer';
4 | import { CodeContext } from '../client/state/contexts';
5 |
6 | // Mock the CodeMirror component
7 | jest.mock('react-codemirror2', () => ({
8 | UnControlled: ({ value }) => {value}
,
9 | }));
10 |
11 | const mockCodeState = {
12 | schema: 'type User { id: ID! name: String! }',
13 | resolver: 'const resolvers = { Query: { users: () => [] } }',
14 | displayCode: 'type User { id: ID! name: String! }',
15 | codeIsOpen: true,
16 | };
17 |
18 | const mockCodeDispatch = jest.fn();
19 |
20 | const renderCodeDrawer = () => {
21 | return render(
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | describe('CodeDrawer', () => {
29 | beforeEach(() => {
30 | jest.clearAllMocks();
31 | });
32 |
33 | it('renders with schema tab active by default', () => {
34 | renderCodeDrawer();
35 |
36 | expect(screen.getByText('Schema')).toBeInTheDocument();
37 | expect(screen.getByText('Resolver')).toBeInTheDocument();
38 | expect(screen.getByTestId('code-mirror')).toBeInTheDocument();
39 |
40 | // Schema tab should be active
41 | expect(screen.getByText('Schema').closest('button')).toHaveClass('schema-drawer__tab--active');
42 | });
43 |
44 | it('switches to resolver tab when clicked', () => {
45 | renderCodeDrawer();
46 |
47 | const resolverTab = screen.getByText('Resolver');
48 | fireEvent.click(resolverTab);
49 |
50 | expect(mockCodeDispatch).toHaveBeenCalledWith({
51 | type: 'SET_DISPLAY',
52 | payload: {
53 | displayCode: mockCodeState.resolver,
54 | },
55 | });
56 | });
57 |
58 | it('has a copy button with tooltip', () => {
59 | renderCodeDrawer();
60 |
61 | const copyButton = screen.getByRole('button', { name: /copy to clipboard/i });
62 | expect(copyButton).toBeInTheDocument();
63 | expect(copyButton).toHaveAttribute('title', 'Copy to clipboard');
64 | });
65 |
66 | it('has a resize handle', () => {
67 | renderCodeDrawer();
68 |
69 | const resizeHandle = document.querySelector('.schema-drawer__resize-handle');
70 | expect(resizeHandle).toBeInTheDocument();
71 | expect(resizeHandle).toHaveClass('schema-drawer__resize-handle');
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/__tests__/DataPage.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import React from 'react';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import DataPage from '../client/pages/DataPage.jsx';
5 |
6 | test('renders DataPage containers', async () => {
7 | render(
8 |
9 |
10 |
11 | );
12 |
13 | // React.lazy + Suspense renders async; wait for mock to appear
14 | expect(await screen.findByTestId('react-flow-mock')).toBeInTheDocument();
15 | });
16 |
--------------------------------------------------------------------------------
/__tests__/DataPage.toggle.test.jsx:
--------------------------------------------------------------------------------
1 | import { fireEvent, render, screen } from '@testing-library/react';
2 | import React from 'react';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import DataPage from '../client/pages/DataPage.jsx';
5 |
6 | // Mock the CodeMirror component
7 | jest.mock('react-codemirror2', () => ({
8 | UnControlled: ({ value }) => {value}
,
9 | }));
10 |
11 | // Mock the initial code state to include schema and resolver data
12 | jest.mock('../client/state/reducers', () => ({
13 | ...jest.requireActual('../client/state/reducers'),
14 | initialCodeState: {
15 | schema: 'type User { id: ID! name: String! }',
16 | resolver: 'const resolvers = { Query: { users: () => [] } }',
17 | displayCode: 'type User { id: ID! name: String! }',
18 | codeIsOpen: false,
19 | },
20 | }));
21 |
22 | describe('DataPage interactions', () => {
23 | it('toggles the code drawer open/closed', async () => {
24 | render(
25 |
26 |
27 |
28 | );
29 |
30 | // Wait for the toggle button to appear (it only shows when code data is available)
31 | const toggleBtn = await screen.findByTitle('Show code panel');
32 | expect(toggleBtn).toBeInTheDocument();
33 |
34 | fireEvent.click(toggleBtn);
35 |
36 | // After opening, the toggle should show hide text
37 | expect(screen.getByTitle('Hide code panel')).toBeInTheDocument();
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/__tests__/NavBar.routes.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import React from 'react';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import NavBar from '../client/components/NavBar.jsx';
5 |
6 | describe('NavBar route rendering', () => {
7 | it('renders home header at /', () => {
8 | render(
9 |
10 |
11 |
12 | );
13 | expect(screen.getByText(/Playground/i)).toBeInTheDocument();
14 | expect(screen.getByText(/FAQ/i)).toBeInTheDocument();
15 | });
16 |
17 | it('renders data header at /visualizer', () => {
18 | render(
19 |
20 |
21 |
22 | );
23 | expect(screen.getByText(/Sandbox/i)).toBeInTheDocument();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/__tests__/NavBar.test.jsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import React from 'react';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import NavBar from '../client/components/NavBar.jsx';
5 |
6 | describe('NavBar', () => {
7 | it('renders home nav with Playground and FAQ links at /', () => {
8 | render(
9 |
10 |
11 |
12 | );
13 |
14 | expect(screen.getByText(/Playground/i)).toBeInTheDocument();
15 | expect(screen.getByText(/FAQ/i)).toBeInTheDocument();
16 | });
17 |
18 | it('renders app header with Sandbox at /visualizer', () => {
19 | render(
20 |
21 |
22 |
23 | );
24 |
25 | expect(screen.getByText(/Sandbox/i)).toBeInTheDocument();
26 | // Sandbox link is present in the /visualizer navbar in this layout
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/__tests__/__mocks__/reactFlowMock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ReactFlow = ({ children }) => (
4 |
5 | {children}
6 |
7 | );
8 |
9 | export const Background = () =>
;
10 | export const Controls = () =>
;
11 | export default ReactFlow;
12 |
--------------------------------------------------------------------------------
/__tests__/api.routes.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const app = require('../server/server');
3 |
4 | describe('API routes', () => {
5 | it('GET /test -> 200 and JSON payload', async () => {
6 | const res = await request(app).get('/test');
7 | expect(res.status).toBe(200);
8 | expect(res.headers['content-type']).toMatch(/json/);
9 | expect(res.body).toEqual({ message: 'Server is working!' });
10 | });
11 |
12 | it('GET /example-schema -> 200 and contains SQLSchema + GQLSchema', async () => {
13 | const res = await request(app).get('/example-schema');
14 | expect(res.status).toBe(200);
15 | expect(res.headers['content-type']).toMatch(/json/);
16 | expect(res.body).toHaveProperty('SQLSchema');
17 | expect(res.body).toHaveProperty('GQLSchema');
18 | });
19 |
20 | it('GET /example-schema-json -> 200 and returns object schema', async () => {
21 | const res = await request(app).get('/example-schema-json');
22 | expect(res.status).toBe(200);
23 | expect(res.headers['content-type']).toMatch(/json/);
24 | expect(typeof res.body).toBe('object');
25 | });
26 |
27 | it('POST /sql-schema -> 200 and returns composed payload', async () => {
28 | const res = await request(app)
29 | .post('/sql-schema')
30 | .send({ link: 'encrypted-placeholder' })
31 | .set('Content-Type', 'application/json');
32 | expect(res.status).toBe(200);
33 | expect(res.headers['content-type']).toMatch(/json/);
34 | expect(res.body).toHaveProperty('SQLSchema');
35 | expect(res.body).toHaveProperty('GQLSchema');
36 | });
37 |
38 | it('GET /nosuchroute -> 404 Not Found', async () => {
39 | const res = await request(app).get('/nosuchroute');
40 | expect(res.status).toBe(404);
41 | expect(res.text).toBe('Not Found');
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/client/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/.DS_Store
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NavBar from './components/NavBar';
3 |
4 | const App = () => {
5 | return ;
6 | };
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/client/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/.DS_Store
--------------------------------------------------------------------------------
/client/assets/GraphQL-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/GraphQL-output.png
--------------------------------------------------------------------------------
/client/assets/database-connect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/database-connect.png
--------------------------------------------------------------------------------
/client/assets/github-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/github-icon.png
--------------------------------------------------------------------------------
/client/assets/graphiql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/graphiql.png
--------------------------------------------------------------------------------
/client/assets/linkedin-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/linkedin-icon.png
--------------------------------------------------------------------------------
/client/assets/navy-github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/navy-github.png
--------------------------------------------------------------------------------
/client/assets/navy-linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/navy-linkedin.png
--------------------------------------------------------------------------------
/client/assets/navy-twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/navy-twitter.png
--------------------------------------------------------------------------------
/client/assets/optimized/images/navy-github.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/optimized/images/navy-github.webp
--------------------------------------------------------------------------------
/client/assets/optimized/images/navy-linkedin.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/optimized/images/navy-linkedin.webp
--------------------------------------------------------------------------------
/client/assets/optimized/images/navy-twitter.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/optimized/images/navy-twitter.webp
--------------------------------------------------------------------------------
/client/assets/optimized/images/white-github.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/optimized/images/white-github.webp
--------------------------------------------------------------------------------
/client/assets/optimized/images/white-linkedin.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/optimized/images/white-linkedin.webp
--------------------------------------------------------------------------------
/client/assets/orbit-logo-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/orbit-logo-black.png
--------------------------------------------------------------------------------
/client/assets/orbit-logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/orbit-logo-white.png
--------------------------------------------------------------------------------
/client/assets/schema-visualization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/schema-visualization.png
--------------------------------------------------------------------------------
/client/assets/white-github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/white-github.png
--------------------------------------------------------------------------------
/client/assets/white-linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/assets/white-linkedin.png
--------------------------------------------------------------------------------
/client/components/BackToTop.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function BackToTop() {
4 | const scrollToTop = () => {
5 | window.scrollTo({
6 | top: 0,
7 | behavior: 'smooth',
8 | });
9 | };
10 |
11 | return (
12 |
13 |
14 |
23 |
24 |
25 | Back to Top
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/client/components/CodeDrawer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
2 | import { CodeContext } from '../state/contexts';
3 |
4 | // Import CodeMirror with proper error handling
5 | let CodeMirror;
6 | try {
7 | CodeMirror = require('react-codemirror2').UnControlled;
8 | } catch (error) {
9 | console.error('Failed to load CodeMirror:', error);
10 | // Fallback component
11 | const CodeMirrorFallback = ({ value, _options }) => (
12 |
13 |
{value || '// CodeMirror failed to load'}
14 |
15 | );
16 | CodeMirror = CodeMirrorFallback;
17 | }
18 |
19 | export default function CodeDrawer({ onWidthChange, onCollapseChange }) {
20 | const { codeState, codeDispatch } = useContext(CodeContext);
21 | const [activeTab, setActiveTab] = useState('schema');
22 | const [isResizing, setIsResizing] = useState(false);
23 | const [drawerWidth, setDrawerWidth] = useState(32); // 32% default width
24 | const [wordWrap, setWordWrap] = useState(false);
25 | const [isCollapsed, setIsCollapsed] = useState(false);
26 | const [showCopyConfirmation, setShowCopyConfirmation] = useState(false);
27 | const [isFormatted, setIsFormatted] = useState(false);
28 | const drawerRef = useRef(null);
29 | const resizeRef = useRef(null);
30 |
31 | // Load state from localStorage on mount
32 | useEffect(() => {
33 | const savedWidth = localStorage.getItem('schema-drawer-width');
34 | const savedTab = localStorage.getItem('schema-drawer-tab');
35 | const savedWordWrap = localStorage.getItem('schema-drawer-wordwrap');
36 | const savedCollapsed = localStorage.getItem('schema-drawer-collapsed');
37 |
38 | if (savedWidth) setDrawerWidth(parseFloat(savedWidth));
39 | if (savedTab) setActiveTab(savedTab);
40 | if (savedWordWrap) setWordWrap(savedWordWrap === 'true');
41 | // Only load collapsed state if the drawer is actually open
42 | if (savedCollapsed && codeState.codeIsOpen) {
43 | setIsCollapsed(savedCollapsed === 'true');
44 | } else {
45 | // Reset collapsed state when drawer opens
46 | setIsCollapsed(false);
47 | }
48 | }, [codeState.codeIsOpen]);
49 |
50 | // Save state to localStorage when it changes
51 | useEffect(() => {
52 | localStorage.setItem('schema-drawer-width', drawerWidth.toString());
53 | }, [drawerWidth]);
54 |
55 | useEffect(() => {
56 | localStorage.setItem('schema-drawer-tab', activeTab);
57 | }, [activeTab]);
58 |
59 | useEffect(() => {
60 | localStorage.setItem('schema-drawer-wordwrap', wordWrap.toString());
61 | }, [wordWrap]);
62 |
63 | useEffect(() => {
64 | localStorage.setItem('schema-drawer-collapsed', isCollapsed.toString());
65 | }, [isCollapsed]);
66 |
67 | // Notify parent of collapse state changes
68 | useEffect(() => {
69 | if (onCollapseChange) {
70 | onCollapseChange(isCollapsed);
71 | }
72 | }, [isCollapsed, onCollapseChange]);
73 |
74 | const handleTabChange = (tab) => {
75 | setActiveTab(tab);
76 | setIsFormatted(false); // Reset formatted state when switching tabs
77 |
78 | if (tab === 'schema') {
79 | codeDispatch({
80 | type: 'SET_DISPLAY',
81 | payload: {
82 | displayCode:
83 | codeState.schema ||
84 | '// No schema data available\n// Load sample data or connect to a database to see the generated GraphQL schema',
85 | },
86 | });
87 | } else if (tab === 'resolver') {
88 | codeDispatch({
89 | type: 'SET_DISPLAY',
90 | payload: {
91 | displayCode:
92 | codeState.resolver ||
93 | '// No resolver data available\n// Load sample data or connect to a database to see the generated GraphQL resolvers',
94 | },
95 | });
96 | }
97 | };
98 |
99 | const handleCopy = () => {
100 | try {
101 | navigator.clipboard.writeText(codeState.displayCode || '');
102 | setShowCopyConfirmation(true);
103 |
104 | // Hide the confirmation message after 2 seconds
105 | setTimeout(() => {
106 | setShowCopyConfirmation(false);
107 | }, 2000);
108 | } catch (error) {
109 | console.error('Failed to copy to clipboard:', error);
110 | }
111 | };
112 |
113 | const handleDownload = () => {
114 | const content = codeState.displayCode || '';
115 | const extension = activeTab === 'schema' ? '.graphql' : '.js';
116 | const filename = `${activeTab}${extension}`;
117 |
118 | const blob = new Blob([content], { type: 'text/plain' });
119 | const url = URL.createObjectURL(blob);
120 | const a = document.createElement('a');
121 | a.href = url;
122 | a.download = filename;
123 | document.body.appendChild(a);
124 | a.click();
125 | document.body.removeChild(a);
126 | URL.revokeObjectURL(url);
127 | };
128 |
129 | const handleFormat = () => {
130 | let content = '';
131 |
132 | // Use the original data based on active tab
133 | if (activeTab === 'schema') {
134 | content = codeState.schema || '';
135 | } else if (activeTab === 'resolver') {
136 | content = codeState.resolver || '';
137 | }
138 |
139 | if (isFormatted) {
140 | // Toggle back to unformatted (original)
141 | codeDispatch({
142 | type: 'SET_DISPLAY',
143 | payload: {
144 | displayCode: content,
145 | },
146 | });
147 | setIsFormatted(false);
148 | } else {
149 | // Format the code
150 | const formatted = content
151 | .split('\n')
152 | .map((line) => line.trim())
153 | .filter((line) => line.length > 0)
154 | .join('\n');
155 |
156 | codeDispatch({
157 | type: 'SET_DISPLAY',
158 | payload: {
159 | displayCode: formatted,
160 | },
161 | });
162 | setIsFormatted(true);
163 | }
164 | };
165 |
166 | const handleToggleCollapse = () => {
167 | try {
168 | const newCollapsedState = !isCollapsed;
169 | setIsCollapsed(newCollapsedState);
170 |
171 | // Ensure the parent is notified immediately
172 | if (onCollapseChange) {
173 | onCollapseChange(newCollapsedState);
174 | }
175 | } catch (error) {
176 | console.error('Error toggling drawer collapse:', error);
177 | // Fallback: ensure drawer is in a safe state
178 | setIsCollapsed(false);
179 | if (onCollapseChange) {
180 | onCollapseChange(false);
181 | }
182 | }
183 | };
184 |
185 | const startResize = (e) => {
186 | e.preventDefault();
187 | setIsResizing(true);
188 | };
189 |
190 | const stopResize = () => {
191 | setIsResizing(false);
192 | };
193 |
194 | const handleResize = useCallback(
195 | (e) => {
196 | if (!isResizing) return;
197 |
198 | const containerWidth = window.innerWidth;
199 | const newWidth = ((containerWidth - e.clientX) / containerWidth) * 100;
200 |
201 | // Limit width between 20% and 60%, minimum 320px
202 | const minWidthPercent = (320 / containerWidth) * 100;
203 | if (newWidth >= Math.max(20, minWidthPercent) && newWidth <= 60) {
204 | setDrawerWidth(newWidth);
205 | if (onWidthChange) {
206 | onWidthChange(newWidth);
207 | }
208 | }
209 | },
210 | [isResizing, onWidthChange]
211 | );
212 |
213 | useEffect(() => {
214 | if (isResizing) {
215 | document.addEventListener('mousemove', handleResize);
216 | document.addEventListener('mouseup', stopResize);
217 |
218 | return () => {
219 | document.removeEventListener('mousemove', handleResize);
220 | document.removeEventListener('mouseup', stopResize);
221 | };
222 | }
223 | }, [isResizing, handleResize]);
224 |
225 | // Set initial display code when component mounts (only once)
226 | useEffect(() => {
227 | if (!codeState.displayCode && !codeState.schema && !codeState.resolver) {
228 | // Only set placeholder if there's no code data at all
229 | codeDispatch({
230 | type: 'SET_DISPLAY',
231 | payload: {
232 | displayCode:
233 | '// No schema data available\n// Load sample data or connect to a database to see the generated GraphQL schema',
234 | },
235 | });
236 | }
237 | }, [codeDispatch, codeState.displayCode, codeState.schema, codeState.resolver]); // Include all dependencies
238 |
239 | const drawerStyle = {
240 | width: isCollapsed ? '44px' : `${drawerWidth}%`,
241 | };
242 |
243 | return (
244 |
249 | {/* Resize handle */}
250 |
251 |
252 | {/* Header */}
253 |
254 |
255 | handleTabChange('schema')}
258 | aria-label="View GraphQL schema"
259 | >
260 | Schema
261 |
262 | handleTabChange('resolver')}
265 | aria-label="View GraphQL resolvers"
266 | >
267 | Resolver
268 |
269 |
270 |
271 |
272 |
278 |
286 |
287 |
288 |
289 |
290 |
291 | {showCopyConfirmation &&
Copied
}
292 |
293 |
299 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
setWordWrap(!wordWrap)}
316 | aria-label="Toggle word wrap"
317 | title="Toggle word wrap"
318 | >
319 |
327 |
328 |
329 |
330 |
331 |
337 |
345 |
346 |
347 |
348 |
349 |
355 |
363 |
364 |
365 |
366 |
367 |
368 |
369 | {/* Code editor area */}
370 |
371 |
386 |
387 |
388 | );
389 | }
390 |
--------------------------------------------------------------------------------
/client/components/CodeMirror.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { UnControlled as CodeMirror } from 'react-codemirror2';
3 | import { CodeContext } from '../state/contexts';
4 |
5 | export default function CodeMirrorComponent() {
6 | const { codeState } = useContext(CodeContext);
7 |
8 | return (
9 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/client/components/ColumnHandleNode.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Handle } from 'react-flow-renderer';
3 |
4 | const ColumnHandleNode = ({ data }) => {
5 | const { tableName, columnName, isSource, isTarget } = data;
6 |
7 | return (
8 |
9 | {isSource && (
10 |
20 | )}
21 | {isTarget && (
22 |
32 | )}
33 |
34 | );
35 | };
36 |
37 | ColumnHandleNode.displayName = 'ColumnHandleNode';
38 |
39 | export default memo(ColumnHandleNode);
40 |
--------------------------------------------------------------------------------
/client/components/ColumnNode.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Handle } from 'react-flow-renderer';
3 |
4 | const ColumnNode = ({ columnName, dataType, id, tableName, hasHandles }) => {
5 | // object of tables and their respective columns that have source and/or target handles
6 | // (i.e., does not include columns that don't have handles)
7 | const colHandles = hasHandles;
8 |
9 | const noHandles = (
10 |
18 | );
19 |
20 | const targetHandle = (
21 |
31 | );
32 |
33 | const sourceHandle = (
34 |
44 | );
45 |
46 | // render source, target, or no handles accordingly
47 | if (!colHandles[tableName]) {
48 | return noHandles;
49 | } else if (
50 | colHandles[tableName].sourceHandles &&
51 | colHandles[tableName].sourceHandles.includes(columnName)
52 | ) {
53 | return (
54 |
55 |
58 |
59 |
{dataType}
60 |
{sourceHandle}
61 |
62 |
63 | );
64 | } else if (
65 | colHandles[tableName].targetHandles &&
66 | colHandles[tableName].targetHandles.includes(columnName)
67 | ) {
68 | return (
69 |
70 |
71 | {targetHandle}
72 |
{columnName}
73 |
74 |
77 |
78 | );
79 | } else {
80 | return noHandles;
81 | }
82 | };
83 |
84 | ColumnNode.displayName = 'ColumnNode';
85 |
86 | export default memo(ColumnNode);
87 |
--------------------------------------------------------------------------------
/client/components/DemoItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CodeMirror from '../assets/GraphQL-output.png';
3 | import UserInput from '../assets/database-connect.png';
4 | import graphiql from '../assets/graphiql.png';
5 | import MovingTables from '../assets/schema-visualization.png';
6 |
7 | export default function DemoItem({ index, title, description, _gif }) {
8 | if (index === 0) {
9 | return (
10 |
11 |
12 |
{title}
13 |
{description}
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | if (index === 1) {
22 | return (
23 |
24 |
25 |
{title}
26 |
{description}
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | if (index === 2) {
35 | return (
36 |
37 |
38 |
{title}
39 |
{description}
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 | if (index === 3) {
48 | return (
49 |
50 |
51 |
{title}
52 |
{description}
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/client/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useEffect, useState } from 'react';
2 | import { Link, Route, Switch, useHistory, useLocation } from 'react-router-dom';
3 | import GithubLogo from '../assets/navy-github.png';
4 | import LinkedinLogo from '../assets/navy-linkedin.png';
5 | import TwitterLogo from '../assets/navy-twitter.png';
6 | import WhiteLogo from '../assets/orbit-logo-white.png';
7 |
8 | // Lazy load pages for better performance
9 | const DataPage = React.lazy(() => import('../pages/DataPage.jsx'));
10 | const HomePage = React.lazy(() => import('../pages/HomePage.jsx'));
11 |
12 | export default function NavBar() {
13 | const location = useLocation();
14 | const history = useHistory();
15 | const isHomePage = location.pathname === '/';
16 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
17 |
18 | // Close mobile menu when clicking outside
19 | useEffect(() => {
20 | const handleClickOutside = (event) => {
21 | const hamburgerMenu = document.querySelector('.hamburger-menu');
22 | const rightLinks = document.querySelector('.rightLinks');
23 |
24 | if (
25 | isMobileMenuOpen &&
26 | hamburgerMenu &&
27 | !hamburgerMenu.contains(event.target) &&
28 | rightLinks &&
29 | !rightLinks.contains(event.target)
30 | ) {
31 | setIsMobileMenuOpen(false);
32 | }
33 | };
34 |
35 | if (isMobileMenuOpen) {
36 | document.addEventListener('mousedown', handleClickOutside);
37 | // Prevent body scroll when menu is open
38 | document.body.style.overflow = 'hidden';
39 | } else {
40 | document.body.style.overflow = 'unset';
41 | }
42 |
43 | return () => {
44 | document.removeEventListener('mousedown', handleClickOutside);
45 | document.body.style.overflow = 'unset';
46 | };
47 | }, [isMobileMenuOpen]);
48 |
49 | const handleFAQClick = () => {
50 | if (location.pathname !== '/') {
51 | history.push('/');
52 | // Wait for navigation to complete, then scroll to FAQ
53 | setTimeout(() => {
54 | const faqElement = document.getElementById('faq');
55 | if (faqElement) {
56 | faqElement.scrollIntoView({ behavior: 'smooth' });
57 | }
58 | }, 100);
59 | } else {
60 | // If already on homepage, just scroll to FAQ
61 | const faqElement = document.getElementById('faq');
62 | if (faqElement) {
63 | faqElement.scrollIntoView({ behavior: 'smooth' });
64 | }
65 | }
66 | // Close mobile menu after navigation
67 | setIsMobileMenuOpen(false);
68 | };
69 |
70 | const toggleMobileMenu = () => {
71 | setIsMobileMenuOpen(!isMobileMenuOpen);
72 | };
73 |
74 | const closeMobileMenu = () => {
75 | setIsMobileMenuOpen(false);
76 | };
77 |
78 | return (
79 |
80 | {/* Mobile menu backdrop */}
81 | {isMobileMenuOpen &&
}
82 |
83 |
84 | {isHomePage ? (
85 | // Home page navigation
86 | <>
87 |
136 |
137 | {/* Hamburger menu button for mobile */}
138 |
143 |
144 |
153 | >
154 | ) : (
155 | // App page navigation
156 | <>
157 |
158 |
167 |
168 |
169 | {/* Hamburger menu button for mobile */}
170 |
175 |
176 | {location.pathname === '/visualizer' && (
177 |
178 |
{
180 | window.open('/playground', '_blank');
181 | closeMobileMenu();
182 | }}
183 | className="headerLinks"
184 | style={{
185 | background: 'none',
186 | border: 'none',
187 | cursor: 'pointer',
188 | }}
189 | >
190 | Sandbox
191 |
192 |
193 |
202 | FAQ
203 |
204 |
205 | )}
206 |
207 | {location.pathname === '/playground' && (
208 |
209 |
210 |
Visualize
211 |
212 |
213 | )}
214 | >
215 | )}
216 |
217 |
218 |
Loading... }>
219 |
220 |
221 |
222 |
223 |
224 |
225 | insert Graphiql playground here
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | );
235 | }
236 |
--------------------------------------------------------------------------------
/client/components/TableNode.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import ColumnNode from './ColumnNode.jsx';
3 |
4 | const TableNode = ({ data }) => {
5 | const { tableName, columns, dataTypes, hasHandles } = data;
6 |
7 | let tableColumns;
8 |
9 | if (columns && dataTypes) {
10 | tableColumns = columns.map((column, index) => (
11 |
19 | ));
20 | }
21 |
22 | return (
23 | <>
24 |
25 | {tableName}
26 |
27 |
28 | {tableColumns}
29 | >
30 | );
31 | };
32 |
33 | TableNode.displayName = 'TableNode';
34 |
35 | export default memo(TableNode);
36 |
--------------------------------------------------------------------------------
/client/components/TeamMember.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import GitHubIcon from '../assets/white-github.png';
3 | import LinkedinIcon from '../assets/white-linkedin.png';
4 |
5 | export default function TeamMember({ name, headshot, github, linkedin }) {
6 | return (
7 |
8 |
17 |
{name}
18 |
19 |
window.open(github)}
28 | />
29 |
window.open(linkedin)}
38 | />
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/client/components/URIButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { FormContext } from '../state/contexts';
3 |
4 | const URIButton = () => {
5 | const { formState, formDispatch } = useContext(FormContext);
6 |
7 | const toggle = () => {
8 | formDispatch({
9 | type: 'TOGGLE_FORM',
10 | payload: {
11 | formIsOpen: !formState.formIsOpen,
12 | },
13 | });
14 | };
15 |
16 | return (
17 |
22 | {formState.formIsOpen ? '-' : '+'}
23 |
24 | );
25 | };
26 |
27 | export default URIButton;
28 |
--------------------------------------------------------------------------------
/client/containers/CodeContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useContext, useEffect, useState } from 'react';
2 | import CodeDrawer from '../components/CodeDrawer';
3 | import { CodeContext } from '../state/contexts';
4 |
5 | export default function CodeContainer() {
6 | const { codeState, codeDispatch } = useContext(CodeContext);
7 | const [drawerWidth, setDrawerWidth] = useState(32);
8 | const [isDrawerCollapsed, setIsDrawerCollapsed] = useState(false);
9 |
10 | const toggleCodeDrawer = () => {
11 | codeDispatch({
12 | type: 'TOGGLE_CODE',
13 | payload: {
14 | codeIsOpen: !codeState.codeIsOpen,
15 | },
16 | });
17 | };
18 |
19 | // Clear collapsed state when drawer is closed
20 | useEffect(() => {
21 | if (!codeState.codeIsOpen) {
22 | localStorage.removeItem('schema-drawer-collapsed');
23 | setIsDrawerCollapsed(false);
24 | }
25 | }, [codeState.codeIsOpen]);
26 |
27 | const handleDrawerWidthChange = (width) => {
28 | setDrawerWidth(width);
29 | };
30 |
31 | const handleDrawerCollapseChange = (collapsed) => {
32 | setIsDrawerCollapsed(collapsed);
33 | };
34 |
35 | const toggleButtonStyle = {
36 | right: codeState.codeIsOpen ? (isDrawerCollapsed ? '44px' : `${drawerWidth}%`) : '0',
37 | };
38 |
39 | return (
40 |
41 |
48 | {codeState.codeIsOpen ? '▶' : '◀'}
49 |
50 |
51 | {codeState.codeIsOpen && (
52 | Loading editor…
}>
53 |
57 |
58 | )}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/client/containers/DBInputContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FormContainer from './FormContainer.jsx';
3 |
4 | export default function DBInputContainer() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/client/containers/DemoContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import graphqlOutput from '../assets/GraphQL-output.png';
4 | import databaseConnect from '../assets/database-connect.png';
5 | import graphiql from '../assets/graphiql.png';
6 | import schemaVisualization from '../assets/schema-visualization.png';
7 |
8 | export default function DemoContainer() {
9 | const content = [
10 | [
11 | 'Connect your PostgreSQL database',
12 | 'Paste in a PostgreSQL connection string and Orbit generates a relational schema map of your tables and columns. No database? Launch the built-in sample to see Orbit in action.',
13 | databaseConnect,
14 | ],
15 | [
16 | 'Explore your schema visually',
17 | 'Orbit displays your schema as an interactive graph where you can clearly trace relationships, expand tables, and understand column structures at a glance.',
18 | schemaVisualization,
19 | ],
20 | [
21 | 'Generate GraphQL boilerplate',
22 | 'From your database structure, Orbit builds a complete GraphQL schema (types, resolvers, and connections) all ready to integrate directly into your API project.',
23 | graphqlOutput,
24 | ],
25 | [
26 | 'Test instantly in GraphiQL',
27 | 'Open an embedded GraphiQL playground to run queries and mutations against your live or sample database, validating schema output and testing connections right away.',
28 | graphiql,
29 | ],
30 | ];
31 |
32 | const introContent = [];
33 | for (let i = 0; i < content.length; i++) {
34 | const feature = content[i];
35 | introContent.push(
36 |
37 |
{i + 1}
38 |
{feature[0]}
39 |
{feature[1]}
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 |
How Orbit works in 4 quick steps.
52 |
53 |
{introContent}
54 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/client/containers/DiagramContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | let FlowContainer;
3 | if (process.env.NODE_ENV === 'test') {
4 | // eslint-disable-next-line global-require
5 | FlowContainer = require('./FlowContainer.jsx').default;
6 | } else {
7 | FlowContainer = React.lazy(() => import('./FlowContainer.jsx'));
8 | }
9 |
10 | export default function DiagramContainer() {
11 | return (
12 |
13 | Loading diagram…
}>
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/client/containers/FAQ.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | export default function AboutUsContainer() {
4 | const [openItem, setOpenItem] = useState(null);
5 |
6 | const faqItems = [
7 | {
8 | id: 'faq-1',
9 | question: 'What is Orbit?',
10 | answer:
11 | 'Orbit is an open-source tool for visualizing GraphQL schemas. It helps you explore relationships, fields, and queries in a clean, interactive interface without needing to manually trace dependencies or dig through raw schema files.',
12 | },
13 | {
14 | id: 'faq-2',
15 | question: 'Why is it called Orbit?',
16 | answer:
17 | 'We chose the name Orbit because it reflects the way databases organize information: with tables, columns, and relationships all revolving around each other. The graph you see in Orbit feels like a system of connected nodes in motion, which inspired the name.',
18 | },
19 | {
20 | id: 'faq-3',
21 | question: 'Who is Orbit built for?',
22 | answer:
23 | "Orbit is designed for engineers working with GraphQL, whether you're building APIs, teaching schema design, or onboarding teammates. It's also helpful for product managers or designers who want a visual map of how different data entities relate.",
24 | },
25 | {
26 | id: 'faq-4',
27 | question: 'Is Orbit free to use?',
28 | answer:
29 | 'Yes! Orbit is completely free and open source. You can clone the repository, contribute improvements, or simply run it locally for your own projects. There are no licensing fees or restrictions on personal or commercial use.',
30 | },
31 | {
32 | id: 'faq-5',
33 | question: 'How do I get started with Orbit?',
34 | answer:
35 | 'You can try Orbit instantly by pasting a GraphQL schema into the app. For more advanced use, clone the repo from GitHub and follow the setup instructions. We provide sample data and a quickstart guide so you can be up and running in minutes.',
36 | },
37 | {
38 | id: 'faq-6',
39 | question: 'Can I contribute to Orbit?',
40 | answer:
41 | 'Absolutely. We welcome pull requests, bug reports, and feature suggestions. Check out the GitHub repo for contribution guidelines. Even small improvements like clarifying documentation or reporting an issue make a difference.',
42 | },
43 | {
44 | id: 'faq-7',
45 | question: "What's on the roadmap?",
46 | answer:
47 | "Upcoming work includes improved schema import options, customizable layouts, and better integration with developer workflows. We're also exploring ways to make large schemas easier to navigate, such as search and filtering features.",
48 | },
49 | ];
50 |
51 | const handleToggle = (itemId) => {
52 | setOpenItem(openItem === itemId ? null : itemId);
53 | };
54 |
55 | const handleKeyDown = (event, itemId) => {
56 | if (event.key === 'Enter' || event.key === ' ') {
57 | event.preventDefault();
58 | handleToggle(itemId);
59 | }
60 | };
61 |
62 | const handleArrowKey = (event, currentIndex) => {
63 | if (event.key === 'ArrowDown') {
64 | event.preventDefault();
65 | const nextIndex = (currentIndex + 1) % faqItems.length;
66 | const nextItem = document.getElementById(`faq-${nextIndex + 1}`);
67 | nextItem?.focus();
68 | } else if (event.key === 'ArrowUp') {
69 | event.preventDefault();
70 | const prevIndex = currentIndex === 0 ? faqItems.length - 1 : currentIndex - 1;
71 | const prevItem = document.getElementById(`faq-${prevIndex + 1}`);
72 | prevItem?.focus();
73 | }
74 | };
75 |
76 | return (
77 |
78 |
79 |
Frequently Asked Questions
80 |
86 | Have more questions? Open an issue on GitHub.
87 |
88 |
89 | {faqItems.map((item, index) => (
90 |
91 |
handleToggle(item.id)}
95 | onKeyDown={(e) => {
96 | handleKeyDown(e, item.id);
97 | handleArrowKey(e, index);
98 | }}
99 | aria-expanded={openItem === item.id}
100 | aria-controls={`${item.id}-answer`}
101 | >
102 | {item.question}
103 |
111 |
118 |
119 |
120 |
125 |
{item.answer}
126 |
127 |
128 | ))}
129 |
130 |
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/client/containers/FlowContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from 'react';
2 |
3 | import ReactFlow, { Background, Controls } from 'react-flow-renderer';
4 |
5 | import ColumnHandleNode from '../components/ColumnHandleNode.jsx';
6 | import TableNode from '../components/TableNode.jsx';
7 | import { DiagramContext } from '../state/contexts';
8 |
9 | const connectionLineStyle = { stroke: '#ff9149', strokeWidth: 2 };
10 | const nodeTypes = {
11 | selectorNode: TableNode,
12 | columnHandleNode: ColumnHandleNode,
13 | };
14 |
15 | const FlowContainer = () => {
16 | const [elements, setElements] = useState([]);
17 |
18 | const { diagramState } = useContext(DiagramContext);
19 |
20 | useEffect(() => {
21 | if (diagramState.tableNodes && diagramState.tableNodes.length > 0) {
22 | setElements(diagramState.tableNodes);
23 | }
24 | }, [diagramState.tableNodes]);
25 |
26 | return (
27 |
28 | {elements.length === 0 ? (
29 |
39 | {diagramState.tableNodes && diagramState.tableNodes.length > 0
40 | ? 'Processing database schema...'
41 | : 'No database schema loaded. Please use the sample database or input your own database URI.'}
42 |
43 | ) : (
44 |
59 |
60 |
61 |
62 | )}
63 |
64 | );
65 | };
66 |
67 | export default FlowContainer;
68 |
--------------------------------------------------------------------------------
/client/containers/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Logo from '../assets/orbit-logo-white.png';
4 |
5 | export default function Footer() {
6 | const currentYear = new Date().getFullYear();
7 |
8 | return (
9 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/client/containers/IntroContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Logo from '../assets/orbit-logo-black.png';
4 |
5 | export default function IntroContainer() {
6 | return (
7 |
8 |
9 |
10 |
11 |
Visualize your database instantly
12 |
13 | Auto-generate schema diagrams and GraphQL boilerplate directly from your PostgreSQL
14 | database.
15 |
16 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Orbit/29a78b31417733c8ee7f373a3058a12e3bda5bb9/client/favicon.ico
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 | Orbit
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App.jsx';
4 | import { BrowserRouter } from 'react-router-dom';
5 |
6 | import './stylesheets/index.scss';
7 |
8 | render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 |
--------------------------------------------------------------------------------
/client/pages/DataPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useReducer } from 'react';
2 | import { CodeContext, DiagramContext, FormContext } from '../state/contexts';
3 | import {
4 | codeReducer,
5 | diagramReducer,
6 | formReducer,
7 | initialCodeState,
8 | initialDiagramState,
9 | initialFormState,
10 | } from '../state/reducers';
11 |
12 | import CodeContainer from '../containers/CodeContainer';
13 | import DBInputContainer from '../containers/DBInputContainer';
14 | import DiagramContainer from '../containers/DiagramContainer';
15 |
16 | export default function DataPage() {
17 | const [codeState, codeDispatch] = useReducer(codeReducer, initialCodeState);
18 | const [diagramState, diagramDispatch] = useReducer(diagramReducer, initialDiagramState);
19 | const [formState, formDispatch] = useReducer(formReducer, initialFormState);
20 |
21 | return (
22 |
23 |
24 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
52 |
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/client/pages/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import BackToTop from '../components/BackToTop.jsx';
4 | import DemoContainer from '../containers/DemoContainer.jsx';
5 | import FAQ from '../containers/FAQ.jsx';
6 | import Footer from '../containers/Footer.jsx';
7 | import IntroContainer from '../containers/IntroContainer.jsx';
8 |
9 | export default function HomePage() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/client/state/contexts.jsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export const DiagramContext = createContext();
4 | export const CodeContext = createContext();
5 | export const FormContext = createContext();
6 |
--------------------------------------------------------------------------------
/client/state/reducers.js:
--------------------------------------------------------------------------------
1 | export const initialDiagramState = {
2 | dbContents: [],
3 | tableNodes: [{}],
4 | };
5 |
6 | export const diagramReducer = (state, action) => {
7 | switch (action.type) {
8 | case 'SET_TABLES':
9 | return {
10 | ...state,
11 | sqlSchema: action.payload.sqlSchema,
12 | tableNodes: action.payload.tableNodes,
13 | dbContents: action.payload.dbContents,
14 | relationalData: action.payload.relationalData,
15 | primaryKeys: action.payload.primaryKeys,
16 | hasHandles: action.payload.hasHandles,
17 | };
18 | case 'SET_EDGES': {
19 | const newTableNodes = [...state.tableNodes, ...action.payload.edges];
20 | return {
21 | ...state,
22 | tableNodes: newTableNodes,
23 | };
24 | }
25 | default:
26 | return state;
27 | }
28 | };
29 |
30 | export const initialCodeState = {
31 | schema: '',
32 | resolver: '',
33 | displayCode: '',
34 | codeIsOpen: false,
35 | };
36 |
37 | export const codeReducer = (state, action) => {
38 | switch (action.type) {
39 | case 'SET_CODE':
40 | return {
41 | ...state,
42 | schema: action.payload.schema,
43 | resolver: action.payload.resolver,
44 | displayCode: action.payload.displayCode,
45 | codeIsOpen: action.payload.codeIsOpen,
46 | };
47 | case 'SET_DISPLAY':
48 | return {
49 | ...state,
50 | displayCode: action.payload.displayCode,
51 | };
52 |
53 | case 'TOGGLE_CODE':
54 | return {
55 | ...state,
56 | codeIsOpen: action.payload.codeIsOpen,
57 | };
58 | default:
59 | return state;
60 | }
61 | };
62 |
63 | export const initialFormState = {
64 | formIsOpen: true,
65 | firstFetch: true,
66 | URIvalidation: '',
67 | isLoading: false,
68 | sampleDBtext: 'Get started by using the sample database:',
69 | inputDBtext: 'Or put a link to your database:',
70 | };
71 |
72 | export const formReducer = (state, action) => {
73 | switch (action.type) {
74 | case 'TOGGLE_FORM':
75 | return {
76 | ...state,
77 | formIsOpen: action.payload.formIsOpen,
78 | firstFetch: action.payload.firstFetch,
79 | URIvalidation: action.payload.URIvalidation,
80 | sampleDBtext: action.payload.sampleDBtext,
81 | inputDBtext: action.payload.inputDBtext,
82 | };
83 | case 'SET_LOADING':
84 | return {
85 | ...state,
86 | isLoading: action.payload.isLoading,
87 | };
88 | case 'SET_VALIDATION':
89 | return {
90 | ...state,
91 | URIvalidation: action.payload.URIvalidation,
92 | };
93 | default:
94 | return state;
95 | }
96 | };
97 |
--------------------------------------------------------------------------------
/client/stylesheets/_colors.scss:
--------------------------------------------------------------------------------
1 | $orange: #ff9149;
2 | $bright-blue: #5a95f5;
3 | $dark-blue: #0f264b;
4 |
5 | :root {
6 | --orbit-orange: #{$orange};
7 | --orbit-navy: #{$dark-blue};
8 | --orbit-border: rgba(255, 145, 73, 0.2);
9 | --orbit-bg-primary: #18263a;
10 | --orbit-bg-secondary: #0f1d31;
11 | --orbit-text-primary: #eaf0f7;
12 | --orbit-text-secondary: #8b9bb4;
13 | --orbit-selection: rgba(255, 145, 73, 0.18);
14 | }
15 |
--------------------------------------------------------------------------------
/client/stylesheets/_fonts.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Raleway';
3 | font-style: normal;
4 | font-weight: 700;
5 | src: url(https://fonts.gstatic.com/s/raleway/v19/1Ptxg8zYS_SKggPN4iEgvnHyvveLxVs9pbCIPrcVIT9d0c8.woff)
6 | format('woff');
7 | }
8 |
9 | @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@700&family=Noto+Serif:wght@400;700&family=Orelega+One&family=Roboto&display=swap');
10 |
11 | $comfortaa: 'Comfortaa', cursive;
12 | $orelega-one: 'Orelega One', cursive;
13 | $roboto: 'Roboto', sans-serif;
14 | $noto-serif: 'Noto Serif', serif;
15 |
--------------------------------------------------------------------------------
/client/stylesheets/aboutProject.scss:
--------------------------------------------------------------------------------
1 | .hero {
2 | min-height: 58vh;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | padding: 36px 16px 48px;
7 | }
8 |
9 | .hero__content {
10 | width: 100%;
11 | max-width: 1100px;
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | gap: 28px;
16 | justify-content: space-between;
17 | height: 100%;
18 | min-height: 50vh;
19 | }
20 |
21 | .hero__logo {
22 | width: clamp(280px, 32vw, 400px);
23 | height: auto;
24 | margin-bottom: 16px;
25 | display: block;
26 | margin-left: auto;
27 | margin-right: auto;
28 | margin-top: 2vh;
29 | }
30 |
31 | .hero__text {
32 | display: flex;
33 | flex-direction: column;
34 | align-items: center;
35 | gap: 12px;
36 | text-align: center;
37 | width: 100%;
38 | backdrop-filter: none;
39 | margin-bottom: 2vh;
40 | }
41 |
42 | @supports (background: color-mix(in srgb, white, transparent)) {
43 | .hero__text {
44 | background: color-mix(in srgb, #ffffff 6%, transparent);
45 | border-radius: 8px;
46 | padding: 4px 8px;
47 | }
48 | }
49 |
50 | .hero__title {
51 | font-family: 'Raleway', sans-serif;
52 | color: #0f264b;
53 | font-weight: 800;
54 | letter-spacing: -0.01em;
55 | font-size: clamp(30px, 4.8vw, 48px);
56 | line-height: 1.15;
57 | max-width: 900px;
58 | margin: 0 auto 10px;
59 | text-align: center;
60 | }
61 |
62 | .hero__subtitle {
63 | font-family: 'Raleway', sans-serif;
64 | color: #475569;
65 | font-size: clamp(18px, 2.3vw, 22px);
66 | max-width: 760px;
67 | margin: 0 auto;
68 | line-height: 1.55;
69 | text-align: center;
70 | }
71 |
72 | .hero__ctas {
73 | display: flex;
74 | gap: 10px;
75 | margin-top: 18px;
76 | flex-wrap: wrap;
77 | justify-content: center;
78 | }
79 |
80 | .btn {
81 | height: 42px;
82 | padding: 0 18px;
83 | border-radius: 9999px;
84 | font-weight: 600;
85 | text-decoration: none;
86 | display: inline-flex;
87 | align-items: center;
88 | font-size: 16px;
89 | transition:
90 | transform 0.06s ease,
91 | box-shadow 0.06s ease,
92 | background-color 0.2s ease,
93 | color 0.2s ease,
94 | border-color 0.2s ease;
95 | }
96 |
97 | .btn--primary {
98 | background: #0f264b;
99 | color: #fff;
100 | }
101 |
102 | .btn--primary:hover {
103 | background: #ff9149;
104 | color: #fff;
105 | }
106 |
107 | .btn--ghost {
108 | border: 1px solid #0f264b;
109 | color: #0f264b;
110 | background: transparent;
111 | }
112 |
113 | .btn--ghost:hover {
114 | border-color: #ff9149;
115 | color: #ff9149;
116 | box-shadow: 0 0 8px rgba(255, 145, 73, 0.3);
117 | }
118 |
119 | .btn:hover {
120 | transform: translateY(-1px);
121 | }
122 |
123 | @media (max-width: 640px) {
124 | .hero {
125 | min-height: 56vh;
126 | padding: 32px 12px;
127 | }
128 |
129 | .hero__logo {
130 | width: clamp(200px, 40vw, 260px);
131 | }
132 |
133 | .hero__title {
134 | font-size: clamp(24px, 5vw, 40px);
135 | }
136 | }
137 |
138 | #about {
139 | width: 100%;
140 | height: 65vh;
141 | display: flex;
142 | justify-content: space-evenly;
143 | align-items: center;
144 | margin: 0px 0px 20px 0px;
145 |
146 | #logo {
147 | height: 50%;
148 | }
149 |
150 | .aboutProject {
151 | background-color: #f0f1f1;
152 | width: 30%;
153 | height: 80%;
154 | display: flex;
155 | flex-direction: column;
156 | border: 2px solid $orange;
157 | box-shadow: 0px 0px 10px #aaa9a9;
158 | border-radius: 5px;
159 |
160 | h3 {
161 | font-size: 50px;
162 | }
163 |
164 | p {
165 | font-size: 31px;
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/client/stylesheets/aboutUs.scss:
--------------------------------------------------------------------------------
1 | .faqContainer {
2 | font-family: 'Raleway', sans-serif;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: center;
7 | max-width: 100%;
8 | height: auto;
9 | min-height: 60vh;
10 | margin-top: 50px;
11 | padding: 0px 0px 80px 0px;
12 | overflow: auto;
13 | scroll-margin-top: 40px;
14 |
15 | .faq__stack {
16 | display: flex;
17 | flex-direction: column;
18 | gap: 12px;
19 | width: 75%;
20 | max-width: 800px;
21 | align-items: center;
22 | }
23 |
24 | h2 {
25 | font-size: 60px;
26 | letter-spacing: 0.05em;
27 | color: $dark-blue;
28 | width: 100%;
29 | display: flex;
30 | justify-content: center;
31 | align-items: center;
32 | text-align: center;
33 | margin: 0;
34 | }
35 |
36 | .faq-anchor {
37 | color: $orange;
38 | text-decoration: none;
39 | font-size: 18px;
40 | margin: 0;
41 | transition: color 0.2s ease;
42 | text-align: center;
43 |
44 | &:hover {
45 | color: darken($orange, 10%);
46 | }
47 | }
48 |
49 | .faq-accordion {
50 | display: flex;
51 | flex-direction: column;
52 | gap: 16px;
53 | margin: 0;
54 | margin-top: 6px;
55 | width: 100%;
56 | }
57 |
58 | .faq-item {
59 | background-color: $dark-blue;
60 | border-radius: 8px;
61 | overflow: hidden;
62 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
63 | }
64 |
65 | .faq-question {
66 | width: 100%;
67 | background-color: $dark-blue;
68 | border: none;
69 | padding: 20px 24px;
70 | display: flex;
71 | justify-content: space-between;
72 | align-items: center;
73 | cursor: pointer;
74 | color: white;
75 | font-family: 'Raleway', sans-serif;
76 | font-size: 18px;
77 | font-weight: 600;
78 | text-align: left;
79 | transition: all 0.2s ease;
80 | border-radius: 8px;
81 |
82 | &:hover {
83 | background-color: lighten($dark-blue, 5%);
84 | }
85 |
86 | &:focus {
87 | outline: 2px solid $orange;
88 | outline-offset: -2px;
89 | }
90 |
91 | &.expanded {
92 | border-bottom-left-radius: 0;
93 | border-bottom-right-radius: 0;
94 | }
95 | }
96 |
97 | .faq-question-text {
98 | flex: 1;
99 | margin-right: 16px;
100 | }
101 |
102 | .faq-chevron {
103 | transition: transform 0.2s ease;
104 | flex-shrink: 0;
105 | color: white;
106 | }
107 |
108 | .faq-question.expanded .faq-chevron {
109 | transform: rotate(90deg);
110 | }
111 |
112 | .faq-answer {
113 | max-height: 0;
114 | overflow: hidden;
115 | transition:
116 | max-height 0.3s ease,
117 | opacity 0.3s ease;
118 | opacity: 0;
119 | background-color: lighten($dark-blue, 3%);
120 |
121 | &.expanded {
122 | max-height: 300px;
123 | opacity: 1;
124 | }
125 |
126 | p {
127 | padding: 20px 24px;
128 | margin: 0;
129 | color: #e0e0e0;
130 | font-size: 16px;
131 | line-height: 1.6;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/client/stylesheets/codeContainer.scss:
--------------------------------------------------------------------------------
1 | .codeContainer {
2 | position: relative;
3 | height: 0;
4 | width: 0;
5 | flex: 0 0 auto;
6 |
7 | .code-toggle-btn {
8 | position: fixed;
9 | top: 50%;
10 | transform: translateY(-50%);
11 | width: 40px;
12 | height: 80px;
13 | background-color: var(--orbit-navy);
14 | border: none;
15 | color: var(--orbit-orange);
16 | font-size: 18px;
17 | cursor: pointer;
18 | border-radius: 8px 0 0 8px;
19 | border-left: 1px solid var(--orbit-border);
20 | z-index: 999;
21 | transition: all 0.2s ease;
22 |
23 | &:hover {
24 | background-color: var(--orbit-orange);
25 | color: var(--orbit-navy);
26 | }
27 |
28 | &.open {
29 | background-color: var(--orbit-orange);
30 | color: var(--orbit-navy);
31 |
32 | &:hover {
33 | background-color: var(--orbit-navy);
34 | color: var(--orbit-orange);
35 | }
36 | }
37 | }
38 | }
39 |
40 | // New Orbit-branded Schema Drawer
41 | .schema-drawer {
42 | position: fixed;
43 | top: 12vh;
44 | right: 0;
45 | height: 88vh;
46 | background-color: var(--orbit-bg-primary);
47 | color: var(--orbit-text-primary);
48 | border-left: 1px solid var(--orbit-border);
49 | transition: width 0.2s ease;
50 | z-index: 1000;
51 | display: flex;
52 | flex-direction: column;
53 | font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'Liberation Mono', monospace;
54 |
55 | &--collapsed {
56 | width: 44px !important;
57 |
58 | .schema-drawer__header {
59 | padding: 0;
60 | justify-content: center;
61 | }
62 |
63 | .schema-drawer__tabs,
64 | .schema-drawer__controls {
65 | display: none;
66 | }
67 |
68 | .schema-drawer__control--collapse {
69 | display: flex !important;
70 | transform: rotate(180deg);
71 | }
72 | }
73 |
74 | // Resize handle
75 | &__resize-handle {
76 | position: absolute;
77 | left: 0;
78 | top: 0;
79 | width: 6px;
80 | height: 100%;
81 | background-color: transparent;
82 | cursor: col-resize;
83 | transition: background-color 0.2s ease;
84 |
85 | &:hover {
86 | background-color: var(--orbit-border);
87 | }
88 |
89 | &:active {
90 | background-color: var(--orbit-orange);
91 | }
92 | }
93 |
94 | // Header
95 | &__header {
96 | display: flex;
97 | align-items: center;
98 | justify-content: space-between;
99 | padding: 0 16px;
100 | height: 44px;
101 | background-color: var(--orbit-navy);
102 | border-bottom: 1px solid var(--orbit-border);
103 | flex-shrink: 0;
104 | position: sticky;
105 | top: 0;
106 | z-index: 10;
107 | }
108 |
109 | // Tabs
110 | &__tabs {
111 | display: flex;
112 | gap: 4px;
113 | }
114 |
115 | &__tab {
116 | padding: 6px 12px;
117 | background-color: transparent;
118 | border: none;
119 | color: var(--orbit-text-secondary);
120 | font-size: 13px;
121 | font-weight: 500;
122 | cursor: pointer;
123 | border-radius: 6px;
124 | transition: all 0.2s ease;
125 | position: relative;
126 |
127 | &:hover {
128 | background-color: rgba(255, 255, 255, 0.1);
129 | color: var(--orbit-text-primary);
130 | }
131 |
132 | &:focus {
133 | outline: 2px solid var(--orbit-orange);
134 | outline-offset: 2px;
135 | }
136 |
137 | &--active {
138 | color: var(--orbit-text-primary);
139 | font-weight: 600;
140 | border-bottom: 2px solid var(--orbit-orange);
141 | }
142 | }
143 |
144 | // Controls
145 | &__controls {
146 | display: flex;
147 | gap: 4px;
148 | align-items: center;
149 | position: relative;
150 | }
151 |
152 | &__copy-confirmation {
153 | position: absolute;
154 | top: -30px;
155 | right: 0;
156 | background-color: var(--orbit-orange);
157 | color: var(--orbit-navy);
158 | padding: 4px 8px;
159 | border-radius: 4px;
160 | font-size: 12px;
161 | font-weight: 500;
162 | white-space: nowrap;
163 | z-index: 20;
164 | animation: fadeInOut 2s ease-in-out;
165 | }
166 |
167 | @keyframes fadeInOut {
168 | 0% {
169 | opacity: 0;
170 | transform: translateY(5px);
171 | }
172 | 20% {
173 | opacity: 1;
174 | transform: translateY(0);
175 | }
176 | 80% {
177 | opacity: 1;
178 | transform: translateY(0);
179 | }
180 | 100% {
181 | opacity: 0;
182 | transform: translateY(-5px);
183 | }
184 | }
185 |
186 | &__control {
187 | width: 28px;
188 | height: 28px;
189 | background-color: transparent;
190 | border: none;
191 | color: var(--orbit-text-secondary);
192 | cursor: pointer;
193 | border-radius: 4px;
194 | display: flex;
195 | align-items: center;
196 | justify-content: center;
197 | transition: all 0.2s ease;
198 |
199 | &:hover {
200 | background-color: rgba(255, 255, 255, 0.1);
201 | color: var(--orbit-text-primary);
202 | }
203 |
204 | &:focus {
205 | outline: 2px solid var(--orbit-orange);
206 | outline-offset: 2px;
207 | }
208 |
209 | &--active {
210 | background-color: var(--orbit-orange);
211 | color: var(--orbit-navy);
212 | }
213 |
214 | &--collapse {
215 | transition: transform 0.2s ease;
216 | }
217 |
218 | &--collapsed {
219 | transform: rotate(180deg);
220 | }
221 |
222 | svg {
223 | width: 16px;
224 | height: 16px;
225 | }
226 | }
227 |
228 | // Editor
229 | &__editor {
230 | flex: 1;
231 | overflow: hidden;
232 | position: relative;
233 | }
234 |
235 | &__code-editor {
236 | height: 100%;
237 | width: 100%;
238 |
239 | .CodeMirror {
240 | height: 100%;
241 | font-family:
242 | 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'Liberation Mono', monospace;
243 | font-size: 13.5px;
244 | line-height: 1.6;
245 | background-color: var(--orbit-bg-primary);
246 | color: var(--orbit-text-primary);
247 |
248 | .CodeMirror-gutters {
249 | background-color: var(--orbit-bg-secondary);
250 | border-right: 1px solid var(--orbit-border);
251 | color: var(--orbit-text-secondary);
252 | }
253 |
254 | .CodeMirror-linenumber {
255 | color: var(--orbit-text-secondary);
256 | padding: 0 8px;
257 | }
258 |
259 | .CodeMirror-cursor {
260 | border-left: 2px solid var(--orbit-orange);
261 | }
262 |
263 | .CodeMirror-selected {
264 | background-color: var(--orbit-selection);
265 | }
266 |
267 | .CodeMirror-activeline-background {
268 | background-color: rgba(255, 255, 255, 0.05);
269 | }
270 |
271 | // Custom theme colors
272 | .cm-s-orbit-dark {
273 | .cm-keyword {
274 | color: var(--orbit-orange);
275 | }
276 |
277 | .cm-string {
278 | color: #8b9bb4;
279 | }
280 |
281 | .cm-comment {
282 | color: #6b7a8f;
283 | }
284 |
285 | .cm-number {
286 | color: #eaf0f7;
287 | }
288 |
289 | .cm-variable {
290 | color: #eaf0f7;
291 | }
292 |
293 | .cm-property {
294 | color: #eaf0f7;
295 | }
296 |
297 | .cm-operator {
298 | color: #eaf0f7;
299 | }
300 |
301 | .cm-punctuation {
302 | color: #eaf0f7;
303 | }
304 | }
305 | }
306 | }
307 |
308 | &__editor-fallback {
309 | height: 100%;
310 | width: 100%;
311 | background-color: var(--orbit-bg-primary);
312 | color: var(--orbit-text-primary);
313 | font-family:
314 | 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'Liberation Mono', monospace;
315 | font-size: 13.5px;
316 | line-height: 1.6;
317 | padding: 16px;
318 | overflow: auto;
319 |
320 | pre {
321 | margin: 0;
322 | white-space: pre-wrap;
323 | word-wrap: break-word;
324 | }
325 | }
326 | }
327 |
328 | // Legacy styles for backward compatibility (can be removed later)
329 | .legacy-codeContainer {
330 | background-color: #f0f1f1;
331 | display: flex;
332 | flex-direction: column;
333 | align-items: stretch;
334 | overflow-y: auto;
335 | font-size: 8px;
336 | margin-top: 4px;
337 |
338 | .codeButtons {
339 | display: flex;
340 | width: 100%;
341 | justify-content: space-evenly;
342 | }
343 |
344 | .codeContainerButton {
345 | border: none;
346 | border-radius: 5px;
347 | width: 30%;
348 | padding: 10px 0px;
349 | background-color: $dark-blue;
350 | color: $orange;
351 | font-size: 15px;
352 | }
353 |
354 | .codeContainerButton:hover {
355 | cursor: pointer;
356 | filter: brightness(85%);
357 | }
358 |
359 | p {
360 | font-size: 10px;
361 | }
362 |
363 | /*
364 | SCHEMA/RESOLVER PANEL STYLING
365 | */
366 | .sidebar {
367 | display: none;
368 | position: absolute;
369 | right: -25%;
370 | height: 80%;
371 | width: 25%;
372 | transition: width 0.5s ease-in-out;
373 | background-color: white;
374 | overflow-y: scroll;
375 | box-shadow: 0px 0px 10px #aaa9a9;
376 | z-index: 9;
377 | }
378 |
379 | .sidebar.open {
380 | display: block;
381 | right: 0;
382 | }
383 |
384 | .codeToggleBtn {
385 | font-size: 50px;
386 | background-color: $orange;
387 | color: $dark-blue;
388 | position: fixed;
389 | width: 50px;
390 | right: 0px;
391 | border: none;
392 | transition: left 0.5s ease-in-out;
393 | box-shadow: 0px 0px 10px #aaa9a9;
394 | z-index: 10;
395 | }
396 |
397 | .codeToggleBtn:hover {
398 | filter: brightness(85%);
399 | }
400 |
401 | .codeToggleBtn.open {
402 | right: 25%;
403 | background-color: $dark-blue;
404 | color: $orange;
405 | }
406 |
407 | .codeToggleBtn.open:hover {
408 | filter: brightness(85%);
409 | }
410 | }
411 |
--------------------------------------------------------------------------------
/client/stylesheets/codeMirror.scss:
--------------------------------------------------------------------------------
1 | /* BASICS */
2 |
3 | .CodeMirror {
4 | /* Set height, width, borders, and global font properties here */
5 | font-family: monospace;
6 | height: 100%;
7 | width: 100%;
8 | color: black;
9 | direction: ltr;
10 | }
11 |
12 | /* PADDING */
13 |
14 | .CodeMirror-lines {
15 | padding: 4px 0; /* Vertical padding around content */
16 | }
17 | .CodeMirror pre.CodeMirror-line,
18 | .CodeMirror pre.CodeMirror-line-like {
19 | padding: 0 4px; /* Horizontal padding of content */
20 | }
21 |
22 | .CodeMirror-scrollbar-filler,
23 | .CodeMirror-gutter-filler {
24 | background-color: white; /* The little square between H and V scrollbars */
25 | }
26 |
27 | /* GUTTER */
28 |
29 | .CodeMirror-gutters {
30 | border-right: 1px solid #ddd;
31 | background-color: #f7f7f7;
32 | white-space: nowrap;
33 | }
34 | .CodeMirror-linenumbers {
35 | }
36 | .CodeMirror-linenumber {
37 | padding: 0 3px 0 5px;
38 | min-width: 20px;
39 | text-align: right;
40 | color: #999;
41 | white-space: nowrap;
42 | }
43 |
44 | .CodeMirror-guttermarker {
45 | color: black;
46 | }
47 | .CodeMirror-guttermarker-subtle {
48 | color: #999;
49 | }
50 |
51 | /* CURSOR */
52 |
53 | .CodeMirror-cursor {
54 | border-left: 1px solid black;
55 | border-right: none;
56 | width: 0;
57 | }
58 | /* Shown when moving in bi-directional text */
59 | .CodeMirror div.CodeMirror-secondarycursor {
60 | border-left: 1px solid silver;
61 | }
62 | .cm-fat-cursor .CodeMirror-cursor {
63 | width: auto;
64 | border: 0 !important;
65 | background: #7e7;
66 | }
67 | .cm-fat-cursor div.CodeMirror-cursors {
68 | z-index: 1;
69 | }
70 | .cm-fat-cursor-mark {
71 | background-color: rgba(20, 255, 20, 0.5);
72 | -webkit-animation: blink 1.06s steps(1) infinite;
73 | -moz-animation: blink 1.06s steps(1) infinite;
74 | animation: blink 1.06s steps(1) infinite;
75 | }
76 | .cm-animate-fat-cursor {
77 | width: auto;
78 | border: 0;
79 | -webkit-animation: blink 1.06s steps(1) infinite;
80 | -moz-animation: blink 1.06s steps(1) infinite;
81 | animation: blink 1.06s steps(1) infinite;
82 | background-color: #7e7;
83 | }
84 | @-moz-keyframes blink {
85 | 0% {
86 | }
87 | 50% {
88 | background-color: transparent;
89 | }
90 | 100% {
91 | }
92 | }
93 | @-webkit-keyframes blink {
94 | 0% {
95 | }
96 | 50% {
97 | background-color: transparent;
98 | }
99 | 100% {
100 | }
101 | }
102 | @keyframes blink {
103 | 0% {
104 | }
105 | 50% {
106 | background-color: transparent;
107 | }
108 | 100% {
109 | }
110 | }
111 |
112 | /* Can style cursor different in overwrite (non-insert) mode */
113 | .CodeMirror-overwrite .CodeMirror-cursor {
114 | }
115 |
116 | .cm-tab {
117 | display: inline-block;
118 | text-decoration: inherit;
119 | }
120 |
121 | .CodeMirror-rulers {
122 | position: absolute;
123 | left: 0;
124 | right: 0;
125 | top: -50px;
126 | bottom: 0;
127 | overflow: hidden;
128 | }
129 | .CodeMirror-ruler {
130 | border-left: 1px solid #ccc;
131 | top: 0;
132 | bottom: 0;
133 | position: absolute;
134 | }
135 |
136 | /* DEFAULT THEME */
137 |
138 | .cm-s-default .cm-header {
139 | color: blue;
140 | }
141 | .cm-s-default .cm-quote {
142 | color: #090;
143 | }
144 | .cm-negative {
145 | color: #d44;
146 | }
147 | .cm-positive {
148 | color: #292;
149 | }
150 | .cm-header,
151 | .cm-strong {
152 | font-weight: bold;
153 | }
154 | .cm-em {
155 | font-style: italic;
156 | }
157 | .cm-link {
158 | text-decoration: underline;
159 | }
160 | .cm-strikethrough {
161 | text-decoration: line-through;
162 | }
163 |
164 | .cm-s-default .cm-keyword {
165 | color: #708;
166 | }
167 | .cm-s-default .cm-atom {
168 | color: #219;
169 | }
170 | .cm-s-default .cm-number {
171 | color: #164;
172 | }
173 | .cm-s-default .cm-def {
174 | color: #00f;
175 | }
176 | .cm-s-default .cm-variable,
177 | .cm-s-default .cm-punctuation,
178 | .cm-s-default .cm-property,
179 | .cm-s-default .cm-operator {
180 | }
181 | .cm-s-default .cm-variable-2 {
182 | color: #05a;
183 | }
184 | .cm-s-default .cm-variable-3,
185 | .cm-s-default .cm-type {
186 | color: #085;
187 | }
188 | .cm-s-default .cm-comment {
189 | color: #a50;
190 | }
191 | .cm-s-default .cm-string {
192 | color: #a11;
193 | }
194 | .cm-s-default .cm-string-2 {
195 | color: #f50;
196 | }
197 | .cm-s-default .cm-meta {
198 | color: #555;
199 | }
200 | .cm-s-default .cm-qualifier {
201 | color: #555;
202 | }
203 | .cm-s-default .cm-builtin {
204 | color: #30a;
205 | }
206 | .cm-s-default .cm-bracket {
207 | color: #997;
208 | }
209 | .cm-s-default .cm-tag {
210 | color: #170;
211 | }
212 | .cm-s-default .cm-attribute {
213 | color: #00c;
214 | }
215 | .cm-s-default .cm-hr {
216 | color: #999;
217 | }
218 | .cm-s-default .cm-link {
219 | color: #00c;
220 | }
221 |
222 | .cm-s-default .cm-error {
223 | color: #f00;
224 | }
225 | .cm-invalidchar {
226 | color: #f00;
227 | }
228 |
229 | .CodeMirror-composing {
230 | border-bottom: 2px solid;
231 | }
232 |
233 | /* Default styles for common addons */
234 |
235 | div.CodeMirror span.CodeMirror-matchingbracket {
236 | color: #0b0;
237 | }
238 | div.CodeMirror span.CodeMirror-nonmatchingbracket {
239 | color: #a22;
240 | }
241 | .CodeMirror-matchingtag {
242 | background: rgba(255, 150, 0, 0.3);
243 | }
244 | .CodeMirror-activeline-background {
245 | background: #e8f2ff;
246 | }
247 |
248 | /* STOP */
249 |
250 | /* The rest of this file contains styles related to the mechanics of
251 | the editor. You probably shouldn't touch them. */
252 |
253 | .CodeMirror {
254 | position: relative;
255 | overflow: hidden;
256 | background: white;
257 | }
258 |
259 | .CodeMirror-scroll {
260 | overflow: scroll !important; /* Things will break if this is overridden */
261 | /* 50px is the magic margin used to hide the element's real scrollbars */
262 | /* See overflow: hidden in .CodeMirror */
263 | margin-bottom: -50px;
264 | margin-right: -50px;
265 | padding-bottom: 50px;
266 | height: 100%;
267 | outline: none; /* Prevent dragging from highlighting the element */
268 | position: relative;
269 | }
270 | .CodeMirror-sizer {
271 | position: relative;
272 | border-right: 50px solid transparent;
273 | }
274 |
275 | /* The fake, visible scrollbars. Used to force redraw during scrolling
276 | before actual scrolling happens, thus preventing shaking and
277 | flickering artifacts. */
278 | .CodeMirror-vscrollbar,
279 | .CodeMirror-hscrollbar,
280 | .CodeMirror-scrollbar-filler,
281 | .CodeMirror-gutter-filler {
282 | position: absolute;
283 | z-index: 6;
284 | display: none;
285 | outline: none;
286 | }
287 | .CodeMirror-vscrollbar {
288 | right: 0;
289 | top: 0;
290 | overflow-x: hidden;
291 | overflow-y: scroll;
292 | }
293 | .CodeMirror-hscrollbar {
294 | bottom: 0;
295 | left: 0;
296 | overflow-y: hidden;
297 | overflow-x: scroll;
298 | }
299 | .CodeMirror-scrollbar-filler {
300 | right: 0;
301 | bottom: 0;
302 | }
303 | .CodeMirror-gutter-filler {
304 | left: 0;
305 | bottom: 0;
306 | }
307 |
308 | .CodeMirror-gutters {
309 | position: absolute;
310 | left: 0;
311 | top: 0;
312 | min-height: 100%;
313 | z-index: 3;
314 | }
315 | .CodeMirror-gutter {
316 | white-space: normal;
317 | height: 100%;
318 | display: inline-block;
319 | vertical-align: top;
320 | margin-bottom: -50px;
321 | }
322 | .CodeMirror-gutter-wrapper {
323 | position: absolute;
324 | z-index: 4;
325 | background: none !important;
326 | border: none !important;
327 | }
328 | .CodeMirror-gutter-background {
329 | position: absolute;
330 | top: 0;
331 | bottom: 0;
332 | z-index: 4;
333 | }
334 | .CodeMirror-gutter-elt {
335 | position: absolute;
336 | cursor: default;
337 | z-index: 4;
338 | }
339 | .CodeMirror-gutter-wrapper ::selection {
340 | background-color: transparent;
341 | }
342 | .CodeMirror-gutter-wrapper ::-moz-selection {
343 | background-color: transparent;
344 | }
345 |
346 | .CodeMirror-lines {
347 | cursor: text;
348 | min-height: 1px; /* prevents collapsing before first draw */
349 | }
350 | .CodeMirror pre.CodeMirror-line,
351 | .CodeMirror pre.CodeMirror-line-like {
352 | /* Reset some styles that the rest of the page might have set */
353 | -moz-border-radius: 0;
354 | -webkit-border-radius: 0;
355 | border-radius: 0;
356 | border-width: 0;
357 | background: transparent;
358 | font-family: inherit;
359 | font-size: inherit;
360 | margin: 0;
361 | white-space: pre;
362 | word-wrap: normal;
363 | line-height: inherit;
364 | color: inherit;
365 | z-index: 2;
366 | position: relative;
367 | overflow: visible;
368 | -webkit-tap-highlight-color: transparent;
369 | -webkit-font-variant-ligatures: contextual;
370 | font-variant-ligatures: contextual;
371 | }
372 | .CodeMirror-wrap pre.CodeMirror-line,
373 | .CodeMirror-wrap pre.CodeMirror-line-like {
374 | word-wrap: break-word;
375 | white-space: pre-wrap;
376 | word-break: normal;
377 | }
378 |
379 | .CodeMirror-linebackground {
380 | position: absolute;
381 | left: 0;
382 | right: 0;
383 | top: 0;
384 | bottom: 0;
385 | z-index: 0;
386 | }
387 |
388 | .CodeMirror-linewidget {
389 | position: relative;
390 | z-index: 2;
391 | padding: 0.1px; /* Force widget margins to stay inside of the container */
392 | }
393 |
394 | .CodeMirror-widget {
395 | }
396 |
397 | .CodeMirror-rtl pre {
398 | direction: rtl;
399 | }
400 |
401 | .CodeMirror-code {
402 | outline: none;
403 | }
404 |
405 | /* Force content-box sizing for the elements where we expect it */
406 | .CodeMirror-scroll,
407 | .CodeMirror-sizer,
408 | .CodeMirror-gutter,
409 | .CodeMirror-gutters,
410 | .CodeMirror-linenumber {
411 | -moz-box-sizing: content-box;
412 | box-sizing: content-box;
413 | }
414 |
415 | .CodeMirror-measure {
416 | position: absolute;
417 | width: 100%;
418 | height: 0;
419 | overflow: hidden;
420 | visibility: hidden;
421 | }
422 |
423 | .CodeMirror-cursor {
424 | position: absolute;
425 | pointer-events: none;
426 | }
427 | .CodeMirror-measure pre {
428 | position: static;
429 | }
430 |
431 | div.CodeMirror-cursors {
432 | visibility: hidden;
433 | position: relative;
434 | z-index: 3;
435 | }
436 | div.CodeMirror-dragcursors {
437 | visibility: visible;
438 | }
439 |
440 | .CodeMirror-focused div.CodeMirror-cursors {
441 | visibility: visible;
442 | }
443 |
444 | .CodeMirror-selected {
445 | background: #d9d9d9;
446 | }
447 | .CodeMirror-focused .CodeMirror-selected {
448 | background: #d7d4f0;
449 | }
450 | .CodeMirror-crosshair {
451 | cursor: crosshair;
452 | }
453 | .CodeMirror-line::selection,
454 | .CodeMirror-line > span::selection,
455 | .CodeMirror-line > span > span::selection {
456 | background: #d7d4f0;
457 | }
458 | .CodeMirror-line::-moz-selection,
459 | .CodeMirror-line > span::-moz-selection,
460 | .CodeMirror-line > span > span::-moz-selection {
461 | background: #d7d4f0;
462 | }
463 |
464 | .cm-searching {
465 | background-color: #ffa;
466 | background-color: rgba(255, 255, 0, 0.4);
467 | }
468 |
469 | /* Used to force a border model for a node */
470 | .cm-force-border {
471 | padding-right: 0.1px;
472 | }
473 |
474 | @media print {
475 | /* Hide the cursor when printing */
476 | .CodeMirror div.CodeMirror-cursors {
477 | visibility: hidden;
478 | }
479 | }
480 |
481 | /* See issue #2901 */
482 | .cm-tab-wrap-hack:after {
483 | content: '';
484 | }
485 |
486 | /* Help users use markselection to safely style text background */
487 | span.CodeMirror-selectedtext {
488 | background: none;
489 | }
490 |
--------------------------------------------------------------------------------
/client/stylesheets/dataPage.scss:
--------------------------------------------------------------------------------
1 | .dataPage {
2 | font-family: 'RocknRoll One';
3 | max-width: 100%;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | }
8 |
9 | // large container to hold visualizer and code
10 | .graphicalContainer {
11 | font-family: 'Raleway';
12 | display: flex;
13 | height: 88vh;
14 | width: 100vw;
15 | justify-content: flex-end;
16 | }
17 |
--------------------------------------------------------------------------------
/client/stylesheets/demo.scss:
--------------------------------------------------------------------------------
1 | .demoContainer {
2 | font-family: 'Raleway', sans-serif;
3 | display: flex;
4 | flex-direction: column;
5 | color: white;
6 | background-color: $dark-blue;
7 | padding: 60px 20px;
8 |
9 | .demoHeader {
10 | text-align: center;
11 | margin-bottom: 60px;
12 |
13 | #sectionHeader {
14 | font-family: 'Raleway', sans-serif;
15 | font-size: 48px;
16 | letter-spacing: 0.05em;
17 | margin: 0 0 16px 0;
18 | color: white;
19 | }
20 |
21 | .demo-lead {
22 | font-size: 18px;
23 | color: #e0e0e0;
24 | margin: 0;
25 | line-height: 1.5;
26 | }
27 | }
28 |
29 | .feature-grid {
30 | display: grid;
31 | grid-template-columns: repeat(2, 1fr);
32 | gap: 32px;
33 | max-width: 1200px;
34 | margin: 0 auto 60px auto;
35 | align-items: stretch;
36 | }
37 |
38 | .feature-card {
39 | background: rgba(255, 255, 255, 0.05);
40 | border-radius: 12px;
41 | padding: 32px;
42 | display: flex;
43 | flex-direction: column;
44 | gap: 16px;
45 | transition: all 0.3s ease;
46 | border: 2px solid transparent;
47 |
48 | &:hover {
49 | transform: translateY(-8px);
50 | border-color: $orange;
51 | box-shadow: 0 8px 24px rgba(255, 145, 73, 0.2);
52 | }
53 | }
54 |
55 | .feature-step {
56 | display: inline-flex;
57 | align-items: center;
58 | justify-content: center;
59 | width: 32px;
60 | height: 32px;
61 | background-color: $orange;
62 | color: white;
63 | border-radius: 50%;
64 | font-weight: 600;
65 | font-size: 16px;
66 | flex-shrink: 0;
67 | }
68 |
69 | .feature-title {
70 | color: $orange;
71 | font-size: 24px;
72 | font-weight: 600;
73 | margin: 0;
74 | line-height: 1.3;
75 | }
76 |
77 | .feature-description {
78 | font-size: 16px;
79 | line-height: 1.6;
80 | color: #e0e0e0;
81 | margin: 0;
82 | }
83 |
84 | .feature-image {
85 | width: 100%;
86 | min-height: 200px;
87 | max-height: 400px;
88 | border-radius: 8px;
89 | overflow: hidden;
90 | border: 1px solid rgba(255, 145, 73, 0.3);
91 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
92 | display: flex;
93 | align-items: center;
94 | justify-content: center;
95 | background-color: rgba(255, 255, 255, 0.05);
96 |
97 | img {
98 | width: 100%;
99 | height: auto;
100 | display: block;
101 | }
102 | }
103 |
104 | // Demo card styles for DemoItem component
105 | .demo-card {
106 | display: flex;
107 | flex-direction: column;
108 | height: 100%;
109 | background: rgba(255, 255, 255, 0.05);
110 | border-radius: 12px;
111 | padding: 32px;
112 | }
113 |
114 | .demo-card__content {
115 | flex: 1;
116 | }
117 |
118 | .demo-card__content .featureName {
119 | margin: 0 0 8px 0;
120 | }
121 |
122 | .demo-card__content .featureDescription {
123 | margin: 0;
124 | }
125 |
126 | .demo-card__media {
127 | margin-top: auto;
128 | }
129 |
130 | .demo-card__media img {
131 | width: 100%;
132 | height: 240px;
133 | object-fit: cover;
134 | border-radius: 8px;
135 | }
136 |
137 | .demo-ctas {
138 | display: flex;
139 | justify-content: center;
140 | gap: 16px;
141 | margin-top: 40px;
142 | }
143 |
144 | .cta-primary {
145 | background-color: $orange;
146 | color: white;
147 | padding: 12px 24px;
148 | border-radius: 8px;
149 | text-decoration: none;
150 | font-weight: 600;
151 | font-size: 16px;
152 | transition: background-color 0.2s ease;
153 |
154 | &:hover {
155 | background-color: darken($orange, 10%);
156 | }
157 | }
158 |
159 | .cta-secondary {
160 | background-color: transparent;
161 | color: white;
162 | padding: 12px 24px;
163 | border-radius: 8px;
164 | text-decoration: none;
165 | font-weight: 600;
166 | font-size: 16px;
167 | border: 1px solid white;
168 | transition: all 0.2s ease;
169 |
170 | &:hover {
171 | background-color: white;
172 | color: $dark-blue;
173 | }
174 | }
175 |
176 | @media (max-width: 768px) {
177 | padding: 40px 16px;
178 |
179 | .demoHeader {
180 | margin-bottom: 40px;
181 |
182 | #sectionHeader {
183 | font-size: 36px;
184 | }
185 |
186 | .demo-lead {
187 | font-size: 16px;
188 | }
189 | }
190 |
191 | .feature-grid {
192 | grid-template-columns: 1fr;
193 | gap: 24px;
194 | margin-bottom: 40px;
195 | }
196 |
197 | .feature-card {
198 | padding: 24px;
199 | gap: 12px;
200 | }
201 |
202 | .feature-title {
203 | font-size: 20px;
204 | }
205 |
206 | .feature-description {
207 | font-size: 15px;
208 | }
209 |
210 | .feature-image {
211 | min-height: 180px;
212 | max-height: 320px;
213 | }
214 |
215 | .demo-card {
216 | padding: 24px;
217 | }
218 |
219 | .demo-card__content {
220 | flex: 1;
221 | }
222 |
223 | .demo-card__media img {
224 | height: 200px;
225 | }
226 |
227 | .demo-ctas {
228 | flex-direction: column;
229 | align-items: center;
230 | gap: 12px;
231 | }
232 |
233 | .cta-primary,
234 | .cta-secondary {
235 | width: 200px;
236 | text-align: center;
237 | }
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/client/stylesheets/footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | width: 100%;
3 | background-color: $dark-blue;
4 | color: #e0e0e0;
5 | font-family: 'Raleway', sans-serif;
6 | padding: 16px 20px;
7 | min-height: 56px;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 |
12 | .footer-content {
13 | width: 100%;
14 | max-width: 1100px;
15 | display: flex;
16 | align-items: center;
17 | justify-content: space-between;
18 | gap: 20px;
19 | }
20 |
21 | .footer-left {
22 | display: flex;
23 | align-items: center;
24 | gap: 8px;
25 | flex-shrink: 0;
26 | }
27 |
28 | .footer-brand-link {
29 | display: flex;
30 | align-items: center;
31 | gap: 8px;
32 | text-decoration: none;
33 | color: inherit;
34 | transition: opacity 0.2s ease;
35 |
36 | &:hover {
37 | opacity: 0.8;
38 | }
39 |
40 | &:focus {
41 | outline: 2px solid $orange;
42 | outline-offset: 2px;
43 | }
44 | }
45 |
46 | .footer-logo {
47 | height: 20px;
48 | width: auto;
49 | display: block;
50 | image-rendering: -webkit-optimize-contrast;
51 | image-rendering: crisp-edges;
52 | }
53 |
54 | .footer-brand {
55 | font-weight: 600;
56 | font-size: 16px;
57 | color: white;
58 | }
59 |
60 | .footer-center {
61 | display: flex;
62 | align-items: center;
63 | gap: 8px;
64 | font-size: 14px;
65 | color: #e0e0e0;
66 | flex-shrink: 0;
67 | }
68 |
69 | .footer-right {
70 | display: flex;
71 | align-items: center;
72 | gap: 8px;
73 | flex-shrink: 0;
74 | }
75 |
76 | .footer-link {
77 | color: #e0e0e0;
78 | text-decoration: none;
79 | font-size: 14px;
80 | transition:
81 | color 0.2s ease,
82 | text-decoration 0.2s ease;
83 |
84 | &:hover {
85 | color: $orange;
86 | text-decoration: underline;
87 | }
88 |
89 | &:focus {
90 | outline: 2px solid $orange;
91 | outline-offset: 2px;
92 | }
93 | }
94 |
95 | .footer-divider {
96 | color: #999;
97 | font-size: 12px;
98 | }
99 |
100 | @media (max-width: 768px) {
101 | padding: 12px 16px;
102 | min-height: auto;
103 |
104 | .footer-content {
105 | flex-direction: column;
106 | gap: 12px;
107 | text-align: center;
108 | }
109 |
110 | .footer-left,
111 | .footer-center,
112 | .footer-right {
113 | justify-content: center;
114 | }
115 | }
116 |
117 | @media (max-width: 480px) {
118 | .footer-content {
119 | gap: 8px;
120 | }
121 |
122 | .footer-center,
123 | .footer-right {
124 | font-size: 13px;
125 | gap: 6px;
126 | }
127 |
128 | .footer-divider {
129 | font-size: 10px;
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/client/stylesheets/homePage.scss:
--------------------------------------------------------------------------------
1 | #homeBody {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0px 0px;
5 | padding: 0px 0px;
6 | background: white;
7 | background-image: radial-gradient(rgb(36, 35, 35) 1px, transparent 0);
8 | background-size: 25px 25px;
9 | background-position: -19px -19px;
10 | }
11 |
12 | .homePage {
13 | width: 100%;
14 | height: 100%;
15 | }
16 |
17 | .back-to-top-container {
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | padding: 30px 0;
22 | }
23 |
24 | .back-to-top-button {
25 | display: flex;
26 | align-items: center;
27 | gap: 6px;
28 | padding: 8px 16px;
29 | background-color: $dark-blue;
30 | color: white;
31 | border: none;
32 | border-radius: 6px;
33 | font-family: 'Raleway', sans-serif;
34 | font-size: 14px;
35 | font-weight: 600;
36 | cursor: pointer;
37 | transition: all 0.2s ease;
38 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
39 |
40 | &:hover {
41 | background-color: lighten($dark-blue, 10%);
42 | transform: translateY(-2px);
43 | box-shadow:
44 | 0 4px 12px rgba(0, 0, 0, 0.15),
45 | 0 0 0 2px rgba(255, 145, 73, 0.3);
46 | }
47 |
48 | &:focus {
49 | outline: 2px solid $orange;
50 | outline-offset: 2px;
51 | }
52 |
53 | &:active {
54 | transform: translateY(0);
55 | }
56 |
57 | svg {
58 | transition: transform 0.2s ease;
59 | width: 16px;
60 | height: 16px;
61 | }
62 |
63 | &:hover svg {
64 | transform: translateY(-1px);
65 | }
66 |
67 | span {
68 | font-weight: 600;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/client/stylesheets/index.scss:
--------------------------------------------------------------------------------
1 | @import './_fonts.scss';
2 | @import './_colors.scss';
3 | @import './aboutUs.scss';
4 | @import './navbar.scss';
5 | @import './homePage.scss';
6 | @import './aboutProject.scss';
7 | @import './demo.scss';
8 | @import './dataPage.scss';
9 | @import './visualizer.scss';
10 | @import './codeContainer.scss';
11 | @import './codeMirror.scss';
12 | @import './footer.scss';
13 | @import './uri.scss';
14 |
15 | html {
16 | height: 100%;
17 | width: 100%;
18 | }
19 |
20 | body {
21 | width: 100%;
22 | height: 100%;
23 | margin: 0px 0px;
24 | padding: 0px 0px;
25 | font-size: 40px;
26 | font-family: 'Raleway';
27 | font-family: 'Noto Serif';
28 | background-size: cover;
29 | }
30 |
--------------------------------------------------------------------------------
/client/stylesheets/navbar.scss:
--------------------------------------------------------------------------------
1 | // Mobile menu backdrop
2 | .mobile-menu-backdrop {
3 | position: fixed;
4 | top: 0;
5 | left: 0;
6 | width: 100%;
7 | height: 100%;
8 | background-color: rgba(0, 0, 0, 0.5);
9 | z-index: 999;
10 | display: none;
11 |
12 | @media (max-width: 768px) {
13 | display: block;
14 | }
15 | }
16 |
17 | #homeHeader {
18 | width: 100%;
19 | display: flex;
20 | justify-content: space-between;
21 | align-items: center;
22 | font-family: 'Raleway';
23 | position: relative;
24 |
25 | .socialLogos {
26 | margin-left: 40px;
27 | }
28 |
29 | .rightLinks {
30 | display: flex;
31 | }
32 |
33 | .headerLinks {
34 | color: $dark-blue;
35 | font-size: 37px;
36 | text-decoration: none;
37 | margin-right: 40px;
38 | }
39 |
40 | .headerLinks:visited {
41 | color: $dark-blue;
42 | }
43 |
44 | .headerLinks:hover {
45 | color: $orange;
46 | }
47 |
48 | .headerLinks:active {
49 | color: $orange;
50 | }
51 |
52 | .homeLogo {
53 | height: 50px;
54 | margin: 10px -10px 0px 0px;
55 | transition: all 0.3s ease;
56 | border-radius: 50%;
57 | }
58 |
59 | .homeLogo:hover {
60 | transform: translateY(-1px);
61 | box-shadow: 0 0 0 2px $orange;
62 | }
63 |
64 | .homeLogo:focus {
65 | outline: 2px solid $orange;
66 | outline-offset: 2px;
67 | }
68 |
69 | .homeLogo:active {
70 | transform: translateY(0);
71 | }
72 |
73 | // Hamburger menu styles
74 | .hamburger-menu {
75 | display: none;
76 | flex-direction: column;
77 | cursor: pointer;
78 | padding: 10px;
79 | z-index: 1001;
80 |
81 | .hamburger-line {
82 | width: 25px;
83 | height: 3px;
84 | background-color: $dark-blue;
85 | margin: 3px 0;
86 | transition: 0.3s;
87 | border-radius: 2px;
88 |
89 | &.open {
90 | &:nth-child(1) {
91 | transform: rotate(-45deg) translate(-5px, 6px);
92 | }
93 | &:nth-child(2) {
94 | opacity: 0;
95 | }
96 | &:nth-child(3) {
97 | transform: rotate(45deg) translate(-5px, -6px);
98 | }
99 | }
100 | }
101 | }
102 |
103 | // Mobile responsive styles
104 | @media (max-width: 768px) {
105 | .socialLogos {
106 | margin-left: 20px;
107 | }
108 |
109 | .homeLogo {
110 | height: 40px;
111 | margin: 8px -8px 0px 0px;
112 | transition: all 0.3s ease;
113 | border-radius: 50%;
114 | }
115 |
116 | .homeLogo:hover {
117 | transform: translateY(-1px);
118 | box-shadow: 0 0 0 2px $orange;
119 | }
120 |
121 | .homeLogo:focus {
122 | outline: 2px solid $orange;
123 | outline-offset: 2px;
124 | }
125 |
126 | .homeLogo:active {
127 | transform: translateY(0);
128 | }
129 |
130 | .hamburger-menu {
131 | display: flex;
132 | margin-right: 20px;
133 | }
134 |
135 | .rightLinks {
136 | position: fixed;
137 | top: 0;
138 | right: -100%;
139 | width: 250px;
140 | height: 100vh;
141 | background-color: white;
142 | flex-direction: column;
143 | justify-content: flex-start;
144 | align-items: center;
145 | padding-top: 80px;
146 | box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
147 | transition: right 0.3s ease;
148 | z-index: 1000;
149 |
150 | &.mobile-open {
151 | right: 0;
152 | }
153 |
154 | .headerLinks {
155 | margin: 20px 0;
156 | font-size: 28px;
157 | width: 100%;
158 | text-align: center;
159 | padding: 15px 0;
160 | border-bottom: 1px solid #eee;
161 |
162 | &:last-child {
163 | border-bottom: none;
164 | }
165 | }
166 | }
167 | }
168 |
169 | @media (max-width: 480px) {
170 | .socialLogos {
171 | margin-left: 15px;
172 | }
173 |
174 | .homeLogo {
175 | height: 35px;
176 | margin: 6px -6px 0px 0px;
177 | transition: all 0.3s ease;
178 | border-radius: 50%;
179 | }
180 |
181 | .homeLogo:hover {
182 | transform: translateY(-1px);
183 | box-shadow: 0 0 0 2px $orange;
184 | }
185 |
186 | .homeLogo:focus {
187 | outline: 2px solid $orange;
188 | outline-offset: 2px;
189 | }
190 |
191 | .homeLogo:active {
192 | transform: translateY(0);
193 | }
194 |
195 | .hamburger-menu {
196 | margin-right: 15px;
197 |
198 | .hamburger-line {
199 | width: 22px;
200 | height: 2px;
201 | }
202 | }
203 |
204 | .rightLinks {
205 | width: 200px;
206 |
207 | .headerLinks {
208 | font-size: 24px;
209 | padding: 12px 0;
210 | }
211 | }
212 | }
213 | }
214 |
215 | #appHeader {
216 | background-color: $dark-blue;
217 | width: 100%;
218 | display: flex;
219 | justify-content: space-between;
220 | align-items: center;
221 | font-family: 'Raleway';
222 | position: relative;
223 |
224 | .rightLinks {
225 | display: flex;
226 | }
227 |
228 | .headerLinks {
229 | color: white;
230 | font-size: 37px;
231 | font-weight: normal !important;
232 | text-decoration: none;
233 | margin-right: 40px;
234 | }
235 |
236 | .headerLinks p {
237 | font-weight: normal !important;
238 | }
239 |
240 | .headerLinks:visited {
241 | color: white;
242 | }
243 |
244 | .headerLinks:hover {
245 | color: $orange;
246 | }
247 |
248 | .headerLinks:active {
249 | color: $orange;
250 | }
251 |
252 | .homeLogo {
253 | height: 80px;
254 | margin-left: 20px;
255 | }
256 |
257 | .homeLogo:hover {
258 | color: $orange;
259 | }
260 |
261 | // Hamburger menu styles
262 | .hamburger-menu {
263 | display: none;
264 | flex-direction: column;
265 | cursor: pointer;
266 | padding: 10px;
267 | z-index: 1001;
268 |
269 | .hamburger-line {
270 | width: 25px;
271 | height: 3px;
272 | background-color: white;
273 | margin: 3px 0;
274 | transition: 0.3s;
275 | border-radius: 2px;
276 |
277 | &.open {
278 | &:nth-child(1) {
279 | transform: rotate(-45deg) translate(-5px, 6px);
280 | }
281 | &:nth-child(2) {
282 | opacity: 0;
283 | }
284 | &:nth-child(3) {
285 | transform: rotate(45deg) translate(-5px, -6px);
286 | }
287 | }
288 | }
289 | }
290 |
291 | // Mobile responsive styles
292 | @media (max-width: 768px) {
293 | .homeLogo {
294 | height: 60px;
295 | margin-left: 15px;
296 | }
297 |
298 | .hamburger-menu {
299 | display: flex;
300 | margin-right: 20px;
301 | }
302 |
303 | .rightLinks {
304 | position: fixed;
305 | top: 0;
306 | right: -100%;
307 | width: 250px;
308 | height: 100vh;
309 | background-color: $dark-blue;
310 | flex-direction: column;
311 | justify-content: flex-start;
312 | align-items: center;
313 | padding-top: 80px;
314 | box-shadow: -2px 0 5px rgba(0, 0, 0, 0.3);
315 | transition: right 0.3s ease;
316 | z-index: 1000;
317 |
318 | &.mobile-open {
319 | right: 0;
320 | }
321 |
322 | .headerLinks {
323 | margin: 20px 0;
324 | font-size: 28px;
325 | width: 100%;
326 | text-align: center;
327 | padding: 15px 0;
328 | border-bottom: 1px solid rgba(255, 255, 255, 0.1);
329 |
330 | &:last-child {
331 | border-bottom: none;
332 | }
333 | }
334 | }
335 | }
336 |
337 | @media (max-width: 480px) {
338 | .homeLogo {
339 | height: 50px;
340 | margin-left: 10px;
341 | }
342 |
343 | .hamburger-menu {
344 | margin-right: 15px;
345 |
346 | .hamburger-line {
347 | width: 22px;
348 | height: 2px;
349 | }
350 | }
351 |
352 | .rightLinks {
353 | width: 200px;
354 |
355 | .headerLinks {
356 | font-size: 24px;
357 | padding: 12px 0;
358 | }
359 | }
360 | }
361 | }
362 |
--------------------------------------------------------------------------------
/client/stylesheets/uri.scss:
--------------------------------------------------------------------------------
1 | /*
2 | URI FORM PANEL STYLING
3 | */
4 | .uripanel {
5 | font-family: 'Raleway', sans-serif;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | justify-content: center;
10 | position: absolute;
11 | top: 27%;
12 | left: -35%;
13 | height: 55%;
14 | width: 35%;
15 | transition: left 0.5s ease-in-out;
16 | z-index: 9;
17 | }
18 |
19 | .uripanel.open {
20 | left: 0;
21 | }
22 |
23 | .uripanelbtn {
24 | font-size: 50px;
25 | position: fixed;
26 | background-color: $orange;
27 | color: $dark-blue;
28 | border: none;
29 | width: 50px;
30 | top: 30%;
31 | left: 0px;
32 | z-index: 10;
33 | transition: left 0.5s ease-in-out;
34 | box-shadow: 0px 0px 10px #aaa9a9;
35 | }
36 |
37 | .uripanelbtn:hover {
38 | filter: brightness(80%);
39 | }
40 |
41 | .uripanelbtn.open {
42 | left: 35%;
43 | background-color: $dark-blue;
44 | color: $orange;
45 | }
46 |
47 | .uripanelbtn.open:hover {
48 | filter: brightness(80%);
49 | }
50 |
51 | /*
52 | MODERN DATABASE CARD STYLING
53 | */
54 | .database-card {
55 | background: white;
56 | border-radius: 12px;
57 | padding: 24px;
58 | box-shadow: 0 4px 20px rgba(15, 38, 75, 0.12);
59 | width: 100%;
60 | max-width: 380px;
61 | border: 1px solid rgba(255, 145, 73, 0.1);
62 | position: relative;
63 | }
64 |
65 | .collapse-button {
66 | position: absolute;
67 | top: 16px;
68 | right: 16px;
69 | background: none;
70 | border: none;
71 | color: #666;
72 | cursor: pointer;
73 | padding: 4px;
74 | border-radius: 4px;
75 | transition: all 0.2s ease;
76 |
77 | &:hover {
78 | color: $dark-blue;
79 | background-color: rgba(15, 38, 75, 0.05);
80 | }
81 |
82 | svg {
83 | transition: transform 0.2s ease;
84 | }
85 | }
86 |
87 | .card-header {
88 | font-family: 'Raleway', sans-serif;
89 | font-size: 20px;
90 | font-weight: 600;
91 | color: $dark-blue;
92 | text-align: center;
93 | margin: 0 0 8px 0;
94 | line-height: 1.3;
95 | }
96 |
97 | .card-helper {
98 | font-family: 'Raleway', sans-serif;
99 | font-size: 14px;
100 | color: #666;
101 | text-align: center;
102 | margin: 0 0 24px 0;
103 | line-height: 1.4;
104 | }
105 |
106 | .card-section {
107 | margin-bottom: 24px;
108 |
109 | &:last-child {
110 | margin-bottom: 0;
111 | }
112 | }
113 |
114 | .input-label {
115 | display: block;
116 | font-family: 'Raleway', sans-serif;
117 | font-size: 14px;
118 | font-weight: 500;
119 | color: $dark-blue;
120 | margin-bottom: 8px;
121 | }
122 |
123 | .database-input {
124 | width: 100%;
125 | padding: 12px 16px;
126 | border: 2px solid #e0e0e0;
127 | border-radius: 8px;
128 | font-family: 'Raleway', sans-serif;
129 | font-size: 14px;
130 | background-color: #f8f9fa;
131 | transition: all 0.2s ease;
132 | box-sizing: border-box;
133 |
134 | &:focus {
135 | outline: none;
136 | border-color: $orange;
137 | background-color: white;
138 | box-shadow: 0 0 0 2px rgba(255, 145, 73, 0.2);
139 | }
140 |
141 | &::placeholder {
142 | color: #666;
143 | }
144 |
145 | &.error {
146 | border-color: #dc3545;
147 | box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.1);
148 | }
149 |
150 | &:disabled {
151 | opacity: 0.6;
152 | cursor: not-allowed;
153 | }
154 | }
155 |
156 | .primary-button {
157 | width: 100%;
158 | min-height: 44px;
159 | padding: 12px 24px;
160 | background-color: $orange;
161 | color: white;
162 | border: none;
163 | border-radius: 8px;
164 | font-family: 'Raleway', sans-serif;
165 | font-size: 16px;
166 | font-weight: 600;
167 | cursor: pointer;
168 | transition: all 0.2s ease;
169 |
170 | &:hover:not(:disabled) {
171 | background-color: darken($orange, 8%);
172 | transform: translateY(-1px);
173 | box-shadow: 0 4px 12px rgba(255, 145, 73, 0.3);
174 | }
175 |
176 | &:active {
177 | transform: translateY(0);
178 | }
179 |
180 | &:disabled {
181 | opacity: 0.6;
182 | cursor: not-allowed;
183 | transform: none;
184 | }
185 | }
186 |
187 | .secondary-button {
188 | width: 100%;
189 | padding: 12px 24px;
190 | background-color: transparent;
191 | color: $dark-blue;
192 | border: 2px solid #d0d0d0;
193 | border-radius: 8px;
194 | font-family: 'Raleway', sans-serif;
195 | font-size: 14px;
196 | font-weight: 600;
197 | cursor: pointer;
198 | transition: all 0.2s ease;
199 | margin-top: 12px;
200 |
201 | &:hover:not(:disabled) {
202 | background-color: $dark-blue;
203 | color: white;
204 | transform: translateY(-1px);
205 | box-shadow: 0 4px 12px rgba(15, 38, 75, 0.2);
206 | }
207 |
208 | &:active {
209 | transform: translateY(0);
210 | }
211 |
212 | &:disabled {
213 | opacity: 0.6;
214 | cursor: not-allowed;
215 | transform: none;
216 | }
217 | }
218 |
219 | .input-helper {
220 | display: flex;
221 | align-items: center;
222 | gap: 6px;
223 | margin: 8px 0 0 0;
224 | font-family: 'Raleway', sans-serif;
225 | font-size: 12px;
226 | color: #666;
227 |
228 | svg {
229 | flex-shrink: 0;
230 | }
231 | }
232 |
233 | .error-message {
234 | color: #dc3545;
235 | font-family: 'Raleway', sans-serif;
236 | font-size: 12px;
237 | margin: 8px 0 0 0;
238 | min-height: 16px;
239 | }
240 |
241 | .loading-spinner {
242 | display: inline-block;
243 | width: 16px;
244 | height: 16px;
245 | border: 2px solid rgba(255, 255, 255, 0.3);
246 | border-radius: 50%;
247 | border-top-color: white;
248 | animation: spin 1s ease-in-out infinite;
249 | }
250 |
251 | @keyframes spin {
252 | to {
253 | transform: rotate(360deg);
254 | }
255 | }
256 |
257 | /*
258 | URI FORM STYLING
259 | */
260 | .uriForm {
261 | width: 100%;
262 | height: 100%;
263 | display: flex;
264 | flex-direction: column;
265 | align-items: center;
266 | justify-content: center;
267 | }
268 |
269 | .formContainer {
270 | width: 100%;
271 | }
272 |
--------------------------------------------------------------------------------
/client/stylesheets/visualizer.scss:
--------------------------------------------------------------------------------
1 | .diagramContainer {
2 | font-family: $comfortaa;
3 | border: 2px solid $orange;
4 | display: flex;
5 | flex-direction: column;
6 | flex-basis: 0;
7 | flex-grow: 999;
8 | }
9 |
10 | .tableHeader {
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | margin: -10px -0px 0px -10px;
15 | width: 370px;
16 | height: 40px;
17 | background-color: $dark-blue;
18 | color: $orange;
19 | font-size: 20px;
20 | }
21 |
22 | .columnDotContainer {
23 | width: 110%;
24 | margin: 0px 0px 0px -18px;
25 | display: flex;
26 | justify-content: space-between;
27 | line-height: 1.5em;
28 | }
29 |
30 | .leftColumn {
31 | display: flex;
32 | justify-content: flex-start;
33 | align-items: flex-start;
34 | padding: 3px 0px;
35 | margin-left: 1px;
36 | }
37 |
38 | .rightColumn {
39 | display: flex;
40 | align-items: flex-start;
41 | justify-content: flex-end;
42 | padding: 3px 0px;
43 | margin-right: 4px;
44 | }
45 |
46 | /*
47 | NO HANDLES
48 | */
49 | .noNodeColumnName {
50 | margin-left: 15px;
51 | }
52 |
53 | .noNodeDataType {
54 | margin-right: 15px;
55 | color: rgb(61, 60, 60);
56 | }
57 |
58 | /*
59 | TARGET HANDLES (orange)
60 | */
61 | .targetColumnName {
62 | margin-left: 5px;
63 | }
64 |
65 | .targetDataType {
66 | margin-right: 15px;
67 | color: rgb(61, 60, 60);
68 | }
69 |
70 | /*
71 | SOURCE HANDLES (blue)
72 | */
73 | .sourceColumnName {
74 | margin-left: 15px;
75 | }
76 |
77 | .sourceDataType {
78 | margin-right: 5px;
79 | color: rgb(61, 60, 60);
80 | }
81 |
82 | .sourceDot {
83 | margin-top: 0px;
84 | display: flex;
85 | align-items: center;
86 | }
87 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'jest-environment-jsdom',
3 | moduleFileExtensions: ['js', 'jsx'],
4 | transform: {
5 | '^.+\\.[jt]sx?$': 'babel-jest',
6 | },
7 | setupFilesAfterEnv: ['/jest.setup.js'],
8 | moduleNameMapper: {
9 | '\\.(css|scss)$': 'identity-obj-proxy',
10 | '\\.(png|jpg|jpeg|gif|svg|ico)$': '/__tests__/__mocks__/fileMock.js',
11 | '^react-flow-renderer$': '/__tests__/__mocks__/reactFlowMock.js',
12 | },
13 | testMatch: ['**/__tests__/**/*.test.{js,jsx}'],
14 | };
15 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | // Polyfills for Node environments used by some libs (e.g., pg)
4 | if (typeof global.TextEncoder === 'undefined') {
5 | const { TextEncoder, TextDecoder } = require('util');
6 | global.TextEncoder = TextEncoder;
7 | global.TextDecoder = TextDecoder;
8 | }
9 |
10 | // Mock fetch for jsdom environment
11 | if (typeof global.fetch === 'undefined') {
12 | global.fetch = jest.fn(() =>
13 | Promise.resolve({
14 | json: () =>
15 | Promise.resolve({
16 | SQLSchema: {},
17 | GQLSchema: { types: '', resolvers: '' },
18 | }),
19 | })
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Orbit",
3 | "version": "2.0.0",
4 | "description": "GraphQL migration aid and database visualization tool",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node server/server.js",
8 | "build": "NODE_ENV=production webpack",
9 | "build:analyze": "NODE_ENV=production ANALYZE=true webpack",
10 | "dev": "NODE_ENV=development nodemon server/server.js & NODE_ENV=development npx webpack serve --open",
11 | "test": "jest --verbose --runInBand",
12 | "lint": "eslint . --ext .js,.jsx",
13 | "lint:fix": "eslint . --ext .js,.jsx --fix",
14 | "format": "prettier --write ."
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/ryanmcd118/Orbit.git"
19 | },
20 | "author": "Chris Carney, Stacy Learn, John Li, & Ryan McDaniel",
21 | "license": "ISC",
22 | "bugs": {
23 | "url": "https://github.com/ryanmcd118/Orbit/issues"
24 | },
25 | "homepage": "https://github.com/ryanmcd118/Orbit#readme",
26 | "dependencies": {
27 | "buffer": "^6.0.3",
28 | "camelcase": "^5.3.1",
29 | "codemirror": "^5.60.0",
30 | "core-js": "^3.38.0",
31 | "crypto-browserify": "^3.12.0",
32 | "crypto-js": "^4.0.0",
33 | "express": "^4.17.1",
34 | "express-graphql": "^0.12.0",
35 | "file-loader": "^6.2.0",
36 | "graphiql": "^1.4.1",
37 | "graphql": "^15.5.0",
38 | "graphql-tools": "^9.0.1",
39 | "mini-css-extract-plugin": "^1.3.9",
40 | "pascal-case": "^3.1.2",
41 | "path": "^0.12.7",
42 | "path-browserify": "^1.0.1",
43 | "pg": "^8.5.1",
44 | "pluralize": "^8.0.0",
45 | "react": "^16.14.0",
46 | "react-codemirror2": "^7.2.1",
47 | "react-devtools": "^4.10.1",
48 | "react-dom": "^16.14.0",
49 | "react-flow-renderer": "^9.4.1",
50 | "react-hooks": "^1.0.1",
51 | "react-router-dom": "^5.2.0",
52 | "regenerator-runtime": "^0.14.1",
53 | "request": "^2.79.0",
54 | "sass": "^1.77.8",
55 | "stream-browserify": "^3.0.0",
56 | "url-loader": "^4.1.1"
57 | },
58 | "devDependencies": {
59 | "@babel/core": "^7.13.10",
60 | "@babel/preset-env": "^7.13.12",
61 | "@babel/preset-react": "^7.12.13",
62 | "@testing-library/jest-dom": "^6.4.8",
63 | "@testing-library/react": "^12.1.5",
64 | "@testing-library/user-event": "^13.5.0",
65 | "babel-jest": "^29.7.0",
66 | "babel-loader": "^8.2.2",
67 | "copy-webpack-plugin": "^12.0.2",
68 | "css-loader": "^5.2.0",
69 | "eslint": "^8.57.0",
70 | "eslint-config-prettier": "^9.1.2",
71 | "eslint-plugin-css-modules": "^2.11.0",
72 | "eslint-plugin-prettier": "^5.5.4",
73 | "eslint-plugin-react": "^7.22.0",
74 | "eslint-plugin-react-hooks": "^4.2.0",
75 | "html-webpack-plugin": "^5.3.1",
76 | "identity-obj-proxy": "^3.0.0",
77 | "jest": "^29.7.0",
78 | "jest-environment-jsdom": "^29.7.0",
79 | "nodemon": "^2.0.22",
80 | "prettier": "^3.6.2",
81 | "react-router": "^5.2.0",
82 | "sass-loader": "^11.0.1",
83 | "style-loader": "^2.0.0",
84 | "supertest": "^7.0.0",
85 | "webpack": "^5.101.2",
86 | "webpack-bundle-analyzer": "^4.10.2",
87 | "webpack-cli": "^4.10.0",
88 | "webpack-dev-server": "^3.11.3"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/scripts/optimize-assets.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Asset Optimization Script for Orbit
4 | # This script converts GIF files to optimized video formats and images to modern formats
5 |
6 | echo "🚀 Starting asset optimization for Orbit..."
7 |
8 | # Create output directories
9 | mkdir -p client/assets/optimized/videos
10 | mkdir -p client/assets/optimized/images
11 |
12 | # Convert GIF files to video formats
13 | echo "📹 Converting GIF files to video formats..."
14 |
15 | GIFS=(
16 | "client/assets/codemirror.gif"
17 | "client/assets/graphiql.gif"
18 | "client/assets/movingtables.gif"
19 | "client/assets/userdbinput.gif"
20 | )
21 |
22 | for gif in "${GIFS[@]}"; do
23 | if [ -f "$gif" ]; then
24 | filename=$(basename "$gif" .gif)
25 |
26 | echo "Converting $gif..."
27 |
28 | # Convert to WebM (best compression)
29 | ffmpeg -i "$gif" -c:v libvpx-vp9 -crf 30 -b:v 0 -an "client/assets/optimized/videos/${filename}.webm" -y
30 |
31 | # Convert to MP4 (better compatibility)
32 | ffmpeg -i "$gif" -c:v libx264 -pix_fmt yuv420p -an "client/assets/optimized/videos/${filename}.mp4" -y
33 |
34 | echo "✅ Converted $filename"
35 | else
36 | echo "⚠️ File not found: $gif"
37 | fi
38 | done
39 |
40 | # Convert PNG files to WebP
41 | echo "🖼️ Converting images to WebP format..."
42 |
43 | IMAGES=(
44 | "client/assets/new-logo.png"
45 | "client/assets/white-logo.png"
46 | "client/assets/navy-github.png"
47 | "client/assets/navy-linkedin.png"
48 | "client/assets/navy-twitter.png"
49 | "client/assets/white-github.png"
50 | "client/assets/white-linkedin.png"
51 | "client/assets/chris-headshot.png"
52 | "client/assets/john-headshot.png"
53 | "client/assets/ryan-headshot.png"
54 | "client/assets/stacy-headshot.png"
55 | )
56 |
57 | for image in "${IMAGES[@]}"; do
58 | if [ -f "$image" ]; then
59 | filename=$(basename "$image" .png)
60 |
61 | echo "Converting $image to WebP..."
62 | cwebp "$image" -o "client/assets/optimized/images/${filename}.webp" -q 80
63 |
64 | echo "✅ Converted $filename to WebP"
65 | else
66 | echo "⚠️ File not found: $image"
67 | fi
68 | done
69 |
70 | echo "✨ Asset optimization complete!"
71 | echo ""
72 | echo "📁 Optimized files are in:"
73 | echo " - client/assets/optimized/videos/ (WebM & MP4 formats)"
74 | echo " - client/assets/optimized/images/ (WebP format)"
75 | echo ""
76 | echo "📊 To use optimized assets:"
77 | echo " 1. Update import paths in components to use optimized versions"
78 | echo " 2. Add fallbacks for browser compatibility"
79 | echo " 3. Test in different browsers"
80 | echo ""
81 | echo "🔧 Required tools:"
82 | echo " - ffmpeg (for video conversion): brew install ffmpeg"
83 | echo " - cwebp (for WebP conversion): brew install webp"
84 |
--------------------------------------------------------------------------------
/server/GQLFactory/helpers/helperFunctions.js:
--------------------------------------------------------------------------------
1 | const isJunctionTable = (foreignKeys, columns) => {
2 | if (!foreignKeys) return false;
3 | return Object.keys(foreignKeys).length + 1 === Object.keys(columns).length;
4 | };
5 |
6 | const setType = (str) => {
7 | switch (str) {
8 | case 'character varying':
9 | return 'String';
10 | case 'character':
11 | return 'String';
12 | case 'integer':
13 | return 'Int';
14 | case 'timestamp':
15 | return 'String';
16 | case 'bigint':
17 | return 'String';
18 | case 'text':
19 | return 'String';
20 | case 'date':
21 | return 'String';
22 | case 'boolean':
23 | return 'Boolean';
24 | default:
25 | return 'String';
26 | }
27 | };
28 |
29 | const typeConversion = {
30 | 'character varying': 'String',
31 | character: 'String',
32 | integer: 'Int',
33 | timestamp: 'String',
34 | text: 'String',
35 | date: 'String',
36 | boolean: 'Boolean',
37 | numeric: 'Int',
38 | bigint: 'String',
39 | };
40 |
41 | module.exports = {
42 | isJunctionTable,
43 | setType,
44 | typeConversion,
45 | };
46 |
--------------------------------------------------------------------------------
/server/GQLFactory/helpers/resolverHelpers.js:
--------------------------------------------------------------------------------
1 | const toCamelCase = require('camelcase');
2 | const { singular } = require('pluralize');
3 | const { isJunctionTable } = require('./helperFunctions');
4 |
5 | const resolverHelper = {};
6 |
7 | resolverHelper.queryByPrimaryKey = (tableName, primaryKey) => {
8 | let queryName = '';
9 | if (tableName === singular(tableName)) {
10 | queryName += `${singular(tableName)}` + `ByID`;
11 | } else queryName = singular(tableName);
12 |
13 | return `
14 | ${toCamelCase(queryName)}: (parent, args) => {
15 | const query = 'SELECT * FROM ${tableName} WHERE ${primaryKey} = $1';
16 | const values = [args.${primaryKey}];
17 | return db.query(query, values)
18 | .then(data => data.rows[0])
19 | .catch(err => new Error(err));
20 | },`;
21 | };
22 |
23 | resolverHelper.queryAll = (tableName) => {
24 | return `
25 | ${toCamelCase(tableName)}: () => {
26 | const query = 'SELECT * FROM ${tableName}';
27 | return db.query(query)
28 | .then(data => data.rows)
29 | .catch(err => new Error(err));
30 | },`;
31 | };
32 |
33 | /* */
34 |
35 | resolverHelper.createMutation = (tableName, primaryKey, columns) => {
36 | const mutationName = toCamelCase('add_' + singular(tableName));
37 | const columnsArray = Object.keys(columns).filter((column) => column !== primaryKey);
38 | const columnsArgument = columnsArray.join(', ');
39 | const valuesArgument = columnsArray.map((column, i) => `$${i + 1}`).join(', ');
40 | const valuesList = columnsArray.map((column) => `args.${column}`).join(', ');
41 |
42 | return `
43 | ${mutationName}: (parent, args) => {
44 | const query = 'INSERT INTO ${tableName} (${columnsArgument}) VALUES (${valuesArgument}) RETURNING *';
45 | const values = [${valuesList}];
46 | return db.query(query, values)
47 | .then(data => data.rows[0])
48 | .catch(err => new Error(err));
49 | },`;
50 | };
51 |
52 | resolverHelper.updateMutation = (tableName, primaryKey, _columns) => {
53 | const mutationName = toCamelCase('update_' + singular(tableName));
54 | // cleaned unused variables
55 |
56 | return `
57 | ${mutationName}: (parent, args) => {
58 | let valList = [];
59 | for (const key of Object.keys(args)) {
60 | if (key !== '${primaryKey}') valList.push(args[key]);
61 | }
62 | valList.push(args.${primaryKey});
63 | const argsArray = Object.keys(args).filter((key) => key !== '${primaryKey}');
64 | let setString = argsArray.map((k, i) => k + ' = $' + (i + 1)).join(', ');
65 | const pKArg = '$' + (argsArray.length + 1);
66 | const query = 'UPDATE ${tableName} SET ' + setString + ' WHERE ${primaryKey} = ' + pKArg + ' RETURNING *';
67 | const values = valList;
68 | return db.query(query, values)
69 | .then(data => data.rows[0])
70 | .catch(err => new Error(err));
71 | },`;
72 | };
73 |
74 | resolverHelper.deleteMutation = (tableName, primaryKey) => {
75 | const mutationName = toCamelCase('delete_' + singular(tableName));
76 |
77 | return `
78 | ${mutationName}: (parent, args) => {
79 | const query = 'DELETE FROM ${tableName} WHERE ${primaryKey} = $1 RETURNING *';
80 | const values = [args.${primaryKey}];
81 | return db.query(query, values)
82 | .then(data => data.rows[0])
83 | .catch(err => new Error(err));
84 | },`;
85 | };
86 |
87 | /* */
88 |
89 | resolverHelper.identifyRelationships = (tableName, sqlSchema) => {
90 | const { primaryKey, referencedBy, foreignKeys } = sqlSchema[tableName];
91 | let resolverBody = '';
92 |
93 | /* Keeps track of custom object types already added to resolverBody string */
94 | const inResolverBody = [];
95 |
96 | /* Looping through each table that references tableName */
97 | for (const refByTable of Object.keys(referencedBy)) {
98 | /* Shorthand labels for refByTable's properties */
99 | const { foreignKeys: refFK, columns: refCols } = sqlSchema[refByTable];
100 | /* If refByTable is a Junction Table we concat its ForeignKeys refTableName to resolverBody */
101 | if (isJunctionTable(refFK, refCols)) {
102 | /* Column name on Junction Table (refByTable) referencing the current tableName */
103 | let refByTableTableNameAlias = '';
104 | for (const fk of Object.keys(refFK)) {
105 | if (refFK[fk].referenceTable === tableName) {
106 | refByTableTableNameAlias = fk;
107 | }
108 | }
109 | /* Loop through refByTable's foreignkeys */
110 | for (const refByTableFK of Object.keys(refFK)) {
111 | /* Filtering out tableName */
112 | if (refFK[refByTableFK].referenceTable !== tableName) {
113 | const refByTableFKName =
114 | refFK[refByTableFK].referenceTable; /* refByTableFKName = people */
115 | const refByTableFKKey = refFK[refByTableFK].referenceKey; /* refByTableFKKey = _id */
116 | /* Check if refByTableFKName has already been added to resolverBody string */
117 | if (!inResolverBody.includes(refByTableFKName)) {
118 | inResolverBody.push(refByTableFKName);
119 |
120 | /* Use inline comments below as example */
121 | resolverBody += resolverHelper.junctionTableRelationships(
122 | tableName, // -------------------species
123 | primaryKey, // ------------------_id
124 | refByTableTableNameAlias, // ----species_id
125 | refByTable, // ------------------species_in_films
126 | refByTableFK, // ----------------film_id
127 | refByTableFKName, // ------------films
128 | refByTableFKKey // --------------_id
129 | );
130 | }
131 | }
132 | }
133 | } else {
134 | /* Check if refByTable has already been added to resolverBody string */
135 | if (!inResolverBody.includes(refByTable)) {
136 | inResolverBody.push(refByTable);
137 | const refByKey = referencedBy[refByTable];
138 | /* referencedBy tables that are not Junction tables */
139 | resolverBody += resolverHelper.customObjectsRelationships(
140 | tableName,
141 | primaryKey,
142 | refByTable,
143 | refByKey
144 | );
145 | }
146 | }
147 | /* Creates resolvers for current tableName's foreignKeys */
148 | if (foreignKeys) {
149 | for (const fk of Object.keys(foreignKeys)) {
150 | const fkTableName = foreignKeys[fk].referenceTable;
151 | /* Check if fk has already been added to resolverBody string */
152 | if (!inResolverBody.includes(fkTableName)) {
153 | inResolverBody.push(fkTableName);
154 | const fkKey = foreignKeys[fk].referenceKey;
155 | resolverBody += resolverHelper.foreignKeyRelationships(
156 | tableName,
157 | primaryKey,
158 | fk,
159 | fkTableName,
160 | fkKey
161 | );
162 | }
163 | }
164 | }
165 | }
166 | return resolverBody;
167 | };
168 |
169 | resolverHelper.junctionTableRelationships = (
170 | tableName,
171 | primaryKey,
172 | refByTableTableNameAlias,
173 | refByTable,
174 | refByTableFK,
175 | refByTableFKName,
176 | refByTableFKKey
177 | ) => {
178 | return `
179 | ${toCamelCase(refByTableFKName)}: (${toCamelCase(tableName)}) => {
180 | const query = 'SELECT * FROM ${refByTableFKName} LEFT OUTER JOIN ${refByTable} ON ${refByTableFKName}.${refByTableFKKey} = ${refByTable}.${refByTableFK} WHERE ${refByTable}.${refByTableTableNameAlias} = $1';
181 | const values = [${tableName}.${primaryKey}];
182 | return db.query(query, values)
183 | .then(data => data.rows)
184 | .catch(err => new Error(err));
185 | }, `;
186 | };
187 |
188 | resolverHelper.customObjectsRelationships = (tableName, primaryKey, refByTable, refByKey) => {
189 | return `
190 | ${toCamelCase(refByTable)}: (${toCamelCase(tableName)}) => {
191 | const query = 'SELECT * FROM ${refByTable} WHERE ${refByKey} = $1';
192 | const values = [${toCamelCase(tableName)}.${primaryKey}];
193 | return db.query(query, values)
194 | .then(data => data.rows)
195 | .catch(err => new Error(err));
196 | },`;
197 | };
198 |
199 | resolverHelper.foreignKeyRelationships = (tableName, primaryKey, fk, fkTableName, fkKey) => {
200 | return `
201 | ${toCamelCase(fkTableName)}: (${toCamelCase(tableName)}) => {
202 | const query = 'SELECT ${fkTableName}.* FROM ${fkTableName} LEFT OUTER JOIN ${tableName} ON ${fkTableName}.${fkKey} = ${tableName}.${fk} WHERE ${tableName}.${primaryKey} = $1';
203 | const values = [${toCamelCase(tableName)}.${primaryKey}];
204 | return db.query(query, values)
205 | .then(data => data.rows)
206 | .catch(err => new Error(err));
207 | }, `;
208 | };
209 |
210 | module.exports = resolverHelper;
211 |
--------------------------------------------------------------------------------
/server/GQLFactory/helpers/typeHelpers.js:
--------------------------------------------------------------------------------
1 | const toCamelCase = require('camelcase');
2 | const { singular } = require('pluralize');
3 | const { pascalCase } = require('pascal-case');
4 | const { typeConversion, isJunctionTable } = require('./helperFunctions');
5 |
6 | /* Functions facilitating creation of mutation types */
7 | const mutationsHelper = {};
8 |
9 | mutationsHelper.create = (tableName, primaryKey, foreignKeys, columns) => {
10 | return `\n ${toCamelCase(`add_${singular(tableName)}`)}(\n${mutationsHelper.mutationFields(
11 | primaryKey,
12 | foreignKeys,
13 | columns,
14 | false
15 | )}): ${pascalCase(singular(tableName))}!\n`;
16 | };
17 |
18 | mutationsHelper.delete = (tableName, primaryKey) => {
19 | return `\n ${toCamelCase(
20 | `delete_${singular(tableName)}`
21 | )}(${primaryKey}: ID!): ${pascalCase(singular(tableName))}!\n`;
22 | };
23 |
24 | mutationsHelper.update = (tableName, primaryKey, foreignKeys, columns) => {
25 | return `\n ${toCamelCase(`update_${singular(tableName)}`)}(\n${mutationsHelper.mutationFields(
26 | primaryKey,
27 | foreignKeys,
28 | columns,
29 | true
30 | )}): ${pascalCase(singular(tableName))}!\n`;
31 | };
32 |
33 | mutationsHelper.mutationFields = (primaryKey, foreignKeys, columns, primaryKeyRequired) => {
34 | let mutationFields = '';
35 | for (const fieldName of Object.keys(columns)) {
36 | const { dataType, isNullable } = columns[fieldName];
37 | // primaryKeyRequired is used to check whether the primary key is needed for the mutation
38 | // create mutations do not need primary key as the ID is usually automatically generated by the database
39 | if (!primaryKeyRequired && fieldName === primaryKey) {
40 | continue;
41 | }
42 | // update mutations need the primary key to update the specific field
43 | // primaryKey fields are ID scalar types
44 | if (primaryKeyRequired && fieldName === primaryKey) {
45 | mutationFields += ` ${fieldName}: ID!,\n`;
46 | // foreignKey field types are ID scalar types
47 | } else if (foreignKeys && foreignKeys[fieldName]) {
48 | mutationFields += ` ${fieldName}: ID`;
49 | // if the field is not nullable and for a create mutation type, ! operator is added to the response type
50 | if (isNullable === 'NO' && !primaryKeyRequired) mutationFields += '!';
51 | mutationFields += ',\n';
52 | } else {
53 | mutationFields += ` ${fieldName}: ${
54 | typeConversion[dataType] ? typeConversion[dataType] : 'Int'
55 | }`;
56 | // if the field is not nullable and for a create mutation type, ! operator is added to the response type
57 | if (isNullable === 'NO' && !primaryKeyRequired) mutationFields += '!';
58 | mutationFields += ',\n';
59 | }
60 | }
61 | if (mutationFields !== '') mutationFields += ' ';
62 | return mutationFields;
63 | };
64 |
65 | /* Functions facilitating creation of custom types */
66 | const customHelper = {};
67 | /* Loops through SQL columns getting their name as fieldName, type, and isNullable to be returned as fields */
68 | customHelper.getFields = (primaryKey, foreignKeys, columns) => {
69 | let fields = ``;
70 | for (const fieldName of Object.keys(columns)) {
71 | // check if current column is neither a foreign key or a primary key
72 | if ((!foreignKeys || !foreignKeys[fieldName]) && fieldName !== primaryKey) {
73 | const { dataType, isNullable } = columns[fieldName];
74 | fields += `\n ${fieldName}: ${typeConversion[dataType]}`;
75 | if (isNullable === 'NO') fields += `!`;
76 | }
77 | }
78 | return fields;
79 | };
80 |
81 | customHelper.getRelationships = (tableName, sqlSchema) => {
82 | let relationshipFields = ``;
83 | const inRelationshipString = []; // used to check if field is already added
84 | const tableData = sqlSchema[tableName];
85 | const { foreignKeys, referencedBy } = tableData;
86 | // tableName's foreign keys : adds each foreign key as fields to custom object type
87 | if (foreignKeys) {
88 | for (const fk of Object.keys(foreignKeys)) {
89 | if (!inRelationshipString.includes(foreignKeys[fk].referenceTable)) {
90 | inRelationshipString.push(foreignKeys[fk].referenceTable);
91 | relationshipFields += `\n ${toCamelCase(
92 | foreignKeys[fk].referenceTable
93 | )}: [${pascalCase(singular(foreignKeys[fk].referenceTable))}]`;
94 | }
95 | }
96 | }
97 | if (referencedBy) {
98 | for (const refTableName of Object.keys(referencedBy)) {
99 | // if the referencedby tableName is a junction table, add all of the junction table's foreign keys to
100 | // the current custom object type's field (excluding its own)
101 | if (isJunctionTable(sqlSchema[refTableName].foreignKeys, sqlSchema[refTableName].columns)) {
102 | const { foreignKeys } = sqlSchema[refTableName];
103 | for (const foreignFK of Object.keys(foreignKeys)) {
104 | if (foreignKeys[foreignFK].referenceTable !== tableName) {
105 | if (!inRelationshipString.includes(foreignKeys[foreignFK].referenceTable)) {
106 | inRelationshipString.push(foreignKeys[foreignFK].referenceTable);
107 | relationshipFields += `\n ${toCamelCase(
108 | foreignKeys[foreignFK].referenceTable
109 | )}: [${pascalCase(singular(foreignKeys[foreignFK].referenceTable))}]`;
110 | }
111 | }
112 | }
113 | // if referencedBy tableName is not a junction table,
114 | // only add the referencedBy tableName if not already added
115 | } else {
116 | if (!inRelationshipString.includes(refTableName)) {
117 | inRelationshipString.push(refTableName);
118 | relationshipFields += `\n ${toCamelCase(
119 | refTableName
120 | )}: [${pascalCase(singular(refTableName))}]`;
121 | }
122 | }
123 | }
124 | }
125 | return relationshipFields;
126 | };
127 |
128 | module.exports = {
129 | mutationsHelper,
130 | customHelper,
131 | };
132 |
--------------------------------------------------------------------------------
/server/GQLFactory/resolverFactory.js:
--------------------------------------------------------------------------------
1 | const { pascalCase } = require('pascal-case');
2 | const { singular } = require('pluralize');
3 | const resolverHelper = require('./helpers/resolverHelpers');
4 | const resolverFactory = {};
5 |
6 | resolverFactory.collectQueries = (tableName, tableData) => {
7 | const { primaryKey } = tableData;
8 | const queryByPK = resolverHelper.queryByPrimaryKey(tableName, primaryKey);
9 | const queryAll = resolverHelper.queryAll(tableName);
10 | return `\n${queryByPK}\n${queryAll}`;
11 | };
12 | /* -------------------------------- */
13 | resolverFactory.collectMutations = (tableName, tableData) => {
14 | const { primaryKey, columns } = tableData;
15 | const createMutation = resolverHelper.createMutation(tableName, primaryKey, columns);
16 | const updateMutation = resolverHelper.updateMutation(tableName, primaryKey, columns);
17 | const deleteMutation = resolverHelper.deleteMutation(tableName, primaryKey);
18 | return `${createMutation}\n${updateMutation}\n${deleteMutation}\n`;
19 | };
20 | /* ------------------------------------ */
21 | resolverFactory.collectCustomObjectRelationships = (tableName, sqlSchema) => {
22 | if (!sqlSchema[tableName].referencedBy) return '';
23 | const resolverName = pascalCase(singular(tableName));
24 | const resolverBody = resolverHelper.identifyRelationships(tableName, sqlSchema);
25 |
26 | return `
27 | ${resolverName}: {
28 | ${resolverBody}
29 | }, \n`;
30 | };
31 |
32 | module.exports = resolverFactory;
33 |
--------------------------------------------------------------------------------
/server/GQLFactory/schemaFactory.js:
--------------------------------------------------------------------------------
1 | const { queries, mutations, customObjects } = require('./typeFactory');
2 | const {
3 | collectQueries,
4 | collectMutations,
5 | collectCustomObjectRelationships,
6 | } = require('./resolverFactory');
7 | const { isJunctionTable } = require('./helpers/helperFunctions');
8 | /* High level functions tasked with assembling the Types and the Resolvers */
9 | const schemaFactory = {};
10 | /* Creates query, mutation, and custom Object Types */
11 | schemaFactory.createTypes = (sqlSchema) => {
12 | let queryType = '';
13 | let mutationType = '';
14 | let customObjectType = '';
15 |
16 | for (const tableName of Object.keys(sqlSchema)) {
17 | const tableData = sqlSchema[tableName];
18 | const { foreignKeys, columns } = tableData;
19 | if (!isJunctionTable(foreignKeys, columns)) {
20 | queryType += queries(tableName, tableData);
21 | mutationType += mutations(tableName, tableData);
22 | customObjectType += customObjects(tableName, sqlSchema);
23 | }
24 | }
25 |
26 | const types =
27 | `${'const typeDefs = `\n' + ' type Query {\n'}${queryType} }\n\n` +
28 | ` type Mutation {${mutationType} }\n\n` +
29 | `${customObjectType}\`;\n\n`;
30 |
31 | return types;
32 | };
33 |
34 | schemaFactory.createResolvers = (sqlSchema) => {
35 | let queryResolvers = '';
36 | let mutationResolvers = '';
37 | let customObjectTypeResolvers = '';
38 |
39 | for (const tableName of Object.keys(sqlSchema)) {
40 | const tableData = sqlSchema[tableName];
41 | const { foreignKeys, columns } = tableData;
42 | if (!isJunctionTable(foreignKeys, columns)) {
43 | queryResolvers += collectQueries(tableName, tableData);
44 | mutationResolvers += collectMutations(tableName, tableData);
45 | customObjectTypeResolvers += collectCustomObjectRelationships(tableName, sqlSchema);
46 | }
47 | }
48 |
49 | const resolvers =
50 | '\nconst resolvers = {\n' +
51 | ' Query: {' +
52 | ` ${queryResolvers}\n` +
53 | ' },\n\n' +
54 | ' Mutation: {\n' +
55 | ` ${mutationResolvers}\n` +
56 | ' },\n' +
57 | ` ${customObjectTypeResolvers}\n }\n`;
58 |
59 | return resolvers;
60 | };
61 |
62 | module.exports = schemaFactory;
63 |
--------------------------------------------------------------------------------
/server/GQLFactory/typeFactory.js:
--------------------------------------------------------------------------------
1 | const { mutationsHelper, customHelper } = require('./helpers/typeHelpers');
2 | const { pascalCase } = require('pascal-case');
3 | const toCamelCase = require('camelcase');
4 | const { singular } = require('pluralize');
5 |
6 | const typeFactory = {};
7 |
8 | typeFactory.queries = (tableName, tableData) => {
9 | const { primaryKey } = tableData;
10 | const singularName = singular(tableName);
11 | let tableByID = toCamelCase(singularName);
12 | if (singularName === tableName) tableByID += 'ById';
13 | return (
14 | ` ${toCamelCase(tableName)}: [${pascalCase(singularName)}!]!\n` +
15 | ` ${tableByID}(${primaryKey}: ID!): ${pascalCase(singularName)}!\n`
16 | );
17 | };
18 |
19 | typeFactory.mutations = (tableName, tableData) => {
20 | const { primaryKey, foreignKeys, columns } = tableData;
21 |
22 | return (
23 | mutationsHelper.create(tableName, primaryKey, foreignKeys, columns) +
24 | mutationsHelper.update(tableName, primaryKey, foreignKeys, columns) +
25 | mutationsHelper.delete(tableName, primaryKey)
26 | );
27 | };
28 |
29 | typeFactory.customObjects = (tableName, sqlSchema) => {
30 | const tableData = sqlSchema[tableName];
31 | const { primaryKey, foreignKeys, columns } = tableData;
32 | const pkType = 'ID';
33 |
34 | return `${
35 | `type ${pascalCase(singular(tableName))} {\n` + ` ${primaryKey}: ${pkType}!`
36 | }${customHelper.getFields(
37 | primaryKey,
38 | foreignKeys,
39 | columns
40 | )}${customHelper.getRelationships(tableName, sqlSchema)}\n}\n\n`;
41 | };
42 |
43 | module.exports = typeFactory;
44 |
--------------------------------------------------------------------------------
/server/controllers/GQLController.js:
--------------------------------------------------------------------------------
1 | const { createTypes, createResolvers } = require('../GQLFactory/schemaFactory');
2 |
3 | const GQLController = {};
4 |
5 | GQLController.createGQLSchema = (req, res, next) => {
6 | const { SQLSchema } = res.locals;
7 |
8 | console.log('GQLController: Creating schema from SQL data');
9 | console.log('SQLSchema type:', typeof SQLSchema);
10 | console.log('SQLSchema keys:', Object.keys(SQLSchema || {}));
11 | console.log('SQLSchema structure:', JSON.stringify(SQLSchema, null, 2));
12 |
13 | try {
14 | const types = createTypes(SQLSchema);
15 | const resolvers = createResolvers(SQLSchema);
16 | res.locals.GQLSchema = { types, resolvers };
17 | console.log('GQLController: Schema created successfully');
18 | return next();
19 | } catch (err) {
20 | console.error('GQLController error:', err);
21 | console.error('GQLController error stack:', err.stack);
22 | const errObject = {
23 | log: `Error in createGQLSchema: ${err}`,
24 | status: 400,
25 | message: {
26 | err: 'Unable to create GQL schema',
27 | },
28 | };
29 |
30 | return next(errObject);
31 | }
32 | };
33 |
34 | module.exports = GQLController;
35 |
--------------------------------------------------------------------------------
/server/controllers/SQLController.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { Pool } = require('pg');
4 | const CryptoJS = require('crypto-js');
5 |
6 | const secretKey = require('../secretKey');
7 | /* Example db URI */
8 | const EX_PG_URI =
9 | 'postgres://zhocexop:Ipv9EKas6bU6z9ehDXZQRorjITIXijGv@ziggy.db.elephantsql.com:5432/zhocexop';
10 |
11 | // Mock database data for testing when the real database is unavailable
12 | const MOCK_DB_DATA = {
13 | users: {
14 | primaryKey: 'id',
15 | foreignKeys: [],
16 | referencedBy: {
17 | posts: 'user_id',
18 | comments: 'user_id',
19 | profiles: 'user_id',
20 | },
21 | columns: {
22 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
23 | name: {
24 | dataType: 'character varying',
25 | columnDefault: null,
26 | charMaxLength: 255,
27 | isNullable: 'YES',
28 | },
29 | email: {
30 | dataType: 'character varying',
31 | columnDefault: null,
32 | charMaxLength: 255,
33 | isNullable: 'YES',
34 | },
35 | created_at: {
36 | dataType: 'timestamp',
37 | columnDefault: null,
38 | charMaxLength: null,
39 | isNullable: 'YES',
40 | },
41 | },
42 | },
43 | posts: {
44 | primaryKey: 'id',
45 | foreignKeys: {
46 | user_id: { referenceTable: 'users', referenceKey: 'id' },
47 | category_id: { referenceTable: 'categories', referenceKey: 'id' },
48 | },
49 | referencedBy: {
50 | comments: 'post_id',
51 | tags_posts: 'post_id',
52 | },
53 | columns: {
54 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
55 | title: {
56 | dataType: 'character varying',
57 | columnDefault: null,
58 | charMaxLength: 255,
59 | isNullable: 'YES',
60 | },
61 | content: { dataType: 'text', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
62 | user_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
63 | category_id: {
64 | dataType: 'integer',
65 | columnDefault: null,
66 | charMaxLength: null,
67 | isNullable: 'YES',
68 | },
69 | published: {
70 | dataType: 'boolean',
71 | columnDefault: null,
72 | charMaxLength: null,
73 | isNullable: 'YES',
74 | },
75 | created_at: {
76 | dataType: 'timestamp',
77 | columnDefault: null,
78 | charMaxLength: null,
79 | isNullable: 'YES',
80 | },
81 | },
82 | },
83 | categories: {
84 | primaryKey: 'id',
85 | foreignKeys: [],
86 | referencedBy: {
87 | posts: 'category_id',
88 | },
89 | columns: {
90 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
91 | name: {
92 | dataType: 'character varying',
93 | columnDefault: null,
94 | charMaxLength: 100,
95 | isNullable: 'YES',
96 | },
97 | description: {
98 | dataType: 'text',
99 | columnDefault: null,
100 | charMaxLength: null,
101 | isNullable: 'YES',
102 | },
103 | },
104 | },
105 | comments: {
106 | primaryKey: 'id',
107 | foreignKeys: {
108 | user_id: { referenceTable: 'users', referenceKey: 'id' },
109 | post_id: { referenceTable: 'posts', referenceKey: 'id' },
110 | },
111 | referencedBy: {},
112 | columns: {
113 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
114 | content: { dataType: 'text', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
115 | user_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
116 | post_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
117 | created_at: {
118 | dataType: 'timestamp',
119 | columnDefault: null,
120 | charMaxLength: null,
121 | isNullable: 'YES',
122 | },
123 | },
124 | },
125 | profiles: {
126 | primaryKey: 'id',
127 | foreignKeys: {
128 | user_id: { referenceTable: 'users', referenceKey: 'id' },
129 | },
130 | referencedBy: {},
131 | columns: {
132 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
133 | bio: { dataType: 'text', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
134 | avatar_url: {
135 | dataType: 'character varying',
136 | columnDefault: null,
137 | charMaxLength: 500,
138 | isNullable: 'YES',
139 | },
140 | user_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
141 | },
142 | },
143 | tags: {
144 | primaryKey: 'id',
145 | foreignKeys: [],
146 | referencedBy: {
147 | tags_posts: 'tag_id',
148 | },
149 | columns: {
150 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
151 | name: {
152 | dataType: 'character varying',
153 | columnDefault: null,
154 | charMaxLength: 50,
155 | isNullable: 'YES',
156 | },
157 | color: {
158 | dataType: 'character varying',
159 | columnDefault: null,
160 | charMaxLength: 7,
161 | isNullable: 'YES',
162 | },
163 | },
164 | },
165 | tags_posts: {
166 | primaryKey: 'id',
167 | foreignKeys: {
168 | tag_id: { referenceTable: 'tags', referenceKey: 'id' },
169 | post_id: { referenceTable: 'posts', referenceKey: 'id' },
170 | },
171 | referencedBy: {},
172 | columns: {
173 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
174 | tag_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
175 | post_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
176 | },
177 | },
178 | likes: {
179 | primaryKey: 'id',
180 | foreignKeys: {
181 | user_id: { referenceTable: 'users', referenceKey: 'id' },
182 | post_id: { referenceTable: 'posts', referenceKey: 'id' },
183 | },
184 | referencedBy: {},
185 | columns: {
186 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
187 | user_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
188 | post_id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
189 | created_at: {
190 | dataType: 'timestamp',
191 | columnDefault: null,
192 | charMaxLength: null,
193 | isNullable: 'YES',
194 | },
195 | },
196 | },
197 | };
198 |
199 | const sqlFilePath = path.resolve(__dirname, '../tableQuery.sql');
200 | console.log('Attempting to read SQL file from:', sqlFilePath);
201 |
202 | const sqlQuery = fs.readFileSync(sqlFilePath, 'utf8');
203 |
204 | const SQLController = {};
205 |
206 | // decrypt incoming PSQL URLs safely; return null on failure
207 | const decryptedURI = (encryptedURL) => {
208 | try {
209 | const bytes = CryptoJS.AES.decrypt(encryptedURL, secretKey);
210 | const decrypted = bytes.toString(CryptoJS.enc.Utf8);
211 | if (!decrypted) throw new Error('Empty decrypt');
212 | return decrypted;
213 | } catch (_err) {
214 | return null;
215 | }
216 | };
217 |
218 | SQLController.getSQLSchema = (req, res, next) => {
219 | let PSQL_URI;
220 |
221 | // if user sent URI, try to decrypt; fall back to example URI on failure
222 | if (req.body.link) {
223 | const maybeURI = decryptedURI(req.body.link);
224 | if (maybeURI) {
225 | PSQL_URI = maybeURI;
226 | } else {
227 | console.log('Invalid encrypted link provided; falling back to example URI');
228 | PSQL_URI = EX_PG_URI;
229 | }
230 | } else {
231 | PSQL_URI = EX_PG_URI;
232 | }
233 |
234 | console.log('Connecting to database...');
235 | console.log('Using URI:', PSQL_URI.substring(0, 20) + '...');
236 |
237 | const db = new Pool({ connectionString: PSQL_URI });
238 |
239 | // Test the connection first
240 | db.query('SELECT NOW()')
241 | .then(() => {
242 | console.log('Database connection successful');
243 | return db.query(sqlQuery);
244 | })
245 | .then((data) => {
246 | console.log('SQL query executed successfully');
247 | console.log('Raw data structure:', typeof data.rows[0].tables);
248 | res.locals.SQLSchema = data.rows[0].tables;
249 | return next();
250 | })
251 | .catch((err) => {
252 | console.error('Database connection error:', err);
253 | console.log('Falling back to mock data...');
254 |
255 | // Use mock data as fallback
256 | res.locals.SQLSchema = MOCK_DB_DATA;
257 | console.log('Mock data set:', res.locals.SQLSchema);
258 | return next();
259 | })
260 | .finally(() => {
261 | db.end();
262 | });
263 | };
264 |
265 | /* Format the SQL Schema for visualizer */
266 | SQLController.formatGraphData = (req, res, next) => {
267 | try {
268 | console.log('formatGraphData: Starting to format data');
269 | const sqlSchema = res.locals.SQLSchema;
270 | console.log('formatGraphData: SQLSchema received:', typeof sqlSchema);
271 |
272 | let graphData = [];
273 |
274 | for (const tableName of Object.keys(sqlSchema)) {
275 | console.log('formatGraphData: Processing table:', tableName);
276 | const tableObject = {};
277 | tableObject[tableName] = sqlSchema[tableName];
278 | if (sqlSchema[tableName].foreignKeys) {
279 | const foreignKeysArray = [];
280 | for (const fk of Object.keys(sqlSchema[tableName].foreignKeys)) {
281 | const foreignKeyObject = {};
282 | foreignKeyObject[fk] = sqlSchema[tableName].foreignKeys[fk];
283 | foreignKeysArray.push(foreignKeyObject);
284 | }
285 | sqlSchema[tableName].foreignKeys = foreignKeysArray;
286 | }
287 |
288 | if (sqlSchema[tableName].referencedBy) {
289 | const referencedByArray = [];
290 | for (const refBy of Object.keys(sqlSchema[tableName].referencedBy)) {
291 | const referencedByObject = {};
292 | referencedByObject[refBy] = sqlSchema[tableName].referencedBy[refBy];
293 | referencedByArray.push(referencedByObject);
294 | }
295 | sqlSchema[tableName].referencedBy = referencedByArray;
296 | }
297 |
298 | if (sqlSchema[tableName].columns) {
299 | const columnsArray = [];
300 | for (const columnName of Object.keys(sqlSchema[tableName].columns)) {
301 | const columnsObject = {};
302 | columnsObject[columnName] = sqlSchema[tableName].columns[columnName];
303 | columnsArray.push(columnsObject);
304 | }
305 | sqlSchema[tableName].columns = columnsArray;
306 | }
307 |
308 | graphData.push(tableObject);
309 | }
310 |
311 | console.log('formatGraphData: Final graph data length:', graphData.length);
312 | res.locals.SQLSchema = graphData;
313 | return next();
314 | } catch (err) {
315 | console.error('formatGraphData error:', err);
316 | const errObject = {
317 | log: `Error in formatGraphData: ${err}`,
318 | status: 400,
319 | message: { err: `Format graph data failed` },
320 | };
321 | return next(errObject);
322 | }
323 | };
324 |
325 | module.exports = SQLController;
326 |
--------------------------------------------------------------------------------
/server/router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const { graphqlHTTP } = require('express-graphql');
4 |
5 | const { getSQLSchema, formatGraphData } = require('./controllers/SQLController');
6 | const { createGQLSchema } = require('./controllers/GQLController');
7 | const schema = require('./schema');
8 |
9 | /* Test endpoint */
10 | router.get('/test', (req, res) => {
11 | res.status(200).json({ message: 'Server is working!' });
12 | });
13 |
14 | /* Test mock data endpoint */
15 | router.get('/test-mock', (req, res) => {
16 | const MOCK_DB_DATA = {
17 | users: {
18 | primaryKey: 'id',
19 | foreignKeys: [],
20 | referencedBy: {
21 | posts: 'user_id',
22 | },
23 | columns: {
24 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
25 | name: {
26 | dataType: 'character varying',
27 | columnDefault: null,
28 | charMaxLength: 255,
29 | isNullable: 'YES',
30 | },
31 | email: {
32 | dataType: 'character varying',
33 | columnDefault: null,
34 | charMaxLength: 255,
35 | isNullable: 'YES',
36 | },
37 | },
38 | },
39 | posts: {
40 | primaryKey: 'id',
41 | foreignKeys: {
42 | user_id: { referenceTable: 'users', referenceKey: 'id' },
43 | },
44 | referencedBy: {},
45 | columns: {
46 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
47 | title: {
48 | dataType: 'character varying',
49 | columnDefault: null,
50 | charMaxLength: 255,
51 | isNullable: 'YES',
52 | },
53 | content: { dataType: 'text', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
54 | user_id: {
55 | dataType: 'integer',
56 | columnDefault: null,
57 | charMaxLength: null,
58 | isNullable: 'YES',
59 | },
60 | },
61 | },
62 | };
63 |
64 | res.status(200).json(MOCK_DB_DATA);
65 | });
66 |
67 | /* Test GQLFactory endpoint */
68 | router.get('/test-gql', (req, res) => {
69 | const { createTypes, createResolvers } = require('./GQLFactory/schemaFactory');
70 | const MOCK_DB_DATA = {
71 | users: {
72 | primaryKey: 'id',
73 | foreignKeys: [],
74 | referencedBy: {
75 | posts: 'user_id',
76 | },
77 | columns: {
78 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
79 | name: {
80 | dataType: 'character varying',
81 | columnDefault: null,
82 | charMaxLength: 255,
83 | isNullable: 'YES',
84 | },
85 | email: {
86 | dataType: 'character varying',
87 | columnDefault: null,
88 | charMaxLength: 255,
89 | isNullable: 'YES',
90 | },
91 | },
92 | },
93 | posts: {
94 | primaryKey: 'id',
95 | foreignKeys: {
96 | user_id: { referenceTable: 'users', referenceKey: 'id' },
97 | },
98 | referencedBy: {},
99 | columns: {
100 | id: { dataType: 'integer', columnDefault: null, charMaxLength: null, isNullable: 'NO' },
101 | title: {
102 | dataType: 'character varying',
103 | columnDefault: null,
104 | charMaxLength: 255,
105 | isNullable: 'YES',
106 | },
107 | content: { dataType: 'text', columnDefault: null, charMaxLength: null, isNullable: 'YES' },
108 | user_id: {
109 | dataType: 'integer',
110 | columnDefault: null,
111 | charMaxLength: null,
112 | isNullable: 'YES',
113 | },
114 | },
115 | },
116 | };
117 |
118 | try {
119 | const types = createTypes(MOCK_DB_DATA);
120 | const resolvers = createResolvers(MOCK_DB_DATA);
121 | res.status(200).json({
122 | success: true,
123 | types: types.substring(0, 200) + '...',
124 | resolvers: resolvers.substring(0, 200) + '...',
125 | });
126 | } catch (err) {
127 | res.status(500).json({
128 | success: false,
129 | error: err.message,
130 | stack: err.stack,
131 | });
132 | }
133 | });
134 |
135 | /* Route for example SQL Schema and example GQL Schema */
136 | router.get(
137 | '/example-schema',
138 | getSQLSchema,
139 | (req, res, next) => {
140 | // If we're using mock data, skip formatGraphData since it's already in the correct format
141 | if (
142 | res.locals.SQLSchema &&
143 | typeof res.locals.SQLSchema === 'object' &&
144 | !Array.isArray(res.locals.SQLSchema)
145 | ) {
146 | return next();
147 | }
148 | return formatGraphData(req, res, next);
149 | },
150 | createGQLSchema,
151 | (req, res) => {
152 | res.status(200).json(res.locals);
153 | }
154 | );
155 |
156 | /* Route for example SQL Schema */
157 | router.get('/example-schema-json', getSQLSchema, (req, res) => {
158 | res.status(200).json(res.locals.SQLSchema);
159 | });
160 |
161 | /* Route to get user db schema */
162 | router.post('/sql-schema', getSQLSchema, createGQLSchema, formatGraphData, (req, res) => {
163 | res.status(200).json(res.locals);
164 | });
165 |
166 | router.use(
167 | '/playground',
168 | graphqlHTTP({
169 | schema,
170 | graphiql: true,
171 | })
172 | );
173 |
174 | module.exports = router;
175 |
--------------------------------------------------------------------------------
/server/schema.js:
--------------------------------------------------------------------------------
1 | const { makeExecutableSchema } = require('graphql-tools');
2 | const { Pool } = require('pg');
3 | const PG_URI =
4 | 'postgres://zhocexop:Ipv9EKas6bU6z9ehDXZQRorjITIXijGv@ziggy.db.elephantsql.com:5432/zhocexop';
5 |
6 | const pool = new Pool({
7 | connectionString: PG_URI,
8 | });
9 |
10 | const db = {};
11 | db.query = (text, params, callback) => {
12 | console.log('Executed query:', text);
13 | return pool.query(text, params, callback);
14 | };
15 |
16 | const typeDefs = `
17 | type Query {
18 | people: [Person!]!
19 | person(_id: ID!): Person!
20 | films: [Film!]!
21 | film(_id: ID!): Film!
22 | planets: [Planet!]!
23 | planet(_id: ID!): Planet!
24 | species: [Species!]!
25 | speciesById(_id: ID!): Species!
26 | vessels: [Vessel!]!
27 | vessel(_id: ID!): Vessel!
28 | starshipSpecs: [StarshipSpec!]!
29 | starshipSpec(_id: ID!): StarshipSpec!
30 | }
31 |
32 | type Mutation {
33 | addPerson(
34 | gender: String,
35 | species_id: ID,
36 | homeworld_id: ID,
37 | height: Int,
38 | mass: String,
39 | hair_color: String,
40 | skin_color: String,
41 | eye_color: String,
42 | name: String!,
43 | birth_year: String,
44 | ): Person!
45 |
46 | updatePerson(
47 | gender: String,
48 | species_id: ID,
49 | homeworld_id: ID,
50 | height: Int,
51 | _id: ID!,
52 | mass: String,
53 | hair_color: String,
54 | skin_color: String,
55 | eye_color: String,
56 | name: String,
57 | birth_year: String,
58 | ): Person!
59 |
60 | addFilm(
61 | director: String!,
62 | opening_crawl: String!,
63 | episode_id: Int!,
64 | title: String!,
65 | release_date: String!,
66 | producer: String!,
67 | ): Film!
68 |
69 | updateFilm(
70 | director: String,
71 | opening_crawl: String,
72 | episode_id: Int,
73 | _id: ID!,
74 | title: String,
75 | release_date: String,
76 | producer: String,
77 | ): Film!
78 |
79 |
80 | addPlanet(
81 | orbital_period: Int,
82 | climate: String,
83 | gravity: String,
84 | terrain: String,
85 | surface_water: String,
86 | population: String,
87 | name: String,
88 | rotation_period: Int,
89 | diameter: Int,
90 | ): Planet!
91 |
92 | updatePlanet(
93 | orbital_period: Int,
94 | climate: String,
95 | gravity: String,
96 | terrain: String,
97 | surface_water: String,
98 | population: String,
99 | _id: ID!,
100 | name: String,
101 | rotation_period: Int,
102 | diameter: Int,
103 | ): Planet!
104 |
105 |
106 | addSpecies(
107 | hair_colors: String,
108 | name: String!,
109 | classification: String,
110 | average_height: String,
111 | average_lifespan: String,
112 | skin_colors: String,
113 | eye_colors: String,
114 | language: String,
115 | homeworld_id: ID,
116 | ): Species!
117 |
118 | updateSpecies(
119 | hair_colors: String,
120 | name: String,
121 | classification: String,
122 | average_height: String,
123 | average_lifespan: String,
124 | skin_colors: String,
125 | eye_colors: String,
126 | language: String,
127 | homeworld_id: ID,
128 | _id: ID!,
129 | ): Species!
130 |
131 |
132 | addVessel(
133 | cost_in_credits: String,
134 | length: String,
135 | vessel_type: String!,
136 | model: String,
137 | manufacturer: String,
138 | name: String!,
139 | vessel_class: String!,
140 | max_atmosphering_speed: String,
141 | crew: Int,
142 | passengers: Int,
143 | cargo_capacity: String,
144 | consumables: String,
145 | ): Vessel!
146 |
147 | updateVessel(
148 | cost_in_credits: String,
149 | length: String,
150 | vessel_type: String,
151 | model: String,
152 | manufacturer: String,
153 | name: String,
154 | vessel_class: String,
155 | max_atmosphering_speed: String,
156 | crew: Int,
157 | passengers: Int,
158 | cargo_capacity: String,
159 | consumables: String,
160 | _id: ID!,
161 | ): Vessel!
162 |
163 |
164 | addStarshipSpec(
165 | vessel_id: ID!,
166 | MGLT: String,
167 | hyperdrive_rating: String,
168 | ): StarshipSpec!
169 |
170 | updateStarshipSpec(
171 | _id: ID!,
172 | vessel_id: ID,
173 | MGLT: String,
174 | hyperdrive_rating: String,
175 | ): StarshipSpec!
176 |
177 | }
178 |
179 | type Person {
180 | _id: ID!
181 | gender: String
182 | height: Int
183 | mass: String
184 | hair_color: String
185 | skin_color: String
186 | eye_color: String
187 | name: String!
188 | birth_year: String
189 | species: [Species]
190 | planets: [Planet]
191 | films: [Film]
192 | vessels: [Vessel]
193 | }
194 |
195 | type Film {
196 | _id: ID!
197 | director: String!
198 | opening_crawl: String!
199 | episode_id: Int!
200 | title: String!
201 | release_date: String!
202 | producer: String!
203 | planets: [Planet]
204 | people: [Person]
205 | vessels: [Vessel]
206 | species: [Species]
207 | }
208 |
209 | type Planet {
210 | _id: ID!
211 | orbital_period: Int
212 | climate: String
213 | gravity: String
214 | terrain: String
215 | surface_water: String
216 | population: String
217 | name: String
218 | rotation_period: Int
219 | diameter: Int
220 | films: [Film]
221 | species: [Species]
222 | people: [Person]
223 | }
224 |
225 | type Species {
226 | _id: ID!
227 | hair_colors: String
228 | name: String!
229 | classification: String
230 | average_height: String
231 | average_lifespan: String
232 | skin_colors: String
233 | eye_colors: String
234 | language: String
235 | planets: [Planet]
236 | people: [Person]
237 | films: [Film]
238 | }
239 |
240 | type Vessel {
241 | _id: ID!
242 | cost_in_credits: String
243 | length: String
244 | vessel_type: String!
245 | model: String
246 | manufacturer: String
247 | name: String!
248 | vessel_class: String!
249 | max_atmosphering_speed: String
250 | crew: Int
251 | passengers: Int
252 | cargo_capacity: String
253 | consumables: String
254 | films: [Film]
255 | people: [Person]
256 | starshipSpecs: [StarshipSpec]
257 | }
258 |
259 | type StarshipSpec {
260 | _id: ID!
261 | MGLT: String
262 | hyperdrive_rating: String
263 | vessels: [Vessel]
264 | }
265 |
266 | `;
267 |
268 | const resolvers = {
269 | Query: {
270 | person: (parent, args) => {
271 | const query = 'SELECT * FROM people WHERE _id = $1';
272 | const values = [args._id];
273 | return db
274 | .query(query, values)
275 | .then((data) => data.rows[0])
276 | .catch((err) => new Error(err));
277 | },
278 |
279 | people: () => {
280 | const query = 'SELECT * FROM people';
281 | return db
282 | .query(query)
283 | .then((data) => data.rows)
284 | .catch((err) => new Error(err));
285 | },
286 |
287 | film: (parent, args) => {
288 | const query = 'SELECT * FROM films WHERE _id = $1';
289 | const values = [args._id];
290 | return db
291 | .query(query, values)
292 | .then((data) => data.rows[0])
293 | .catch((err) => new Error(err));
294 | },
295 |
296 | films: () => {
297 | const query = 'SELECT * FROM films';
298 | return db
299 | .query(query)
300 | .then((data) => data.rows)
301 | .catch((err) => new Error(err));
302 | },
303 |
304 | planet: (parent, args) => {
305 | const query = 'SELECT * FROM planets WHERE _id = $1';
306 | const values = [args._id];
307 | return db
308 | .query(query, values)
309 | .then((data) => data.rows[0])
310 | .catch((err) => new Error(err));
311 | },
312 |
313 | planets: () => {
314 | const query = 'SELECT * FROM planets';
315 | return db
316 | .query(query)
317 | .then((data) => data.rows)
318 | .catch((err) => new Error(err));
319 | },
320 |
321 | speciesById: (parent, args) => {
322 | const query = 'SELECT * FROM species WHERE _id = $1';
323 | const values = [args._id];
324 | return db
325 | .query(query, values)
326 | .then((data) => data.rows[0])
327 | .catch((err) => new Error(err));
328 | },
329 |
330 | species: () => {
331 | const query = 'SELECT * FROM species';
332 | return db
333 | .query(query)
334 | .then((data) => data.rows)
335 | .catch((err) => new Error(err));
336 | },
337 |
338 | vessel: (parent, args) => {
339 | const query = 'SELECT * FROM vessels WHERE _id = $1';
340 | const values = [args._id];
341 | return db
342 | .query(query, values)
343 | .then((data) => data.rows[0])
344 | .catch((err) => new Error(err));
345 | },
346 |
347 | vessels: () => {
348 | const query = 'SELECT * FROM vessels';
349 | return db
350 | .query(query)
351 | .then((data) => data.rows)
352 | .catch((err) => new Error(err));
353 | },
354 |
355 | starshipSpec: (parent, args) => {
356 | const query = 'SELECT * FROM starship_specs WHERE _id = $1';
357 | const values = [args._id];
358 | return db
359 | .query(query, values)
360 | .then((data) => data.rows[0])
361 | .catch((err) => new Error(err));
362 | },
363 |
364 | starshipSpecs: () => {
365 | const query = 'SELECT * FROM starship_specs';
366 | return db
367 | .query(query)
368 | .then((data) => data.rows)
369 | .catch((err) => new Error(err));
370 | },
371 | },
372 |
373 | Mutation: {
374 | addPerson: (parent, args) => {
375 | const query =
376 | 'INSERT INTO people (gender, species_id, homeworld_id, height, mass, hair_color, skin_color, eye_color, name, birth_year) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *';
377 | const values = [
378 | args.gender,
379 | args.species_id,
380 | args.homeworld_id,
381 | args.height,
382 | args.mass,
383 | args.hair_color,
384 | args.skin_color,
385 | args.eye_color,
386 | args.name,
387 | args.birth_year,
388 | ];
389 | return db
390 | .query(query, values)
391 | .then((data) => data.rows[0])
392 | .catch((err) => new Error(err));
393 | },
394 |
395 | updatePerson: (parent, args) => {
396 | let valList = [];
397 | for (const key of Object.keys(args)) {
398 | if (key !== '_id') valList.push(args[key]);
399 | }
400 | valList.push(args._id);
401 | const argsArray = Object.keys(args).filter((key) => key !== '_id');
402 | let setString = argsArray.map((key, i) => `${key} = $${i + 1}`).join(', ');
403 | const pKArg = `$${argsArray.length + 1}`;
404 | const query = `UPDATE people SET ${setString} WHERE _id = ${pKArg} RETURNING *`;
405 | const values = valList;
406 | return db
407 | .query(query, values)
408 | .then((data) => data.rows[0])
409 | .catch((err) => new Error(err));
410 | },
411 |
412 | addFilm: (parent, args) => {
413 | const query =
414 | 'INSERT INTO films (director, opening_crawl, episode_id, title, release_date, producer) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *';
415 | const values = [
416 | args.director,
417 | args.opening_crawl,
418 | args.episode_id,
419 | args.title,
420 | args.release_date,
421 | args.producer,
422 | ];
423 | return db
424 | .query(query, values)
425 | .then((data) => data.rows[0])
426 | .catch((err) => new Error(err));
427 | },
428 |
429 | updateFilm: (parent, args) => {
430 | let valList = [];
431 | for (const key of Object.keys(args)) {
432 | if (key !== '_id') valList.push(args[key]);
433 | }
434 | valList.push(args._id);
435 | const argsArray = Object.keys(args).filter((key) => key !== '_id');
436 | let setString = argsArray.map((key, i) => `${key} = $${i + 1}`).join(', ');
437 | const pKArg = `$${argsArray.length + 1}`;
438 | const query = `UPDATE films SET ${setString} WHERE _id = ${pKArg} RETURNING *`;
439 | const values = valList;
440 | return db
441 | .query(query, values)
442 | .then((data) => data.rows[0])
443 | .catch((err) => new Error(err));
444 | },
445 |
446 | addPlanet: (parent, args) => {
447 | const query =
448 | 'INSERT INTO planets (orbital_period, climate, gravity, terrain, surface_water, population, name, rotation_period, diameter) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *';
449 | const values = [
450 | args.orbital_period,
451 | args.climate,
452 | args.gravity,
453 | args.terrain,
454 | args.surface_water,
455 | args.population,
456 | args.name,
457 | args.rotation_period,
458 | args.diameter,
459 | ];
460 | return db
461 | .query(query, values)
462 | .then((data) => data.rows[0])
463 | .catch((err) => new Error(err));
464 | },
465 |
466 | updatePlanet: (parent, args) => {
467 | let valList = [];
468 | for (const key of Object.keys(args)) {
469 | if (key !== '_id') valList.push(args[key]);
470 | }
471 | valList.push(args._id);
472 | const argsArray = Object.keys(args).filter((key) => key !== '_id');
473 | let setString = argsArray.map((key, i) => `${key} = $${i + 1}`).join(', ');
474 | const pKArg = `$${argsArray.length + 1}`;
475 | const query = `UPDATE planets SET ${setString} WHERE _id = ${pKArg} RETURNING *`;
476 | const values = valList;
477 | return db
478 | .query(query, values)
479 | .then((data) => data.rows[0])
480 | .catch((err) => new Error(err));
481 | },
482 |
483 | addSpecies: (parent, args) => {
484 | const query =
485 | 'INSERT INTO species (hair_colors, name, classification, average_height, average_lifespan, skin_colors, eye_colors, language, homeworld_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *';
486 | const values = [
487 | args.hair_colors,
488 | args.name,
489 | args.classification,
490 | args.average_height,
491 | args.average_lifespan,
492 | args.skin_colors,
493 | args.eye_colors,
494 | args.language,
495 | args.homeworld_id,
496 | ];
497 | return db
498 | .query(query, values)
499 | .then((data) => data.rows[0])
500 | .catch((err) => new Error(err));
501 | },
502 |
503 | updateSpecies: (parent, args) => {
504 | let valList = [];
505 | for (const key of Object.keys(args)) {
506 | if (key !== '_id') valList.push(args[key]);
507 | }
508 | valList.push(args._id);
509 | const argsArray = Object.keys(args).filter((key) => key !== '_id');
510 | let setString = argsArray.map((key, i) => `${key} = $${i + 1}`).join(', ');
511 | const pKArg = `$${argsArray.length + 1}`;
512 | const query = `UPDATE species SET ${setString} WHERE _id = ${pKArg} RETURNING *`;
513 | const values = valList;
514 | return db
515 | .query(query, values)
516 | .then((data) => data.rows[0])
517 | .catch((err) => new Error(err));
518 | },
519 |
520 | addVessel: (parent, args) => {
521 | const query =
522 | 'INSERT INTO vessels (cost_in_credits, length, vessel_type, model, manufacturer, name, vessel_class, max_atmosphering_speed, crew, passengers, cargo_capacity, consumables) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *';
523 | const values = [
524 | args.cost_in_credits,
525 | args.length,
526 | args.vessel_type,
527 | args.model,
528 | args.manufacturer,
529 | args.name,
530 | args.vessel_class,
531 | args.max_atmosphering_speed,
532 | args.crew,
533 | args.passengers,
534 | args.cargo_capacity,
535 | args.consumables,
536 | ];
537 | return db
538 | .query(query, values)
539 | .then((data) => data.rows[0])
540 | .catch((err) => new Error(err));
541 | },
542 |
543 | updateVessel: (parent, args) => {
544 | let valList = [];
545 | for (const key of Object.keys(args)) {
546 | if (key !== '_id') valList.push(args[key]);
547 | }
548 | valList.push(args._id);
549 | const argsArray = Object.keys(args).filter((key) => key !== '_id');
550 | let setString = argsArray.map((key, i) => `${key} = $${i + 1}`).join(', ');
551 | const pKArg = `$${argsArray.length + 1}`;
552 | const query = `UPDATE vessels SET ${setString} WHERE _id = ${pKArg} RETURNING *`;
553 | const values = valList;
554 | return db
555 | .query(query, values)
556 | .then((data) => data.rows[0])
557 | .catch((err) => new Error(err));
558 | },
559 |
560 | addStarshipSpec: (parent, args) => {
561 | const query =
562 | 'INSERT INTO starship_specs (vessel_id, MGLT, hyperdrive_rating) VALUES ($1, $2, $3) RETURNING *';
563 | const values = [args.vessel_id, args.MGLT, args.hyperdrive_rating];
564 | return db
565 | .query(query, values)
566 | .then((data) => data.rows[0])
567 | .catch((err) => new Error(err));
568 | },
569 |
570 | updateStarshipSpec: (parent, args) => {
571 | let valList = [];
572 | for (const key of Object.keys(args)) {
573 | if (key !== '_id') valList.push(args[key]);
574 | }
575 | valList.push(args._id);
576 | const argsArray = Object.keys(args).filter((key) => key !== '_id');
577 | let setString = argsArray.map((key, i) => `${key} = $${i + 1}`).join(', ');
578 | const pKArg = `$${argsArray.length + 1}`;
579 | const query = `UPDATE starship_specs SET ${setString} WHERE _id = ${pKArg} RETURNING *`;
580 | const values = valList;
581 | return db
582 | .query(query, values)
583 | .then((data) => data.rows[0])
584 | .catch((err) => new Error(err));
585 | },
586 | },
587 |
588 | Person: {
589 | films: (people) => {
590 | const query =
591 | 'SELECT * FROM films LEFT OUTER JOIN people_in_films ON films._id = people_in_films.film_id WHERE people_in_films.person_id = $1';
592 | const values = [people._id];
593 | return db
594 | .query(query, values)
595 | .then((data) => data.rows)
596 | .catch((err) => new Error(err));
597 | },
598 | species: (people) => {
599 | const query =
600 | 'SELECT species.* FROM species LEFT OUTER JOIN people ON species._id = people.species_id WHERE people._id = $1';
601 | const values = [people._id];
602 | return db
603 | .query(query, values)
604 | .then((data) => data.rows)
605 | .catch((err) => new Error(err));
606 | },
607 | planets: (people) => {
608 | const query =
609 | 'SELECT planets.* FROM planets LEFT OUTER JOIN people ON planets._id = people.homeworld_id WHERE people._id = $1';
610 | const values = [people._id];
611 | return db
612 | .query(query, values)
613 | .then((data) => data.rows)
614 | .catch((err) => new Error(err));
615 | },
616 | vessels: (people) => {
617 | const query =
618 | 'SELECT * FROM vessels LEFT OUTER JOIN pilots ON vessels._id = pilots.vessel_id WHERE pilots.person_id = $1';
619 | const values = [people._id];
620 | return db
621 | .query(query, values)
622 | .then((data) => data.rows)
623 | .catch((err) => new Error(err));
624 | },
625 | },
626 |
627 | Film: {
628 | planets: (films) => {
629 | const query =
630 | 'SELECT * FROM planets LEFT OUTER JOIN planets_in_films ON planets._id = planets_in_films.planet_id WHERE planets_in_films.film_id = $1';
631 | const values = [films._id];
632 | return db
633 | .query(query, values)
634 | .then((data) => data.rows)
635 | .catch((err) => new Error(err));
636 | },
637 | people: (films) => {
638 | const query =
639 | 'SELECT * FROM people LEFT OUTER JOIN people_in_films ON people._id = people_in_films.person_id WHERE people_in_films.film_id = $1';
640 | const values = [films._id];
641 | return db
642 | .query(query, values)
643 | .then((data) => data.rows)
644 | .catch((err) => new Error(err));
645 | },
646 | vessels: (films) => {
647 | const query =
648 | 'SELECT * FROM vessels LEFT OUTER JOIN vessels_in_films ON vessels._id = vessels_in_films.vessel_id WHERE vessels_in_films.film_id = $1';
649 | const values = [films._id];
650 | return db
651 | .query(query, values)
652 | .then((data) => data.rows)
653 | .catch((err) => new Error(err));
654 | },
655 | species: (films) => {
656 | const query =
657 | 'SELECT * FROM species LEFT OUTER JOIN species_in_films ON species._id = species_in_films.species_id WHERE species_in_films.film_id = $1';
658 | const values = [films._id];
659 | return db
660 | .query(query, values)
661 | .then((data) => data.rows)
662 | .catch((err) => new Error(err));
663 | },
664 | },
665 |
666 | Planet: {
667 | films: (planets) => {
668 | const query =
669 | 'SELECT * FROM films LEFT OUTER JOIN planets_in_films ON films._id = planets_in_films.film_id WHERE planets_in_films.planet_id = $1';
670 | const values = [planets._id];
671 | return db
672 | .query(query, values)
673 | .then((data) => data.rows)
674 | .catch((err) => new Error(err));
675 | },
676 | species: (planets) => {
677 | const query = 'SELECT * FROM species WHERE homeworld_id = $1';
678 | const values = [planets._id];
679 | return db
680 | .query(query, values)
681 | .then((data) => data.rows)
682 | .catch((err) => new Error(err));
683 | },
684 | people: (planets) => {
685 | const query = 'SELECT * FROM people WHERE homeworld_id = $1';
686 | const values = [planets._id];
687 | return db
688 | .query(query, values)
689 | .then((data) => data.rows)
690 | .catch((err) => new Error(err));
691 | },
692 | },
693 |
694 | Species: {
695 | people: (species) => {
696 | const query = 'SELECT * FROM people WHERE species_id = $1';
697 | const values = [species._id];
698 | return db
699 | .query(query, values)
700 | .then((data) => data.rows)
701 | .catch((err) => new Error(err));
702 | },
703 | planets: (species) => {
704 | const query =
705 | 'SELECT planets.* FROM planets LEFT OUTER JOIN species ON planets._id = species.homeworld_id WHERE species._id = $1';
706 | const values = [species._id];
707 | return db
708 | .query(query, values)
709 | .then((data) => data.rows)
710 | .catch((err) => new Error(err));
711 | },
712 | films: (species) => {
713 | const query =
714 | 'SELECT * FROM films LEFT OUTER JOIN species_in_films ON films._id = species_in_films.film_id WHERE species_in_films.species_id = $1';
715 | const values = [species._id];
716 | return db
717 | .query(query, values)
718 | .then((data) => data.rows)
719 | .catch((err) => new Error(err));
720 | },
721 | },
722 |
723 | Vessel: {
724 | films: (vessels) => {
725 | const query =
726 | 'SELECT * FROM films LEFT OUTER JOIN vessels_in_films ON films._id = vessels_in_films.film_id WHERE vessels_in_films.vessel_id = $1';
727 | const values = [vessels._id];
728 | return db
729 | .query(query, values)
730 | .then((data) => data.rows)
731 | .catch((err) => new Error(err));
732 | },
733 | people: (vessels) => {
734 | const query =
735 | 'SELECT * FROM people LEFT OUTER JOIN pilots ON people._id = pilots.person_id WHERE pilots.vessel_id = $1';
736 | const values = [vessels._id];
737 | return db
738 | .query(query, values)
739 | .then((data) => data.rows)
740 | .catch((err) => new Error(err));
741 | },
742 | starshipSpecs: (vessels) => {
743 | const query = 'SELECT * FROM starship_specs WHERE vessel_id = $1';
744 | const values = [vessels._id];
745 | return db
746 | .query(query, values)
747 | .then((data) => data.rows)
748 | .catch((err) => new Error(err));
749 | },
750 | },
751 | };
752 |
753 | const schema = makeExecutableSchema({
754 | typeDefs,
755 | resolvers,
756 | });
757 |
758 | module.exports = schema;
759 |
--------------------------------------------------------------------------------
/server/secretKey.js:
--------------------------------------------------------------------------------
1 | const secretKey = '),E?/*viF%ul,.d';
2 |
3 | module.exports = secretKey;
4 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | // const path = require('path');
3 | const router = require('./router');
4 | const app = express();
5 |
6 | app.use(express.json());
7 | app.use(express.urlencoded({ extended: true }));
8 |
9 | // route to dummy db
10 | app.use('/', router);
11 |
12 | // Serve the main index.html file for the root route
13 | app.get('/', (req, res) => {
14 | res
15 | .status(200)
16 | .send(
17 | 'Backend server is running. Use the webpack dev server at localhost:8080 for the frontend.'
18 | );
19 | });
20 |
21 | /* /visualizer Refresh Testing */
22 | app.get('/visualizer', (req, res) => {
23 | res
24 | .status(200)
25 | .send(
26 | 'Backend server is running. Use the webpack dev server at localhost:8080 for the frontend.'
27 | );
28 | });
29 |
30 | /* Catch All Route */
31 | app.use('*', (req, res) => {
32 | res.status(404).send('Not Found');
33 | });
34 |
35 | /* Global Error Handler */
36 | app.use((err, req, res, _next) => {
37 | console.log('error handler', err);
38 | res.status(500).send('Internal Server Error');
39 | });
40 |
41 | // eslint-disable-next-line no-unused-vars
42 | if (process.env.NODE_ENV !== 'test') {
43 | app.listen(3000, () => {
44 | console.log('Server listening on port 3000');
45 | });
46 | }
47 |
48 | module.exports = app;
49 |
--------------------------------------------------------------------------------
/server/tableQuery.sql:
--------------------------------------------------------------------------------
1 | SELECT json_object_agg( --> creates a json object; accepts 2 args; 1st is key, 2nd is value
2 | pk.table_name, json_build_object( --> creates a json object; accepts variable # args; matches up key, then value as pair
3 | 'primaryKey', pk.primary_key,
4 | 'foreignKeys', fk.foreign_keys,
5 | 'referencedBy', rd.referenced_by,
6 | 'columns', td.columns
7 | )
8 | ) AS tables
9 |
10 | FROM ( ---> Primary key data (pk)
11 |
12 | -------------------------------------------------------------------------
13 | SELECT conrelid::regclass AS table_name, -- regclass will turn conrelid to actual table name
14 | substring(pg_get_constraintdef(oid), '\((.*?)\)') AS primary_key ---(.*?) matches any character (except for line terminators))
15 | FROM pg_constraint ---- The catalog pg_constraint stores check, primary key, unique, and foreign key constraints on tables -- https://www.postgresql.org/docs/8.2/catalog-pg-constraint.html
16 | WHERE contype = 'p' AND connamespace = 'public'::regnamespace --- regnamespace will turn connamespace(number) to actual name space
17 | ---------------------------------------------------------------------------
18 | ) AS pk
19 |
20 | LEFT OUTER JOIN ( --- Foreign key data (fk)
21 | -------------------------------------------------------------------------------------
22 | SELECT conrelid::regclass AS table_name,
23 | json_object_agg(
24 | substring(pg_get_constraintdef(oid), '\((.*?)\)'), json_build_object(
25 | 'referenceTable', substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\('),
26 | 'referenceKey', substring(pg_get_constraintdef(oid), 'REFERENCES.*?\((.*?)\)')
27 | )
28 | ) AS foreign_keys
29 | FROM pg_constraint
30 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace
31 | GROUP BY table_name
32 | ---------------------------------------------------------------------------
33 | ) AS fk
34 | ON pk.table_name = fk.table_name
35 |
36 | LEFT OUTER JOIN ( --- Reference data (rd)
37 | -----------------------------------------------------------------------------------------
38 | SELECT substring(pg_get_constraintdef(oid), 'REFERENCES (.*?)\(') AS table_name, json_object_agg(
39 | conrelid::regclass, substring(pg_get_constraintdef(oid), '\((.*?)\)')
40 | ) AS referenced_by
41 | FROM pg_constraint
42 | WHERE contype = 'f' AND connamespace = 'public'::regnamespace
43 | GROUP BY table_name
44 | ----------------------------------------------------------------------------------------
45 | ) AS rd
46 | ON pk.table_name::regclass = rd.table_name::regclass
47 |
48 | LEFT OUTER JOIN ( --- Table data (td)
49 | -----------------------------------------------------------
50 | SELECT tab.table_name, json_object_agg(
51 | col.column_name, json_build_object(
52 | 'dataType', col.data_type,
53 | 'columnDefault', col.column_default,
54 | 'charMaxLength', col.character_maximum_length,
55 | 'isNullable', col.is_nullable
56 | )
57 | ) AS columns
58 |
59 | --- Table names
60 | FROM (
61 | SELECT table_name FROM information_schema.tables ---- built-in; lists of all the tables in a selected database --- https://www.sqlshack.com/learn-sql-the-information_schema-database/
62 | WHERE table_type='BASE TABLE' AND table_schema='public'
63 | ) AS tab
64 |
65 | --- Table columns
66 | INNER JOIN information_schema.columns AS col
67 | ON tab.table_name = col.table_name
68 | GROUP BY tab.table_name
69 | ------------------------------------------------------
70 | ) AS td
71 | ON td.table_name::regclass = pk.table_name
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
6 |
7 | const isProd = process.env.NODE_ENV === 'production';
8 |
9 | module.exports = {
10 | mode: process.env.NODE_ENV || 'development',
11 | entry: ['core-js/stable', 'regenerator-runtime/runtime', './client/index.js'],
12 | output: {
13 | path: path.resolve(__dirname, 'build'),
14 | filename: isProd ? 'bundle.[contenthash].js' : '[name].bundle.js',
15 | chunkFilename: isProd ? '[name].[contenthash].js' : '[name].chunk.js',
16 | publicPath: '/',
17 | clean: true,
18 | },
19 | devServer: {
20 | contentBase: path.join(__dirname, 'build'),
21 | hot: true,
22 | port: 8080,
23 | historyApiFallback: true,
24 | proxy: {
25 | '/example-schema': 'http://localhost:3000',
26 | '/sql-schema': 'http://localhost:3000',
27 | '/playground': 'http://localhost:3000',
28 | '/test': 'http://localhost:3000',
29 | '/test-mock': 'http://localhost:3000',
30 | '/test-gql': 'http://localhost:3000',
31 | },
32 | },
33 | plugins: [
34 | new MiniCssExtractPlugin({
35 | filename: isProd ? '[name].[contenthash].css' : '[name].css',
36 | chunkFilename: isProd ? '[id].[contenthash].css' : '[id].css',
37 | }),
38 | new HtmlWebpackPlugin({
39 | template: './client/index.html',
40 | filename: 'index.html',
41 | inject: 'body',
42 | /* eslint-disable indent */
43 | minify: isProd
44 | ? {
45 | removeComments: true,
46 | collapseWhitespace: true,
47 | removeAttributeQuotes: true,
48 | }
49 | : false,
50 | /* eslint-enable indent */
51 | }),
52 | new CopyWebpackPlugin({
53 | patterns: [
54 | { from: 'client/favicon.ico', to: 'favicon.ico' },
55 | { from: 'server/tableQuery.sql', to: 'tableQuery.sql' },
56 | {
57 | from: 'client/assets/example-schema.json',
58 | to: 'example-schema',
59 | toType: 'file',
60 | },
61 | // Optimized assets available but not auto-copied to avoid path conflicts
62 | ],
63 | }),
64 | ...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : []),
65 | ],
66 | optimization: {
67 | splitChunks: {
68 | chunks: 'all',
69 | cacheGroups: {
70 | vendor: {
71 | test: /[\\/]node_modules[\\/]/,
72 | name: 'vendors',
73 | chunks: 'all',
74 | },
75 | common: {
76 | name: 'common',
77 | minChunks: 2,
78 | chunks: 'all',
79 | enforce: true,
80 | },
81 | },
82 | },
83 | usedExports: true,
84 | sideEffects: false,
85 | minimize: isProd,
86 | },
87 | module: {
88 | rules: [
89 | {
90 | test: /\.(js|jsx)$/,
91 | exclude: /node_modules/,
92 | use: {
93 | loader: 'babel-loader',
94 | options: {
95 | presets: [
96 | [
97 | '@babel/preset-env',
98 | {
99 | targets: '> 0.25%, not dead',
100 | modules: false,
101 | useBuiltIns: 'usage',
102 | corejs: 3,
103 | },
104 | ],
105 | ['@babel/preset-react', { runtime: 'automatic' }],
106 | ],
107 | },
108 | },
109 | },
110 | {
111 | test: /\.s[ac]ss$/i,
112 | use: ['style-loader', 'css-loader', 'sass-loader'],
113 | },
114 | {
115 | test: /\.css$/i,
116 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
117 | },
118 | {
119 | test: /\.(png|jpe?g|gif|webp|avif)$/i,
120 | type: 'asset',
121 | parser: {
122 | dataUrlCondition: {
123 | maxSize: 8 * 1024, // 8kb
124 | },
125 | },
126 | generator: {
127 | filename: 'images/[name].[hash][ext]',
128 | },
129 | },
130 | ],
131 | },
132 | resolve: {
133 | extensions: ['.js', '.jsx', 'css'],
134 | fallback: {
135 | crypto: require.resolve('crypto-browserify'),
136 | buffer: require.resolve('buffer/'),
137 | stream: require.resolve('stream-browserify'),
138 | },
139 | },
140 | performance: {
141 | hints: isProd ? 'warning' : false,
142 | maxAssetSize: 1000000, // 1MB - more realistic for this app with large GIFs
143 | maxEntrypointSize: 3000000, // 3MB - account for vendor bundle size
144 | },
145 | };
146 |
--------------------------------------------------------------------------------