├── Procfile ├── .vscode └── extensions.json ├── src ├── vite-env.d.ts ├── assets │ ├── drawer.png │ ├── graph3.avif │ ├── package.avif │ ├── add_query.png │ ├── cache-aside.avif │ ├── denormalize.avif │ ├── niko_profile.avif │ ├── ryan_profile.avif │ ├── Query-Flow-icon.png │ ├── george_profile.avif │ ├── install_chart.avif │ ├── philip_profile.avif │ ├── queryflow_gif3.gif │ ├── vivek_profile.avif │ ├── Pagila-ER-Diagram.png │ ├── QueryFlow Tag Logo.png │ ├── bar_query_metrics.avif │ ├── flow-background.avif │ ├── QueryFlow-logo-white.png │ ├── join-not-optimized.avif │ ├── queryflow_allmetrics.gif │ ├── average_query_metrics.avif │ ├── queryflow_gif_single2.gif │ ├── queryflow_main_chart.avif │ ├── registry-editor_hires.avif │ └── scatter_query_metrics.avif ├── main.ts ├── pages │ ├── UserLogin.svelte │ ├── About.svelte │ ├── Tips.svelte │ ├── AllMetrics.svelte │ ├── Home.svelte │ └── Landing.svelte ├── types.d.ts ├── graphs │ ├── Table.svelte │ ├── RedisChart.svelte │ ├── GroupQuery.svelte │ ├── SingleScatterPlot.svelte │ └── SingleBarGraph.svelte ├── App.svelte ├── store.ts ├── app.css └── lib │ ├── RedisForm.svelte │ ├── Login.svelte │ ├── Navbar.svelte │ ├── Metrics.svelte │ ├── Drawer.svelte │ ├── Signup.svelte │ └── Terms.svelte ├── public ├── Query-Flow-icon.png └── vite.svg ├── postcss.config.cjs ├── cypress ├── fixtures │ └── example.json ├── e2e │ └── queryflow_testing │ │ ├── userlogin.cy.ts │ │ ├── mock.js │ │ ├── drawer.cy.ts │ │ ├── home.cy.ts │ │ ├── login.cy.ts │ │ ├── usercontroller.cy.ts │ │ ├── navbar.cy.ts │ │ ├── signup.cy.ts │ │ └── store.cy.ts └── support │ ├── e2e.ts │ └── commands.ts ├── tsconfig.node.json ├── .eslintignore ├── .prettierignore ├── server ├── middleware │ ├── oauth.js │ └── auth.js ├── models │ ├── redisModel.mjs │ └── ourDBModel.mjs ├── routes │ ├── loginRoutes.mjs │ └── apiRoutes.mjs ├── controllers │ ├── redisController.mjs │ ├── ourDBController.js │ ├── userController.mjs │ ├── clientDBController.mjs │ └── oAuthController.js └── server.mjs ├── svelte.config.js ├── cypress.config.ts ├── .prettierrc ├── .gitignore ├── vite.config.ts ├── index.html ├── tsconfig.json ├── LICENSE ├── .eslintrc.cjs ├── tailwind.config.js ├── package.json └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node server/server.mjs 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/assets/drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/drawer.png -------------------------------------------------------------------------------- /src/assets/graph3.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/graph3.avif -------------------------------------------------------------------------------- /src/assets/package.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/package.avif -------------------------------------------------------------------------------- /src/assets/add_query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/add_query.png -------------------------------------------------------------------------------- /public/Query-Flow-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/public/Query-Flow-icon.png -------------------------------------------------------------------------------- /src/assets/cache-aside.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/cache-aside.avif -------------------------------------------------------------------------------- /src/assets/denormalize.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/denormalize.avif -------------------------------------------------------------------------------- /src/assets/niko_profile.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/niko_profile.avif -------------------------------------------------------------------------------- /src/assets/ryan_profile.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/ryan_profile.avif -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/Query-Flow-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/Query-Flow-icon.png -------------------------------------------------------------------------------- /src/assets/george_profile.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/george_profile.avif -------------------------------------------------------------------------------- /src/assets/install_chart.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/install_chart.avif -------------------------------------------------------------------------------- /src/assets/philip_profile.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/philip_profile.avif -------------------------------------------------------------------------------- /src/assets/queryflow_gif3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/queryflow_gif3.gif -------------------------------------------------------------------------------- /src/assets/vivek_profile.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/vivek_profile.avif -------------------------------------------------------------------------------- /src/assets/Pagila-ER-Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/Pagila-ER-Diagram.png -------------------------------------------------------------------------------- /src/assets/QueryFlow Tag Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/QueryFlow Tag Logo.png -------------------------------------------------------------------------------- /src/assets/bar_query_metrics.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/bar_query_metrics.avif -------------------------------------------------------------------------------- /src/assets/flow-background.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/flow-background.avif -------------------------------------------------------------------------------- /src/assets/QueryFlow-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/QueryFlow-logo-white.png -------------------------------------------------------------------------------- /src/assets/join-not-optimized.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/join-not-optimized.avif -------------------------------------------------------------------------------- /src/assets/queryflow_allmetrics.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/queryflow_allmetrics.gif -------------------------------------------------------------------------------- /src/assets/average_query_metrics.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/average_query_metrics.avif -------------------------------------------------------------------------------- /src/assets/queryflow_gif_single2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/queryflow_gif_single2.gif -------------------------------------------------------------------------------- /src/assets/queryflow_main_chart.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/queryflow_main_chart.avif -------------------------------------------------------------------------------- /src/assets/registry-editor_hires.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/registry-editor_hires.avif -------------------------------------------------------------------------------- /src/assets/scatter_query_metrics.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QueryFlow/HEAD/src/assets/scatter_query_metrics.avif -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.css'; 2 | import App from './App.svelte'; 3 | 4 | // hanging app here 5 | const app = new App({ 6 | target: document.getElementById('app'), 7 | }); 8 | 9 | export default app; 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /server/middleware/oauth.js: -------------------------------------------------------------------------------- 1 | import {google} from 'googleapis'; 2 | 3 | const oauth2Client = new google.auth.OAuth2( 4 | process.env.CLIENT_ID, 5 | process.env.CLIENT_SECRET, 6 | process.env.REDIRECT_URL 7 | ); 8 | 9 | export default oauth2Client; 10 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | viewportWidth: 1920, 5 | viewportHeight: 1080, 6 | e2e: { 7 | setupNodeEvents(on, config) { 8 | // implement node event listeners here 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "printWidth": 100, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "pluginSearchDirs": ["."], 9 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # Ignore .env for production 11 | 12 | node_modules 13 | dist 14 | dist-ssr 15 | *.local 16 | .env 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | const PORT = process.env.PORT || 3000; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | server: { 8 | proxy: { 9 | '/api': { 10 | target: `http://localhost:${PORT}`, 11 | changeOrigin: true, 12 | }, 13 | }, 14 | }, 15 | plugins: [svelte()], 16 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | QueryFlow 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /server/models/redisModel.mjs: -------------------------------------------------------------------------------- 1 | import redis from 'redis'; 2 | 3 | const redisModel = redis.createClient({ 4 | password: process.env.REDIS_PASS, 5 | socket: { 6 | host: 'redis-16881.c267.us-east-1-4.ec2.cloud.redislabs.com', 7 | port: '16881' 8 | }, 9 | }); 10 | 11 | redisModel.on('connect',() => { 12 | console.log('Connected to Redis successfully!'); 13 | }); 14 | 15 | redisModel.connect(); 16 | 17 | export default redisModel; 18 | -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/userlogin.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('landing', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:5173/login'); 6 | }); 7 | 8 | it('Renders Login component by default', () => { 9 | cy.get('div.login-shadow').contains('Login'); 10 | }); 11 | 12 | it('Login component should have title "Login"', () => { 13 | cy.get('h1').should('have.text', 'Login'); 14 | }); 15 | }); -------------------------------------------------------------------------------- /server/models/ourDBModel.mjs: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | const { Pool } = pg; 3 | 4 | // Our PG URI 5 | const myURI = process.env.URI; 6 | 7 | const pool = new Pool({ 8 | connectionString: myURI, 9 | //Some Postgres Services have self signing certificates so this tells node to bypass and trust the cert. Remove below if new DB comes and it isnt working. 10 | ssl: { 11 | rejectUnauthorized: false, 12 | }, 13 | }); 14 | 15 | const ourDBModel = function(text, params, callback) { 16 | 17 | return pool.query(text, params, callback); 18 | }; 19 | 20 | export default ourDBModel; 21 | 22 | -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/mock.js: -------------------------------------------------------------------------------- 1 | export const mockData = { 2 | metricData: [ 3 | { 4 | _id: 1, 5 | queryString: 'SELECT * FROM users', 6 | queryMetrics: [ 7 | { 8 | planningTime: 10, 9 | executionTime: 50, 10 | totalTime: 60, 11 | cacheSize: '10MB', 12 | workingMem: '20MB', 13 | sharedHitBlocks: 100, 14 | sharedReadBlocks: 50, 15 | } 16 | ], 17 | queryName: 'getUsers', 18 | queryCount: 10, 19 | queryDelay: 100, 20 | averageTime: 200, 21 | createdAt: '2022-07-14T07:21:21.779Z', 22 | }, 23 | ], 24 | userInfo: { 25 | firstName: 'John', 26 | lastName: 'Doe', 27 | }, 28 | }; -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny":true, 5 | "target": "ESNext", 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | "resolveJsonModule": true, 9 | /** 10 | * Typecheck JS in `.svelte` and `.js` files by default. 11 | * Disable checkJs if you'd like to use dynamic types in JS. 12 | * Note that setting allowJs false does not prevent the use 13 | * of JS in `.svelte` files. 14 | */ 15 | "allowJs": true, 16 | "checkJs": true, 17 | "isolatedModules": true, 18 | "inlineSourceMap": true, 19 | "sourceMap": false, 20 | "types": ["cypress", "node"] 21 | }, 22 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/UserLogin.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | flowing rays of light 17 | 18 | 19 | {#if $renderSignup} 20 | 21 | {:else} 22 | 23 | {/if} 24 |
25 | 26 | 31 | -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | const authenticationMiddleware = async (req,res,next) => { 4 | const authHeader = req.headers.authorization; 5 | 6 | if(!authHeader || !authHeader.startsWith('Bearer ')){ 7 | return next({ 8 | log: 'Error handler caught error in auth middleware, No Header', 9 | status: 400, 10 | message: 'Error handler caught error in auth middleware, No Header', 11 | }); 12 | } 13 | 14 | const token = authHeader.split(' ')[1]; 15 | 16 | try { 17 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 18 | 19 | const { _id } = decoded; 20 | req.user = { _id }; 21 | next(); 22 | } catch (error) { 23 | next({ 24 | log: 'Error handler caught error in auth middleware', 25 | status: 400, 26 | message: 'Error handler caught error in auth middleware', 27 | }); 28 | } 29 | }; 30 | 31 | export default authenticationMiddleware; -------------------------------------------------------------------------------- /server/routes/loginRoutes.mjs: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | import userController from '../controllers/userController.mjs'; 4 | import oAuthController from '../controllers/oAuthController.js'; 5 | 6 | // Signing up 7 | router.post('/signup', userController.create, (req, res) => { 8 | res.status(201).json({msg: 'Account made'}); 9 | }); 10 | 11 | // Logging in 12 | router.post('/login', userController.login, (req, res) => { 13 | res.status(200).json({userData: res.locals.data,token:res.locals.authentication}); 14 | }); 15 | 16 | // Google Login 17 | router.get('/google-login', oAuthController.login, (req, res) => { 18 | res.status(200).json({msg:'successful google login'}); 19 | }); 20 | 21 | // Google Callback 22 | router.get('/google-login/callback', oAuthController.googleResponse, (req, res) => { 23 | res.status(200).json({msg:'callback successful'}); 24 | }); 25 | 26 | 27 | export default router; 28 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface QueryMetrics { 2 | planningTime: number; 3 | executionTime: number; 4 | totalTime: number; 5 | cacheSize: string; 6 | workingMem: string; 7 | sharedHitBlocks: number; 8 | sharedReadBlocks: number; 9 | } 10 | 11 | export interface QueryData { 12 | _id: number; 13 | queryString: string; 14 | queryMetrics: QueryMetrics[]; 15 | queryName: string; 16 | queryCount: number; 17 | queryDelay: number; 18 | averageTime: number; 19 | createdAt: string; 20 | } 21 | 22 | export interface GraphData { 23 | x: number; 24 | y: number; 25 | type: string; 26 | name: string; 27 | date?: string; 28 | } 29 | 30 | export interface RedisData{ 31 | totalTimeRedis: number; 32 | totalTimeSQL: number; 33 | } 34 | 35 | export interface WorkingArr { 36 | topValue?: number; 37 | bottomValue?: number; 38 | numberOfQueries?: number; 39 | } 40 | 41 | export interface Directions { 42 | top: number; 43 | right: number; 44 | left: number; 45 | bottom: numbe; 46 | } 47 | 48 | export type UserType = { 49 | firstName: string; 50 | lastName: string; 51 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/drawer.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Drawer Tests', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:5173/home'); 6 | cy.get('label').contains('Add a Query').click(); 7 | }); 8 | 9 | it('should open the drawer when the button is clicked', () => { 10 | cy.get('.drawer-side').should('have.css', 'left', '0px'); 11 | }); 12 | 13 | it('allows input of query details', () => { 14 | 15 | cy.get('#queryName') 16 | .type('My Query') 17 | .should('have.value', 'My Query'); 18 | 19 | cy.get('#uri') 20 | .type('postgres://exampleURI.com/example') 21 | .should('have.value', 'postgres://exampleURI.com/example'); 22 | 23 | cy.get('#queryString') 24 | .type('SELECT * FROM your_table') 25 | .should('have.value', 'SELECT * FROM your_table'); 26 | }); 27 | 28 | 29 | it('clicking outside of the drawer should close the drawer', () => { 30 | cy.get('.drawer-side').should('have.css', 'left', '0px'); 31 | cy.get('.drawer-overlay').click(); 32 | cy.get('.drawer-side').should('have.css', 'left', '-1536px'); 33 | }); 34 | 35 | }); -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // module.exports = { 2 | // root: true, 3 | // extends: [ 4 | // 'eslint:recommended', 5 | // 'plugin:@typescript-eslint/recommended', 6 | // 'plugin:svelte/recommended', 7 | // 'prettier' 8 | // ], 9 | // parser: '@typescript-eslint/parser', 10 | // plugins: ['@typescript-eslint'], 11 | // parserOptions: { 12 | // sourceType: 'module', 13 | // ecmaVersion: 2021, 14 | // extraFileExtensions: ['.svelte'] 15 | // }, 16 | // env: { 17 | // browser: true, 18 | // es2017: true, 19 | // node: true 20 | // }, 21 | // overrides: [ 22 | // { 23 | // files: ['*.svelte'], 24 | // parser: 'svelte-eslint-parser', 25 | // parserOptions: { 26 | // parser: '@typescript-eslint/parser' 27 | // } 28 | // } 29 | // ], 30 | // // Adds rules for codesmith-esque linting 31 | // rules: { 32 | // indent: ['warn', 2], 33 | // '@typescript-eslint/no-unused-vars': ['off', { vars: 'local' }], 34 | // 'prefer-const': 'warn', 35 | // quotes: ['warn', 'single'], 36 | // semi: ['warn', 'always'], 37 | // 'space-infix-ops': 'warn', 38 | // }, 39 | 40 | // }; 41 | -------------------------------------------------------------------------------- /src/graphs/Table.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {#each queryMetrics as metric, i} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {/each} 35 | 36 |
#Planning TimeExecution TimeTotal TimeShared Hit BlocksShared Read Blocks
{i+1}{metric.planningTime}{metric.executionTime}{metric.totalTime}{metric.sharedHitBlocks}{metric.sharedReadBlocks}
37 |
38 |
-------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{svelte,js,ts}', './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'], 3 | daisyui: { 4 | themes: [ 5 | 'light', 6 | 'dark', 7 | 'cupcake', 8 | 'bumblebee', 9 | 'emerald', 10 | 'corporate', 11 | 'synthwave', 12 | 'retro', 13 | 'cyberpunk', 14 | 'valentine', 15 | 'halloween', 16 | 'garden', 17 | 'forest', 18 | 'aqua', 19 | 'lofi', 20 | 'pastel', 21 | 'fantasy', 22 | 'wireframe', 23 | 'black', 24 | 'luxury', 25 | 'dracula', 26 | 'cmyk', 27 | 'autumn', 28 | 'business', 29 | 'acid', 30 | 'lemonade', 31 | 'night', 32 | 'coffee', 33 | 'winter', 34 | { 35 | mytheme: { 36 | primary: '#1245a8', 37 | 38 | secondary: '#1f5eda', 39 | 40 | accent: '#1fb2a6', 41 | 42 | neutral: '#2a323c', 43 | 44 | 'base-100': 'white', 45 | 46 | info: '#3abff8', 47 | 48 | success: '#36d399', 49 | 50 | warning: '#fbbd23', 51 | 52 | error: '#f87272', 53 | }, 54 | }, 55 | ], 56 | }, 57 | plugins: [require('flowbite/plugin'),require('@tailwindcss/typography'),require('daisyui')], 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/home.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { 4 | metricData, 5 | } from '../../../src/store'; 6 | 7 | describe('Home Tests', () => { 8 | beforeEach(() => { 9 | cy.visit('http://localhost:5173/home', { 10 | }); 11 | }); 12 | it('loads the homepage', () => { 13 | cy.url().should('include', '/'); 14 | }); 15 | 16 | it('checks the dropdown selection', () => { 17 | cy.get('select').should('have.value', 'all'); 18 | }); 19 | 20 | it('tests if the query metrics are being fetched', () => { 21 | cy.intercept('POST', '/api/get-metrics').as('getMetrics'); 22 | }); 23 | 24 | 25 | }); 26 | 27 | 28 | const metricDataSample = [ 29 | { 30 | _id: 1, 31 | queryString: 'SELECT * FROM table', 32 | queryMetrics: [ 33 | { 34 | planningTime: 10, 35 | executionTime: 20, 36 | totalTime: 30, 37 | cacheSize: '10MB', 38 | workingMem: '256MB', 39 | sharedHitBlocks: 100, 40 | sharedReadBlocks: 50, 41 | }, 42 | ], 43 | queryName: 'Sample Query', 44 | queryCount: 5, 45 | queryDelay: 100, 46 | averageTime: 50, 47 | createdAt: '2023-07-15', 48 | }, 49 | ]; 50 | 51 | describe('Home Tests', () => { 52 | beforeEach(() => { 53 | cy.visit('http://localhost:5173/home'); 54 | metricData.set(metricDataSample); 55 | }); 56 | }); -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { QueryData, UserType,RedisData } from './types'; 3 | import type { Writable } from 'svelte/store'; 4 | 5 | // initially empty, updatable through source code 6 | export const userInfoStore = writable({ 7 | firstName: '', 8 | lastName: '', 9 | }); 10 | 11 | // Store for main metrics array from metric get request in the home.svelte 12 | export const metricData: Writable = writable([]); 13 | 14 | // Store for first column on the homepage. Basically filtered metricsData based on the selection dropdown 15 | export const filterMetricData: Writable = writable([]); 16 | 17 | // Store for second column on the homepage. Basically filtered metricsData based on the selection dropdown 18 | export const filterMetricDataTwo: Writable = writable([]); 19 | 20 | // Store for Redis data 21 | export const redisData: Writable = writable(); 22 | 23 | // Authentication status, initially false, updatable through source code 24 | export const isAuthenticated: Writable = writable(false); 25 | 26 | // Loading status for running queries, initially false, updatable through source code 27 | export const isLoading: Writable = writable(false); 28 | 29 | // Render the login or signup component on the userlogin page 30 | export const renderSignup: Writable = writable(false); -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/login.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Login tests', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:5173/login'); 6 | }); 7 | 8 | it('fills out the form and submits', () => { 9 | cy.get('input[name=email]').type('test@example.com'); 10 | cy.get('input[name=password]').type('password123'); 11 | cy.get('button[type=submit]').click(); 12 | }); 13 | 14 | it('clicks the Sign Up button', () => { 15 | cy.get('button').contains('Sign Up').click(); 16 | }); 17 | 18 | it('verifies all elements exist', () => { 19 | cy.get('input[name=email]'); 20 | cy.get('input[name=password]'); 21 | cy.get('button[type=submit]'); 22 | cy.get('button').contains('Sign Up'); 23 | cy.get('button').contains('Sign in with Google'); 24 | }); 25 | 26 | it('submits form with empty fields', () => { 27 | cy.get('button[type=submit]').click(); 28 | }); 29 | 30 | it('submits form with incorrect email format', () => { 31 | cy.get('input[name=email]').type('notanemail'); 32 | cy.get('input[name=password]').type('password'); 33 | cy.get('button[type=submit]').click(); 34 | }); 35 | 36 | it('submits form with incorrect password', () => { 37 | cy.get('input[name=email]').type('user@example.com'); 38 | cy.get('input[name=password]').type('wrongpassword'); 39 | cy.get('button[type=submit]').click(); 40 | cy.get('.alert-warning').should('be.visible'); 41 | }); 42 | }); -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/usercontroller.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('User API', () => { 4 | const user = { 5 | firstName: 'Test1k2jbh34kb124k', 6 | lastName: 'User', 7 | email: 'testuser2@example.com', 8 | password: 'testpassword', 9 | organization: 'Test Org', 10 | database: 'testdb', 11 | }; 12 | 13 | beforeEach(() => { 14 | cy.visit('http://localhost:5173/'); 15 | }); 16 | 17 | 18 | 19 | it('should create a new user', () => { 20 | cy.request({ 21 | method: 'POST', 22 | url: 'http://localhost:3000/api/signup', 23 | body: user, 24 | headers: { 25 | 'Content-Type': 'application/json' 26 | } 27 | }).then((response) => { 28 | expect(response.status).to.eq(201); 29 | expect(response.body).to.have.property('msg', 'Account made'); 30 | }); 31 | }); 32 | 33 | it('should login test user',() => { 34 | cy.request({ 35 | method: 'POST', 36 | url: 'http://localhost:3000/api/login', 37 | body: { email: 'testuser2@example.com', password: 'testpassword' }, 38 | headers: { 39 | 'Content-Type': 'application/json' 40 | } 41 | }).then((response) => { 42 | expect(response.status).to.eq(200); 43 | }); 44 | }); 45 | }); 46 | 47 | // This test will work if you change the static folder line and sendfile in server.mjs(express server file) back to src folder and index.html respectively. 48 | 49 | // it('successfully communicates with the backend', () => { 50 | // cy.request('http://localhost:3000/') 51 | // .its('status') 52 | // .should('eq', 200); 53 | // }); 54 | -------------------------------------------------------------------------------- /server/routes/apiRoutes.mjs: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | import clientDBController from '../controllers/clientDBController.mjs'; 4 | import ourDBController from '../controllers/ourDBController.js'; 5 | import redisController from '../controllers/redisController.mjs'; 6 | import authenticationMiddleware from '../middleware/auth.js'; 7 | 8 | 9 | // Posts metrics to our PostgreSQL metrics table 10 | router.post('/query-metrics', authenticationMiddleware, clientDBController.queryMetrics, ourDBController.queryPush, (req, res) => { 11 | res.status(201).json(res.locals.metrics); 12 | }); 13 | 14 | // Retrieves metrics from our PostgreSQL metrics table 15 | router.post('/get-metrics', authenticationMiddleware, ourDBController.queryGet, (req, res) => { 16 | res.status(200).json(res.locals.getMetrics); 17 | }); 18 | 19 | // Retrieves data from user's database, measures the time, posts to our Redis instance, measures time. 20 | router.post('/redis-metrics', authenticationMiddleware ,clientDBController.queryTimeSQL, redisController.latency, (req, res) => { 21 | res.status(200).json(res.locals.comparisonData); 22 | }); 23 | 24 | // Deletes a query from the PostgreSQL metrics table by id 25 | router.delete('/delete-metrics-id', authenticationMiddleware, ourDBController.deleteQueryById, (req, res) => { 26 | res.status(200).json({msg: 'Query Deleted'}); 27 | }); 28 | 29 | // ===== Unused route, preserving for later use if needed ===== 30 | // Deletes a query from the PostgreSQL metrics table by name 31 | // router.delete('/delete-metrics-name', ourDBController.deleteQueryByName, (req, res) => { 32 | // res.status(200).json({msg: 'Query Deleted'}); 33 | // }); 34 | 35 | export default router; -------------------------------------------------------------------------------- /server/controllers/redisController.mjs: -------------------------------------------------------------------------------- 1 | import redisModel from '../models/redisModel.mjs'; 2 | 3 | const redisController = {}; 4 | 5 | redisController.latency = async (req, res, next) => { 6 | 7 | const { resultData, totalTimeSQL } = res.locals.queryResultSQL; 8 | 9 | try { 10 | await redisModel.json.set('Result', '.', resultData, (err) => { 11 | if (err) { 12 | console.error('Error storing data in Redis: ', err); 13 | return res.status(500).json({ error: 'Failed to store data in Redis' }); 14 | } 15 | return res.status(200).json({ message: 'Data stored successfully' }); 16 | }); 17 | await redisModel.expire('Result', 2); 18 | // Calculates performance metrics (e.g., time taken to retrieve the value) 19 | const startTime = process.hrtime(); 20 | const getResult = await redisModel.json.get('Result', { 21 | path: '.', 22 | }); 23 | // Below comment may work exactly as the above getResult 24 | // const getResult = await redisModel.json.get('Result', '.'); 25 | const endTime = process.hrtime(startTime); 26 | const totalTimeRedis = (endTime[0] * 1000 + endTime[1] / 1000000).toFixed(2); 27 | const resObj = {}; 28 | // Time for redis to get the key-value data 29 | resObj.totalTimeRedis = totalTimeRedis; 30 | // Time for PostgreSQL to get the data 31 | resObj.totalTimeSQL = totalTimeSQL; 32 | 33 | res.locals.comparisonData = resObj; 34 | 35 | return next(); 36 | } catch (err) { 37 | return next({ 38 | log: 'Error handler caught error in redisController.latency middleware', 39 | status: 500, 40 | message: 'Failed to perform Redis operation' 41 | }); 42 | } 43 | }; 44 | 45 | export default redisController; -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/navbar.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Navbar Tests', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:5173/login', { 6 | onBeforeLoad(win) { 7 | // Mocks the localStorage getItem method to return a token 8 | cy.stub(win.localStorage, 'getItem').returns('token'); 9 | }, 10 | }); 11 | }); 12 | 13 | it('displays the logo and link to homepage', () => { 14 | cy.get('.logo-text img').should('have.attr', 'src', '/src/assets/Query-Flow-icon.png'); 15 | cy.get('.logo-text').contains('QueryFlow'); 16 | }); 17 | 18 | it('displays the navigation links', () => { 19 | cy.get('.navtags').should('have.length', 5); 20 | cy.get('.navtags').eq(0).should('have.text', 'Home'); 21 | cy.get('.navtags').eq(1).should('have.text', 'About'); 22 | cy.get('.navtags').eq(2).should('have.text', 'SQL Tips'); 23 | cy.get('.navtags').eq(3).should('have.text', 'GitHub').should('have.attr', 'href', 'https://github.com/oslabs-beta/QueryFlow'); 24 | }); 25 | 26 | 27 | it('logs out and redirects to homepage', () => { 28 | cy.intercept('https://accounts.google.com/o/oauth2/revoke*').as('revokeToken'); 29 | cy.url().should('eq', 'http://localhost:5173/home'); 30 | }); 31 | 32 | it('navigates to the About page', () => { 33 | cy.get('.navtags').contains('About').click(); 34 | cy.url().should('include', 'http://localhost:5173/about'); 35 | }); 36 | 37 | it('navigates to the SQL Tips page', () => { 38 | cy.get('.navtags').contains('SQL Tips').click(); 39 | cy.url().should('include', 'http://localhost:5173/tips'); 40 | }); 41 | 42 | it('opens the GitHub link in a new tab', () => { 43 | cy.get('.navtags').contains('GitHub').should('have.attr', 'target', '_blank'); 44 | }); 45 | }); -------------------------------------------------------------------------------- /src/graphs/RedisChart.svelte: -------------------------------------------------------------------------------- 1 | 50 | 51 | 52 |
53 | 54 |
55 | 56 |
57 |
-------------------------------------------------------------------------------- /server/server.mjs: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import { fileURLToPath } from 'url'; 4 | import cors from 'cors'; 5 | import cookieParser from 'cookie-parser'; 6 | import { dirname, resolve } from 'path'; 7 | import compression from 'compression'; 8 | import apiRouter from './routes/apiRoutes.mjs'; 9 | import loginRouter from './routes/loginRoutes.mjs'; 10 | 11 | // We utilize the fileURLToPath function from the url module to convert the import.meta.url to the corresponding file path?? 12 | const __filename = fileURLToPath(import.meta.url); 13 | 14 | // We extract the directory name using the dirname function from the path module 15 | const __dirname = dirname(__filename); 16 | 17 | // Initiate express server 18 | const app = express(); 19 | const PORT = process.env.PORT || 3000; 20 | 21 | // Enable compression middleware for better performance & other middleware 22 | app.use(compression()); 23 | app.use(cors()); 24 | app.use(cookieParser()); 25 | app.use(express.json()); 26 | 27 | // Serve static assets 28 | // Change to '../src' if problem. This current setup is for distribution 29 | app.use(express.static(resolve(__dirname, '../dist'))); 30 | 31 | // Use apiRouter/loginRouter 32 | app.use('/api', apiRouter, loginRouter); 33 | 34 | // Serve index.html file 35 | // Change to '../index.html' if problem. This current setup is for distribution 36 | app.get('/*', (req, res) => { 37 | res.sendFile(resolve(__dirname, '../dist/index.html')); 38 | }); 39 | 40 | 41 | // Route error handler 42 | app.use('*', (req, res) => { 43 | return res.status(404).send('Page not Found'); 44 | }); 45 | 46 | // GLOBAL error handler 47 | app.use((err, req, res, next) => { 48 | const defaultErr = { 49 | log: 'Express error handler caught unknown middleware error', 50 | status: 500, 51 | message: { err: 'An Error occurred' } 52 | }; 53 | const errorObj = Object.assign({}, defaultErr, err); 54 | console.log(errorObj.log); 55 | return res.status(errorObj.status).json(errorObj.message); 56 | }); 57 | 58 | app.listen(PORT, () => { 59 | console.log(`Server started on port ${PORT}`); 60 | }); 61 | 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QueryFlow", 3 | "author": "Vivek Patel, George 'KING' Greer, Niko Amescua, Philip Brown, Ryan Campbell", 4 | "private": true, 5 | "version": "0.0.0", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "concurrently \"vite\" \"nodemon server/server.mjs\"", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "check": "svelte-check --tsconfig ./tsconfig.json" 12 | }, 13 | "devDependencies": { 14 | "@floating-ui/dom": "^1.4.2", 15 | "@sveltejs/vite-plugin-svelte": "^2.4.2", 16 | "@tsconfig/svelte": "^4.0.1", 17 | "@types/d3": "^7.4.0", 18 | "@types/jest": "^29.5.3", 19 | "@types/js-cookie": "^3.0.3", 20 | "@typescript-eslint/eslint-plugin": "^5.60.1", 21 | "@typescript-eslint/parser": "^5.60.1", 22 | "autoprefixer": "^10.4.14", 23 | "concurrently": "^8.2.0", 24 | "cypress": "^12.17.1", 25 | "daisyui": "^3.1.7", 26 | "eslint": "^8.43.0", 27 | "eslint-config-prettier": "^8.8.0", 28 | "eslint-plugin-svelte": "^2.32.0", 29 | "highlight.js": "^11.8.0", 30 | "jest": "^29.5.0", 31 | "prettier": "^2.8.8", 32 | "prettier-plugin-svelte": "^2.10.1", 33 | "sass": "^1.63.6", 34 | "svelte": "^3.58.0", 35 | "svelte-check": "^3.3.1", 36 | "tailwindcss": "^3.3.2", 37 | "tslib": "^2.5.0", 38 | "typescript": "^5.0.2", 39 | "vite": "^4.3.9" 40 | }, 41 | "dependencies": { 42 | "@popperjs/core": "^2.11.8", 43 | "@tailwindcss/typography": "^0.5.9", 44 | "bcrypt": "^5.1.0", 45 | "bcryptjs": "^2.4.3", 46 | "body-parser": "^1.20.2", 47 | "compression": "^1.7.4", 48 | "cookie-parser": "^1.4.6", 49 | "cors": "^2.8.5", 50 | "d3": "^7.8.5", 51 | "dotenv": "^16.3.1", 52 | "express": "^4.18.2", 53 | "flowbite": "^1.7.0", 54 | "flowbite-svelte": "^0.39.2", 55 | "googleapis": "^120.0.0", 56 | "js-cookie": "^3.0.5", 57 | "jsonwebtoken": "^9.0.1", 58 | "jwt-decode": "^3.1.2", 59 | "moment": "^2.29.4", 60 | "nodemon": "^2.0.22", 61 | "pg": "^8.11.1", 62 | "redis": "^4.6.7", 63 | "svelte-routing": "^1.10.0", 64 | "svelte-toasts": "^1.1.2", 65 | "tailwind-merge": "^1.13.2", 66 | "uninstall": "^0.0.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cypress/e2e/queryflow_testing/signup.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Signup tests', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:5173/login'); 6 | cy.get('button').contains('Sign Up').click(); 7 | 8 | }); 9 | 10 | it('displays validation error on empty submission', () => { 11 | cy.get('button[type=submit]').click(); 12 | }); 13 | 14 | it('displays validation error on incorrect input', () => { 15 | cy.get('input[name=email]').type('incorrectemail@test.com'); 16 | cy.get('input[name=password]').type('incorrectpassword{enter}'); 17 | }); 18 | 19 | it('navigates to home on successful login with correct input', () => { 20 | cy.get('input[name=email]').type('correctemail@test.com'); 21 | cy.get('input[name=password]').type('correctpassword{enter}'); 22 | }); 23 | it('displays validation error on mismatched passwords', () => { 24 | cy.get('input#grid-first-name').type('Test'); 25 | cy.get('input#grid-last-name').type('User'); 26 | cy.get('input#email').type('testuser@test.com'); 27 | cy.get('input#password').type('testpassword'); 28 | cy.get('input#confirm-password').type('wrongpassword'); 29 | cy.get('input#terms').check(); 30 | cy.get('button[type=submit]').click(); 31 | }); 32 | 33 | it('displays validation error on terms not agreed', () => { 34 | cy.get('input#grid-first-name').type('Test'); 35 | cy.get('input#grid-last-name').type('User'); 36 | cy.get('input#email').type('testuser@test.com'); 37 | cy.get('input#password').type('testpassword'); 38 | cy.get('input#confirm-password').type('testpassword'); 39 | 40 | cy.get('button[type=submit]').click(); 41 | }); 42 | 43 | it('displays validation error on already existing email', () => { 44 | cy.get('input#grid-first-name').type('Test'); 45 | cy.get('input#grid-last-name').type('User'); 46 | cy.get('input#email').type('existingemail@test.com'); 47 | cy.get('input#password').type('testpassword'); 48 | cy.get('input#confirm-password').type('testpassword'); 49 | cy.get('input#terms').check(); 50 | 51 | cy.get('button[type=submit]').click(); 52 | }); 53 | 54 | it('navigates to home on successful signup', () => { 55 | cy.get('input#grid-first-name').type('Test'); 56 | cy.get('input#grid-last-name').type('User'); 57 | cy.get('input#email').type('testuser@test.com'); 58 | cy.get('input#password').type('testpassword'); 59 | cy.get('input#confirm-password').type('testpassword'); 60 | cy.get('input#terms').check(); 61 | cy.get('button[type=submit]').click(); 62 | }); 63 | }); -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Heebo:wght@600&family=Sarabun:wght@600&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code&display=swap'); 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | /* reset app div sizing */ 8 | #app { 9 | margin: 0; 10 | padding: 0; 11 | min-height: 100vh; 12 | } 13 | 14 | /* body full screen height */ 15 | body { 16 | min-height: 100vh; 17 | min-width: 100vw; 18 | } 19 | .range { 20 | --range-shdw: var(--bc) !important; 21 | } 22 | 23 | /* body no scrollbar */ 24 | body::-webkit-scrollbar { 25 | display: none; 26 | } 27 | 28 | /* dot styles/colors */ 29 | 30 | .dot.A { 31 | fill: #1245a8; 32 | } 33 | 34 | .dot.B { 35 | fill: #efce00; 36 | } 37 | 38 | .dot.C { 39 | fill: green; 40 | } 41 | 42 | /* bar styles/colors */ 43 | .bar { 44 | fill-opacity: 0.7; 45 | } 46 | 47 | .bar.A { 48 | fill: #1245a8; 49 | } 50 | 51 | .bar.B { 52 | fill: #efce00; 53 | } 54 | 55 | .bar.C { 56 | fill: green; 57 | } 58 | 59 | /* navbar 'QueryFlow' text styles */ 60 | .logo-text { 61 | font-family: 'Sarabun', sans-serif; 62 | font-size: 25px; 63 | } 64 | 65 | /* NPM Package text style */ 66 | .codeText { 67 | font-family: 'Fira Code', monospace; 68 | } 69 | 70 | /* card graph axis label styles */ 71 | .axis-label { 72 | font-size: 12px; 73 | font-weight: bold; 74 | color: white !important; 75 | } 76 | 77 | /* card hover tooltip styles */ 78 | .tooltip { 79 | background-color: rgba(0, 0, 0, 0.7); 80 | color: white; 81 | padding: 7px; 82 | font-size: 12px; 83 | } 84 | 85 | /* class to hide scrollbars */ 86 | .scrollbar-hide::-webkit-scrollbar { 87 | display: none; 88 | } 89 | 90 | /* for IE, Edge and Firefox */ 91 | .scrollbar-hide { 92 | -ms-overflow-style: none; /* IE and Edge */ 93 | scrollbar-width: none; /* Firefox */ 94 | } 95 | 96 | /* query-string code box no indentation in the beginning */ 97 | pre { 98 | text-indent: -1.2em; 99 | } 100 | 101 | /* decrease padding in between the table columns */ 102 | th, 103 | td { 104 | padding-left: 5px !important; 105 | padding-right: 5px !important; 106 | } 107 | 108 | article > h1 { 109 | text-align: center; 110 | } 111 | 112 | /* responsiveness for different media types */ 113 | @media only screen and (max-width: 667px) { 114 | /* hide the group bar graph on the bottom left */ 115 | .group-query { 116 | display: none; 117 | } 118 | } 119 | @media only screen and (min-width: 768px) and (max-width: 1300px) { 120 | /* hide the group bar graph on the bottom left */ 121 | .group-query { 122 | display: none; 123 | } 124 | } 125 | 126 | article > h1 { 127 | text-align: center; 128 | } 129 | ::-webkit-scrollbar { 130 | width: 5px; /* Adjust as per your preference */ 131 | } 132 | 133 | /* Handle */ 134 | ::-webkit-scrollbar-thumb { 135 | background: #888; /* Change the color as you wish */ 136 | } 137 | 138 | /* Handle on hover */ 139 | ::-webkit-scrollbar-thumb:hover { 140 | background: #555; /* Change the color as you wish */ 141 | } 142 | -------------------------------------------------------------------------------- /server/controllers/ourDBController.js: -------------------------------------------------------------------------------- 1 | import ourDBModel from '../models/ourDBModel.mjs'; 2 | import moment from 'moment'; 3 | 4 | const ourDBController = {}; 5 | 6 | // Pushes queries into the database 7 | ourDBController.queryPush = async (req, res, next) => { 8 | const { _id, queryString, queryName, queryDelay, queryCount, queryMetrics, averageTime } = res.locals.metrics; 9 | const stringQueryMetrics = JSON.stringify(queryMetrics); 10 | const createdAt = moment().format('MMMM Do, YYYY, h:mma Z'); 11 | const string = { text: `INSERT INTO metrics (queryString, queryMetrics, queryName, queryCount, queryDelay, averageTime, users_id, createdAt) 12 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8);`, values: [queryString, stringQueryMetrics, queryName, queryCount, queryDelay, averageTime, _id, createdAt] }; 13 | try { 14 | 15 | await ourDBModel(string); 16 | res.locals.metrics.createdAt = createdAt; 17 | return next(); 18 | } catch (err) { 19 | return next({ 20 | log: 'Error handler caught error in ourDBController.queryPush middleware', 21 | status: 400, 22 | message: 'An error occurred while pushing a query into the primary database', 23 | }); 24 | } 25 | }; 26 | 27 | // Gets query data for the user 28 | ourDBController.queryGet = async (req, res, next) => { 29 | const { _id } = req.user; 30 | 31 | const string = {text: 'SELECT * FROM metrics WHERE users_id = $1', values:[_id] }; 32 | try { 33 | const result = await ourDBModel(string); 34 | const resultData = result.rows; 35 | const returnDataMetrics = resultData.map(({ users_id, ...rest }) => rest); 36 | res.locals.getMetrics = returnDataMetrics; 37 | 38 | return next(); 39 | } catch (err) { 40 | return next({ 41 | log: 'Error handler caught error in ourDBController.queryGet middleware', 42 | status: 400, 43 | message: 'An error occurred while fetching query data', 44 | }); 45 | } 46 | }; 47 | 48 | // Deletes queries by their id 49 | ourDBController.deleteQueryById = async (req, res, next) => { 50 | const { _id } = req.body; 51 | 52 | const userId = req.user._id; 53 | 54 | const string = {text: 'DELETE FROM metrics WHERE _id = $1 AND users_id = $2', values:[_id,userId] }; 55 | try { 56 | const result = await ourDBModel(string); 57 | 58 | return next(); 59 | } catch (err) { 60 | return next({ 61 | log: 'Error handler caught error in ourDBController.deleteQueryById middleware', 62 | status: 400, 63 | message: 'Error handler caught error in ourDBController.deleteQueryById middleware', 64 | }); 65 | } 66 | }; 67 | 68 | // ===== Unused middleware, preserving for later use if needed ===== 69 | // Deletes queries by their name 70 | // ourDBController.deleteQueryByName = async (req, res, next) => { 71 | // const { queryName } = req.body; 72 | // const string = {text: 'DELETE FROM metrics WHERE queryName = $1', values:[queryName] }; 73 | // try { 74 | // const result = await ourDBModel(string); 75 | // return next(); 76 | // } catch (err) { 77 | // return next({ 78 | // log: 'Error handler caught error in ourDBController.deleteQueryByName middleware', 79 | // status: 400, 80 | // message: 'Error handler caught error in ourDBController.deleteQueryByName middleware', 81 | // }); 82 | // } 83 | // }; 84 | 85 | export default ourDBController; -------------------------------------------------------------------------------- /src/lib/RedisForm.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 |
44 |

Redis vs. PostgreSQL Performance

45 |
46 |
getRedisData()}> 47 |
48 | 51 | 59 |
60 |
61 |
62 | 63 |