├── Dockerfile ├── .DS_Store ├── dump.rdb ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── mstile-150x150.png ├── favicon_package_v0.16.zip ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── favicon_package_v0.16.zip:Zone.Identifier ├── browserconfig.xml ├── uploads │ └── NF.sql ├── site.webmanifest └── safari-pinned-tab.svg ├── src ├── index.jsx ├── components │ ├── VerticalBar.jsx │ ├── SchemaButton.jsx │ ├── Heading.jsx │ ├── SchemaButtonsContainer.jsx │ ├── MetricsVisualizer.jsx │ └── SchemaSelector.jsx ├── modules │ ├── services.js │ └── barDataOptions.js ├── index.html.ejs ├── App.jsx └── stylesheets │ └── index.css ├── .eslintrc.json ├── babel.config.js ├── LICENSE ├── docker-compose.yml ├── .gitignore ├── package.json ├── webpack.config.js ├── README.md ├── redis └── redis-commands.js └── server.js /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.10.0 2 | WORKDIR /usr/src/app -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/.DS_Store -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/dump.rdb -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicon_package_v0.16.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/favicon_package_v0.16.zip -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SpeQL8/HEAD/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "./stylesheets/index.css"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /public/favicon_package_v0.16.zip:Zone.Identifier: -------------------------------------------------------------------------------- 1 | [ZoneTransfer] 2 | ZoneId=3 3 | ReferrerUrl=https://realfavicongenerator.net/favicon_result?file_id=p1f6kno4cdsvl1a2q1jjg1u9nn706 4 | HostUrl=https://realfavicongenerator.net/files/ab081870eb670b7eb584ea1369906e5f12a999c3/favicon_package_v0.16.zip 5 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "plugin:node/recommended"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "no-unused-vars": "warn", 7 | "no-console": "off", 8 | "func-names": "off", 9 | "no-process-exit": "off", 10 | "object-shorthand": "off", 11 | "class-methods-use-this": "off" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceMaps: true, 3 | presets: [ 4 | [ 5 | require.resolve('@babel/preset-env'), 6 | { 7 | targets: 'latest 2 versions', 8 | modules: false, 9 | }, 10 | ], 11 | require.resolve('@babel/preset-react'), 12 | ], 13 | plugins: [require.resolve('@babel/plugin-proposal-class-properties')], 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/VerticalBar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Bar } from "react-chartjs-2"; 3 | import { withTheme } from "styled-components"; 4 | 5 | const VerticalBar = (props) => { 6 | const { testData, setTestData } = props; 7 | const { options, setOptions } = props; 8 | return ; 9 | }; 10 | 11 | export default VerticalBar; 12 | -------------------------------------------------------------------------------- /src/components/SchemaButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SchemaButton = (props) => { 4 | const { className, id, onClick, value } = props; 5 | 6 | return ( 7 |
  • 8 | 11 |
  • 12 | ); 13 | }; 14 | 15 | export default SchemaButton; 16 | -------------------------------------------------------------------------------- /public/uploads/NF.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE movies ( 3 | "movie" varchar NOT NULL, 4 | "year" smallint, 5 | "genre" varchar, 6 | "duration" smallint, 7 | "rating" decimal(2,1), 8 | "director" varchar NOT NULL, 9 | "director_dob" date, 10 | "director_country" varchar, 11 | "actors" varchar, 12 | "theater_name" varchar, 13 | "theater_address" varchar, 14 | "theater_state" varchar, 15 | "theater_zip" smallint, 16 | "date" date, 17 | "time" time 18 | ); -------------------------------------------------------------------------------- /src/modules/services.js: -------------------------------------------------------------------------------- 1 | const services = [ 2 | { 3 | label: 'SWAPI', 4 | db_uri: 'postgres://wkydcwrh:iLsy9WNRsMy_LVodJG9Uxs9PARNbiBLb@queenie.db.elephantsql.com:5432/wkydcwrh', 5 | port: 4000, 6 | fromFile: false 7 | }, 8 | { 9 | label: 'Users', 10 | db_uri: 'postgres://dgpvvmbt:JzsdBZGdpT1l5DfQz0hfz0iT7BrKgxhr@queenie.db.elephantsql.com:5432/dgpvvmbt', 11 | port: 4001, 12 | fromFile: false 13 | }, 14 | ] 15 | 16 | exports.services = services; -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Heading.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Heading() { 4 | return ( 5 |
    6 |

    S P E Q L 8

    7 |

    ← grow with confidence →

    8 |
    9 | 14 |
    15 |
    16 | ); 17 | } 18 | 19 | export default Heading; 20 | -------------------------------------------------------------------------------- /src/components/SchemaButtonsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SchemaButton from "./SchemaButton"; 3 | 4 | const SchemaButtonsContainer = (props) => { 5 | const { schemaList } = props; 6 | const { handleSchemaButtonClick } = props; 7 | 8 | const schemaButtonList = schemaList.map((item, index) => { 9 | return ( 10 | 17 | ); 18 | }); 19 | 20 | return
      {schemaButtonList}
    ; 21 | }; 22 | 23 | export default SchemaButtonsContainer; 24 | -------------------------------------------------------------------------------- /src/components/MetricsVisualizer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import VerticalBar from "./VerticalBar"; 3 | 4 | const MetricsVisualizer = (props) => { 5 | const { lastQuerySpeed } = props; 6 | const { handleSaveClick } = props; 7 | const { handleCacheClick } = props; 8 | const { testData } = props; 9 | const { options } = props; 10 | 11 | return ( 12 |
    13 |
    14 | 15 | 16 |

    Query Response Time

    17 |

    18 | {lastQuerySpeed} 19 | ms 20 |

    21 |
    22 |
    23 | 24 |
    25 |
    26 | ); 27 | }; 28 | 29 | export default MetricsVisualizer; 30 | -------------------------------------------------------------------------------- /src/index.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | SpeQL8 19 | 20 | 21 | 22 |
    23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 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 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | services: 3 | postgres: 4 | restart: always 5 | environment: 6 | - POSTGRES_PASSWORD=password 7 | - POSTGRES_USER=user 8 | - POSTGRES_DB=db 9 | image: postgres:latest 10 | ports: 11 | - 5433:5432 12 | redis: 13 | container_name: redis 14 | image: redis:latest 15 | command: ["redis-server", "--bind", "redis", "--port", "6379"] 16 | speql8: 17 | container_name: speql8 18 | image: speql8:latest 19 | depends_on: 20 | - redis 21 | build: ./ 22 | volumes: 23 | - ./:/usr/src/app 24 | ports: 25 | - 3333:3333 26 | - 4000:4000 27 | - 4001:4001 28 | - 4002:4002 29 | - 4003:4003 30 | - 4004:4004 31 | - 4005:4005 32 | - 4006:4006 33 | - 4007:4007 34 | - 4008:4008 35 | - 4009:4009 36 | - 4010:4010 37 | - 4011:4011 38 | - 4012:4012 39 | - 4013:4013 40 | - 4014:4014 41 | - 4015:4015 42 | - 4016:4016 43 | - 4017:4017 44 | - 4018:4018 45 | - 4019:4019 46 | - 4020:4020 47 | - 4021:4021 48 | - 4022:4022 49 | - 4032:4023 50 | environment: 51 | - NODE_ENV=production 52 | - PORT=3333 53 | command: sh -c 'npm i && npm run build && npm start' 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speql8", 3 | "version": "1.0.2", 4 | "description": "Speculative GraphQL metrics for your Postgres database", 5 | "main": "index.jsx", 6 | "scripts": { 7 | "start": "NODE_ENV=production node ./server.js", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "NODE_ENV=development webpack serve --open & NODE_ENV=development nodemon ./server.js", 10 | "speql8": "docker-compose -f docker-compose.yml up" 11 | }, 12 | "author": "Allan MacLean, Ekaterina Vasileva, Russell Hayward, Akiko Hagio Dulaney", 13 | "license": "MIT", 14 | "dependencies": { 15 | "apollo-log": "^1.0.1", 16 | "apollo-server-express": "^2.25.0", 17 | "chart.js": "2.9.4", 18 | "cors": "^2.8.5", 19 | "express": "^4.17.1", 20 | "file-loader": "^6.2.0", 21 | "graphiql": "^1.4.1", 22 | "graphql": "^15.4.0-experimental-stream-defer.1", 23 | "ioredis": "^4.27.2", 24 | "multer": "^1.4.2", 25 | "pg": "^8.6.0", 26 | "postgraphile": "^4.12.1", 27 | "postgraphile-apollo-server": "^0.1.1", 28 | "react": "^17.0.2", 29 | "react-chartjs-2": "2.11.1", 30 | "react-hot-loader": "^4.13.0", 31 | "regenerator-runtime": "^0.13.7", 32 | "shelljs": "^0.8.4", 33 | "socket.io": "^4.1.2", 34 | "socket.io-client": "^4.1.2", 35 | "styled-chart": "^1.3.5", 36 | "styled-components": "^5.3.0", 37 | "svg-inline-loader": "^0.8.2" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.14.3", 41 | "@babel/plugin-proposal-class-properties": "^7.13.0", 42 | "@babel/preset-env": "^7.14.2", 43 | "@babel/preset-react": "^7.13.13", 44 | "@webpack-cli/serve": "^1.4.0", 45 | "babel-loader": "^8.2.2", 46 | "cross-env": "^7.0.3", 47 | "css-loader": "^5.2.6", 48 | "eslint": "^7.27.0", 49 | "eslint-config-airbnb": "^18.2.1", 50 | "eslint-config-node": "^4.1.0", 51 | "eslint-config-prettier": "^8.3.0", 52 | "eslint-plugin-import": "^2.23.4", 53 | "eslint-plugin-jsx-a11y": "^6.4.1", 54 | "eslint-plugin-node": "^11.1.0", 55 | "eslint-plugin-prettier": "^3.4.0", 56 | "eslint-plugin-react": "^7.24.0", 57 | "eslint-plugin-react-hooks": "^1.7.0", 58 | "html-webpack-plugin": "^5.3.1", 59 | "nodemon": "^2.0.7", 60 | "prettier": "^2.3.0", 61 | "react-dom": "^17.0.2", 62 | "style-loader": "^2.0.0", 63 | "webpack": "^5.38.0", 64 | "webpack-cli": "^4.7.0", 65 | "webpack-dev-server": "^3.11.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/modules/barDataOptions.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | labels: [], 3 | datasets: [ 4 | { 5 | label: "Time in ms", 6 | data: [], 7 | queries: [], 8 | cacheTime: [], 9 | backgroundColor: [ 10 | "rgba(255, 99, 132, 0.2)", 11 | "rgba(54, 162, 235, 0.2)", 12 | "rgba(255, 206, 86, 0.2)", 13 | "rgba(75, 192, 192, 0.2)", 14 | "rgba(153, 102, 255, 0.2)", 15 | "rgba(255, 159, 64, 0.2)", 16 | "rgba(255, 99, 132, 0.2)", 17 | "rgba(54, 162, 235, 0.2)", 18 | "rgba(255, 206, 86, 0.2)", 19 | "rgba(75, 192, 192, 0.2)", 20 | "rgba(153, 102, 255, 0.2)", 21 | "rgba(255, 159, 64, 0.2)", 22 | "rgba(255, 99, 132, 0.2)", 23 | "rgba(54, 162, 235, 0.2)", 24 | "rgba(255, 206, 86, 0.2)", 25 | "rgba(75, 192, 192, 0.2)", 26 | "rgba(153, 102, 255, 0.2)", 27 | "rgba(255, 159, 64, 0.2)", 28 | ], 29 | borderColor: [ 30 | "rgba(255, 99, 132, 1)", 31 | "rgba(54, 162, 235, 1)", 32 | "rgba(255, 206, 86, 1)", 33 | "rgba(75, 192, 192, 1)", 34 | "rgba(153, 102, 255, 1)", 35 | "rgba(255, 159, 64, 1)", 36 | "rgba(255, 99, 132, 1)", 37 | "rgba(54, 162, 235, 1)", 38 | "rgba(255, 206, 86, 1)", 39 | "rgba(75, 192, 192, 1)", 40 | "rgba(153, 102, 255, 1)", 41 | "rgba(255, 159, 64, 1)", 42 | "rgba(255, 99, 132, 1)", 43 | "rgba(54, 162, 235, 1)", 44 | "rgba(255, 206, 86, 1)", 45 | "rgba(75, 192, 192, 1)", 46 | "rgba(153, 102, 255, 1)", 47 | "rgba(255, 159, 64, 1)", 48 | ], 49 | borderWidth: 1, 50 | }, 51 | ], 52 | }; 53 | 54 | const defaultOptions = { 55 | tooltips: { 56 | callbacks: { 57 | afterLabel: function (tooltipItem, data) { 58 | return [ 59 | ...data.datasets[0].queries, 60 | data.datasets[0].cacheTime[tooltipItem.index], 61 | ]; 62 | }, 63 | }, 64 | }, 65 | scales: { 66 | xAxes: [ 67 | { 68 | ticks: { 69 | beginAtZero: true, 70 | fontColor: "white", 71 | }, 72 | }, 73 | ], 74 | yAxes: [ 75 | { 76 | ticks: { 77 | beginAtZero: true, 78 | fontColor: "white", 79 | }, 80 | }, 81 | ], 82 | }, 83 | legend: { 84 | labels: { 85 | fontColor: "white", 86 | }, 87 | }, 88 | }; 89 | 90 | exports.data = data; 91 | exports.defaultOptions = defaultOptions; 92 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | const isDev = process.env.NODE_ENV === 'development'; 4 | 5 | module.exports = { 6 | entry: isDev 7 | ? [ 8 | 'react-hot-loader/patch', // activate HMR for React 9 | 'webpack-dev-server/client?http://localhost:8080', // bundle the client for webpack-dev-server and connect to the provided endpoint 10 | 'webpack/hot/only-dev-server', // bundle the client for hot reloading, only- means to only hot reload for successful updates 11 | './index.jsx', // the entry point of our app 12 | ] 13 | : './index.jsx', 14 | context: path.resolve(__dirname, './src'), 15 | mode: 'development', 16 | devtool: 'inline-source-map', 17 | performance: { 18 | hints: false, 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.html$/, 24 | use: ['file?name=[name].[ext]'], 25 | }, 26 | 27 | // for graphql module, which uses mjs still 28 | // { 29 | // type: 'javascript/auto', 30 | // test: /\.mjs$/, 31 | // use: [], 32 | // include: /node_modules/, 33 | // }, 34 | { 35 | test: /\.(js|jsx)$/, 36 | use: [ 37 | { 38 | loader: 'babel-loader', 39 | options: { 40 | presets: [ 41 | ['@babel/preset-env', { modules: false }], 42 | '@babel/preset-react', 43 | ], 44 | }, 45 | }, 46 | ], 47 | }, 48 | 49 | 50 | { 51 | test: /\.css$/, 52 | use: ['style-loader', 'css-loader'], 53 | }, 54 | { 55 | test: /\.svg$/, 56 | use: [{ loader: 'svg-inline-loader' }], 57 | }, 58 | { 59 | test: /\.(woff|woff2|eot|ttf|otf)$/, 60 | use: ['file-loader'], 61 | }, 62 | { 63 | test: /\.(png|jpe?g|gif)$/i, 64 | loader: 'file-loader', 65 | options: { 66 | name: '[path][name].[ext]', 67 | }, 68 | }, 69 | ], 70 | }, 71 | resolve: { 72 | extensions: ['.js', '.json', '.jsx', '.css', '.mjs'], 73 | }, 74 | plugins: [ 75 | new HtmlWebpackPlugin({ 76 | template: 'index.html.ejs', 77 | }), 78 | ], 79 | devServer: { 80 | hot: true, 81 | // bypass simple localhost CORS restrictions by setting 82 | // these to 127.0.0.1 in /etc/hosts 83 | allowedHosts: ['local.test.com', 'graphiql.com'], 84 | }, 85 | // node: { 86 | // //fs: 'empty', 87 | // module: 'empty', 88 | // }, 89 | 90 | resolve: { 91 | extensions: ['.js', '.jsx'], 92 | fallback: { 93 | fs: false 94 | } 95 | } 96 | 97 | }; 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    2 | 3 |

    4 |

    SpeQL8

    5 |

    Speculative GraphQL metrics for your Postgres databases

    6 |

    ✨✨✨

    7 | 8 | ___ 9 | ## About 10 | SpeQL8 enables you to run GraphQL queries on an existing Postgres database and collect request-response metrics per query. View and compare query response times from both your database and from a lightning fast Redis cache, all in the comfort and security of your local development environment. 11 | 12 | Upload a .sql or .tar file to spin up your postgres database from SpeQL8 or simply plug in a Postgres database client URL (e.g. ElephantSQL). 13 | 14 | ___ 15 | ## Get Started With SpeQL8: 16 | You can either spin up locally on your own machine, or inside a Docker container: 17 | 18 | A) Local Install 19 | * Fork and clone this repository 20 | * Ensure you have an instance of Redis Server active. 21 | * Run `npm install && npm build && npm start` 22 | * Open `localhost:3333` 23 | 24 | B) Containerized 25 | * Fork and clone this repository 26 | * In the SpeQL8 root directory, run the command `npm run speql8` 27 | * Open `localhost:3333` 28 | * please note - creating GraphQL API instances from a .sql or .tar file is a forthcoming feature in the containerized version 29 | ___ 30 | ## Prerequisites 31 | Be sure to have PostgreSQL and Docker installed on your local machine before attempting to run SpeQL8 via Dockerfile. If running locally, you'll need Redis CLI & Redis Server installed. 32 | ___ 33 | ## Built With 34 | [Apollo-Server](https://www.apollographql.com/docs/apollo-server/) | [Socket.IO](https://socket.io) | [ioredis](https://docs.redislabs.com/latest/rs/references/client_references/client_ioredis/) | [Postgraphile](https://www.graphile.org/postgraphile/) | [GraphiQL](https://github.com/graphql/graphiql) | [React](https://reactjs.org) | [Node.js](node.js) | [Express](https://expressjs.com) | [Docker-Compose](https://docs.docker.com/compose/) | [PostgreSQL](https://www.postgresql.org) 35 | 36 | Thank you! 37 | ___ 38 | ## Contribute 39 | SpeQL8 is open-source and accepting contributions. If you would like to contribute to SpeQL8, please fork [this repo](https://github.com/oslabs-beta/SpeQL8), add changes to a feature branch, and make a pull request. Thank you for your support and contributions, and don't forget to give us a ⭐! 40 | ___ 41 | ## Maintainers 42 | 🌠 [Allan MacLean](https://github.com/allanmaclean) 43 | 44 | 🌠 [Akiko Hagio Dulaney](https://github.com/akikoinhd) 45 | 46 | 🌠 [Ekaterina Vasileva](https://github.com/vs-kat) 47 | 48 | 🌠 [Russell Hayward](https://github.com/russdawg44) 49 | 50 | ___ 51 | 52 | ## License 53 | This product is released under the MIT License 54 | 55 | This product is accelerated by [OS Labs](https://opensourcelabs.io/). 56 | -------------------------------------------------------------------------------- /redis/redis-commands.js: -------------------------------------------------------------------------------- 1 | // REDIS 2 | const Redis = require("ioredis"); 3 | const redis = new Redis({ host: "redis", port: 6379 }); 4 | 5 | // SOCKET.IO STUFF 6 | let updater = {}; 7 | 8 | 9 | // SOME FUNCTIONS 10 | const addEntry = async (hashCode) => { 11 | await redis.incr("totalEntries"); 12 | const key = await redis.get("totalEntries", async (err, res) => { 13 | if (err) throw err; 14 | else await redis.set(res, hashCode); 15 | }); 16 | return key; 17 | }; 18 | 19 | const timer = (t0, t1) => { 20 | const start = parseInt(t0[0])*1000000 + parseInt(t0[1]); 21 | const stop = parseInt(t1[0])*1000000 + parseInt(t1[1]); 22 | return stop - start; 23 | } 24 | 25 | // EXPRESS MIDDLEWARE 26 | const redisController = {}; 27 | 28 | redisController.serveMetrics = async (req, res, next) => { 29 | 30 | const start = await redis.time(); 31 | redis.hgetall(req.params['hash'], async (err, result) => { 32 | if (err) { 33 | console.log(err); 34 | return next(err); 35 | } else { 36 | const stop = await redis.time(); 37 | result.cacheTime = await timer(start, stop); 38 | res.locals.metrics = result; 39 | 40 | return next(); 41 | } 42 | }); 43 | }; 44 | 45 | // APOLLO SERVER PLUGIN 46 | const cachePlugin = { 47 | requestDidStart(context) { 48 | console.log('cache plugin fired'); 49 | const clientQuery = context.request.query; 50 | const cq = Object.values(clientQuery); 51 | if (cq[11]!=='I'&&cq[12]!=='n'&&cq[13]!=='t'&&cq[14]!=='r'&&cq[15]!=='o'&&cq[16]!=='s'&&cq[17]!=='p'&&cq[18]!=='e') { 52 | return { 53 | async willSendResponse(requestContext) { 54 | // console.log('schemaHash: ' + requestContext.schemaHash); 55 | // console.log('queryHash: ' + requestContext.queryHash); 56 | 57 | console.log('operation: ' + requestContext.errors); 58 | const totalDuration = requestContext.response.extensions.tracing.duration; 59 | 60 | const resolvers = JSON.stringify(requestContext.response.extensions.tracing.execution.resolvers); 61 | const now = Date.now(); 62 | const hash = `${now}-${requestContext.queryHash}` 63 | const timeStamp = new Date().toString(); 64 | await redis.hset(`${hash}`, 'totalDuration', `${totalDuration}`); 65 | 66 | 67 | //....queryBreakdown 68 | await redis.hset(`${hash}`, 'clientQuery', `${clientQuery.toString()}`); 69 | await redis.hset(`${hash}`, 'timeStamp', `${timeStamp}`); 70 | await redis.hset(`${hash}`, `resolvers`, `${resolvers}`); 71 | 72 | addEntry(hash); 73 | 74 | updater.totalDuration = totalDuration; 75 | updater.clientQuery = clientQuery; 76 | updater.hash = hash; 77 | 78 | }, 79 | }; 80 | } else return console.log('Introspection Query Fired'); 81 | } 82 | }; 83 | 84 | 85 | // EXPORT MIDDLEWARE, APOLLO PLUGIN, UPDATER OBJECT 86 | // EXPORT REDIS INSTANCE FOR DOCKER-COMPOSE 87 | module.exports = { redisController, cachePlugin, updater, redis }; 88 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import GraphiQL from "graphiql"; 3 | import "graphiql/graphiql.min.css"; 4 | import "codemirror/theme/material-ocean.css"; 5 | import socketIOClient from "socket.io-client"; 6 | let ENDPOINT = "http://localhost:3333"; 7 | const regeneratorRuntime = require("regenerator-runtime"); 8 | 9 | const servicesModule = require("./modules/services"); 10 | const services = servicesModule.services; 11 | const barDataOptionsModule = require("./modules/barDataOptions"); 12 | const data = barDataOptionsModule.data; 13 | const defaultOptions = barDataOptionsModule.defaultOptions; 14 | 15 | import SchemaSelector from "./components/SchemaSelector"; 16 | import MetricsVisualizer from "./components/MetricsVisualizer"; 17 | import SchemaButtonsContainer from "./components/SchemaButtonsContainer"; 18 | 19 | const App = () => { 20 | const [currentSchema, changeCurrentSchema] = useState(""); 21 | const [schemaList, updateSchemaList] = useState(["SWAPI", "Users"]); 22 | const [fetchURL, setFetchURL] = useState( 23 | `http://localhost:${services[0].port}/graphql` 24 | ); 25 | const [lastQuerySpeed, setLastQuerySpeed] = useState("-"); 26 | const [lastQuery, setLastQuery] = useState(""); 27 | const [currentPort, setCurrentPort] = useState(services[0].port); 28 | const [dataSet, setDataSet] = useState([]); 29 | const [lastHash, setLastHash] = useState(""); 30 | const [testData, setTestData] = useState(data); 31 | const [queryNumber, setQueryNumber] = useState(1); 32 | const [options, setOptions] = useState(defaultOptions); 33 | 34 | useEffect(() => { 35 | const socket = socketIOClient(ENDPOINT); 36 | socket.on("FromAPI", (data) => { 37 | if (typeof data.totalDuration === "number") { 38 | setLastQuerySpeed(Math.round(data.totalDuration / 1000000)); 39 | setLastQuery(data.clientQuery); 40 | setLastHash(data.hash); 41 | } 42 | }); 43 | }, []); 44 | 45 | useEffect(() => { 46 | //this conditional is required to make sure we don't overwrite the default state of fetchURL before a schema has been selected 47 | if (currentSchema !== "") { 48 | let gqlApiString; 49 | let port; 50 | for (let i = 0; i < services.length; i++) { 51 | // console.log(`LABEL for ${i} iteration: ${services[i].label}`); 52 | if (services[i].label === currentSchema) { 53 | port = services[i].port; 54 | gqlApiString = `http://localhost:${port}/graphql`; 55 | break; 56 | } else { 57 | console.log("did not find a matching label"); 58 | gqlApiString = "http://localhost:4000/graphql"; 59 | } 60 | } 61 | setCurrentPort(port); 62 | setFetchURL(gqlApiString); 63 | } 64 | }); 65 | 66 | const handleSchemaButtonClick = (e) => { 67 | e.preventDefault(); 68 | changeCurrentSchema(e.target.innerText); 69 | const allSchemaButtons = document.getElementsByClassName( 70 | "schema-list-element" 71 | ); 72 | console.log(allSchemaButtons.length); 73 | for (let i = 0; i < allSchemaButtons.length; i++) { 74 | allSchemaButtons[i].children[0].classList.remove( 75 | "schema-button-selected" 76 | ); 77 | } 78 | e.target.classList.add("schema-button-selected"); 79 | }; 80 | 81 | const handleSaveClick = () => { 82 | const copy = [...testData.datasets]; 83 | copy[0].data = [...copy[0].data, lastQuerySpeed]; 84 | const queryToString = lastQuery; 85 | console.log(queryToString); 86 | const result = []; 87 | let line = ""; 88 | 89 | const linebreak = "\n"; 90 | for (let i = 0; i < queryToString.length; i++) { 91 | if (queryToString[i] !== linebreak) { 92 | line += queryToString[i]; 93 | } else { 94 | result.push(line); 95 | line = ""; 96 | } 97 | } 98 | console.log(result); 99 | 100 | // console.log(result); 101 | copy[0].queries = result; 102 | copy[0].cacheTime = [...copy[0].cacheTime, ""]; 103 | setTestData((prevState) => ({ 104 | ...prevState, 105 | labels: [...prevState.labels, `Query #${queryNumber}: ${currentSchema}`], 106 | datasets: copy, 107 | })); 108 | setQueryNumber(queryNumber + 1); 109 | }; 110 | 111 | const handleCacheClick = async () => { 112 | const response = await fetch(`http://localhost:4000/redis/${lastHash}`, { 113 | method: "GET", 114 | credentials: "same-origin", 115 | }); 116 | const responseJson = await response.json(); 117 | const cacheTime = responseJson.cacheTime; 118 | const copy = [...testData.datasets]; 119 | copy[0].cacheTime[ 120 | copy[0].cacheTime.length - 1 121 | ] = `Cached Query Response Time: ${cacheTime} microseconds`; 122 | setTestData((prevState) => ({ 123 | ...prevState, 124 | datasets: copy, 125 | })); 126 | }; 127 | 128 | return ( 129 | //this outermost div MUST have the id of 'graphiql' in order for graphiql to render properly 130 |
    131 | 141 | 145 | 155 | { 159 | const data = await fetch(fetchURL, { 160 | method: "POST", 161 | headers: { 162 | Accept: "application/json", 163 | "Content-Type": "application/json", 164 | }, 165 | body: JSON.stringify(graphQLParams), 166 | credentials: "same-origin", 167 | }); 168 | return data.json().catch(() => data.text()); 169 | }} 170 | /> 171 |
    172 | ); 173 | }; 174 | 175 | export default App; 176 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 14 | 24 | 25 | 27 | 29 | 89 | 91 | 94 | 96 | 98 | 99 | 100 | 102 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/components/SchemaSelector.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | const servicesModule = require("../modules/services"); 3 | const services = servicesModule.services; 4 | 5 | import Heading from "./Heading"; 6 | 7 | const schemaDisplay = (props) => { 8 | const [input, inputChange] = useState(""); 9 | const [uriInput, changeUri] = useState(""); 10 | const { currentSchema, changeCurrentSchema } = props; 11 | const { schemaList, updateSchemaList } = props; 12 | const { setFetchURL } = props; 13 | const { currentPort, setCurrentPort } = props; 14 | 15 | function handleDelete(e) { 16 | //hard coding back to SWAPI so GraphiQL does not throw an error due to not having a valid fetch URL 17 | setFetchURL(`http://localhost:${services[0].port}/graphql`); 18 | 19 | fetch(`http://localhost:3333/deleteServer/${currentPort}`, { 20 | method: "DELETE", 21 | mode: "cors", 22 | headers: { 23 | "Content-Type": "application/json", 24 | }, 25 | }).then((data) => { 26 | data.json(); 27 | }); 28 | updateSchemaList( 29 | schemaList.filter((el) => { 30 | return el !== currentSchema; 31 | }) 32 | ); 33 | 34 | changeCurrentSchema(""); 35 | setCurrentPort(4000); 36 | } 37 | 38 | function handleDbUri(e) { 39 | changeUri(e.target.value); 40 | // console.log("this is the value from the db uri box", uriInput); 41 | } 42 | 43 | function handleAdd(e) { 44 | // console.log("this is the event for the add schema buttton", e); 45 | e.preventDefault(); 46 | 47 | if (input !== "" && uriInput !== "") { 48 | let lastAddedPort = services[services.length - 1].port; 49 | // console.log(`last added port is ${lastAddedPort}`); 50 | const newPort = lastAddedPort + 1; 51 | const newService = { 52 | label: input, 53 | db_uri: uriInput, 54 | port: newPort, 55 | }; 56 | services.push(newService); 57 | console.log(services); 58 | 59 | fetch("http://localhost:3333/newServer", { 60 | method: "POST", 61 | mode: "cors", 62 | headers: { 63 | "Content-Type": "application/json", 64 | }, 65 | body: JSON.stringify(newService), 66 | }) 67 | //at which point do we need to intervene in order not to render a dead button? 68 | .then((data) => data.json()) 69 | //can probably get rid of this .then 70 | .then((results) => { 71 | console.log( 72 | "these are the results from the fetch request in the handle add", 73 | results 74 | ); 75 | }) 76 | .catch((err) => { 77 | console.log("DID THIS GET HIT??"); 78 | console.log(err); 79 | }); 80 | 81 | updateSchemaList((prevState) => [...prevState, input]); 82 | inputChange(""); 83 | changeUri(""); 84 | } else { 85 | alert( 86 | "Please ensure 'Schema Name' and 'PostgreSQL URI' fields are populated" 87 | ); 88 | } 89 | } 90 | 91 | function handleSchemaNameChange(e) { 92 | inputChange(e.target.value); 93 | } 94 | 95 | function handleFileSubmit(e) { 96 | e.preventDefault(); 97 | const label = document.getElementById("schemaNameFromFile").value; 98 | const form = document.getElementById("uploadFileForm"); 99 | const formData = new FormData(form); 100 | const file = formData.get("myFile"); 101 | const indexOfDot = file.name.lastIndexOf("."); 102 | const fileExtension = file.name.slice(`${indexOfDot}`); 103 | if (fileExtension !== ".sql" && fileExtension !== ".tar") { 104 | alert("please upload .sql or .tar file"); 105 | return; 106 | } else if (label.trim() === "" || label === "") { 107 | alert("please provide a name for your database"); 108 | return; 109 | } else { 110 | fetch("http://localhost:3333/uploadFile", { 111 | method: "POST", 112 | mode: "cors", 113 | body: formData, 114 | }) 115 | .then((JSONdata) => JSONdata.json()) 116 | .then((data) => services.push(data)) 117 | // .then(() => console.log(services)) 118 | .then(() => updateSchemaList((prevState) => [...prevState, label])); 119 | // .then(() => console.log(schemaList)); 120 | // .then(() => inputChange("")) 121 | // .then(() => changeUri("")); 122 | document.getElementById("schemaNameFromFile").value = ""; 123 | } 124 | } 125 | 126 | //--------------------------------------- 127 | 128 | return ( 129 |
    130 | 131 |
    132 | 133 | Current Schema: 134 |
    135 | {currentSchema === "" && ( 136 | 137 | None selected. Select a schema from the tabs, or add one below 138 | 139 | )} 140 | {currentSchema !== "" && ( 141 | {currentSchema} 142 | )} 143 |
    144 | 145 | {currentSchema !== "" && ( 146 | 149 | )} 150 |
    151 | ✨ ✨ ✨ ✨ ✨ 152 |
    153 | Run with connection string... 154 |
    155 | 158 | 165 | 168 | 175 | 178 |
    179 | ...or upload .sql or .tar file 180 |
    handleFileSubmit(e)} 186 | > 187 | 190 | 195 | 198 | 199 | 207 |
    208 |
    209 |
    210 | ); 211 | }; 212 | 213 | export default schemaDisplay; 214 | -------------------------------------------------------------------------------- /src/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | min-height: 100vh; 6 | min-width: 100vw; 7 | font-family: "Lato", sans-serif; 8 | background-color: #001a17; 9 | background-image: url("https://cdn.discordapp.com/attachments/825520363151556638/847539075778478120/mirrored-squares.png"); 10 | /* This is mostly intended for prototyping; please download the pattern and re-host for production environments. Thank you! */ 11 | position: absolute; 12 | } 13 | 14 | p { 15 | margin: 0; 16 | } 17 | 18 | .selector { 19 | display: grid; 20 | color: #c3e88d; 21 | padding-left: 10px; 22 | padding-right: 10px; 23 | /* grid-column: 1 / 2; */ 24 | grid-template-columns: auto-fill; 25 | grid-row: 1 / 5; 26 | align-items: start; 27 | align-content: start; 28 | background-color: rgba(4, 0, 53, 0.548); 29 | border-right: 4px solid #c792ea; 30 | background-image: url("https://cdn.discordapp.com/attachments/825520363151556638/847147619544989706/SpeQL82.png"); 31 | background-repeat: no-repeat; 32 | background-position: bottom; 33 | background-size: contain; 34 | } 35 | 36 | .selector button { 37 | background: linear-gradient(#657099, #0f111a); 38 | color: #eeffff; 39 | border: none; 40 | border-radius: 3px; 41 | padding: 4px 7px; 42 | } 43 | 44 | #deleteButton { 45 | margin-left: 3px; 46 | } 47 | 48 | .query-speed-box button { 49 | background: rgba(4, 0, 53, 0.548); 50 | color: #eeffff; 51 | border: none; 52 | border-bottom: 1px solid #c792ea; 53 | padding: 10px; 54 | width: 100%; 55 | } 56 | 57 | .query-speed-box button:hover { 58 | cursor: pointer; 59 | color: #ff5370; 60 | } 61 | 62 | #root { 63 | height: 100vh; 64 | } 65 | 66 | h1 h3, 67 | h4, 68 | p { 69 | color: #c3e88d; 70 | } 71 | 72 | #catchPhrase { 73 | font-size: small; 74 | margin-top: -10px; 75 | font-family: "Lato", sans-serif; 76 | } 77 | 78 | #goldThing { 79 | height: 80px; 80 | width: 120px; 81 | margin-left: 9px; 82 | } 83 | 84 | #currentSchema { 85 | margin-right: 3px; 86 | font-family: "Lato", sans-serif; 87 | color: #c3e88d; 88 | } 89 | 90 | div#forms { 91 | display: flex; 92 | flex-direction: column; 93 | max-width: 235px; 94 | } 95 | 96 | form#mainForm { 97 | margin-bottom: 20px; 98 | } 99 | 100 | #formText { 101 | font-size: small; 102 | font-family: "Lato", sans-serif; 103 | } 104 | 105 | #schemaNameFromFile, 106 | #schemaNameFromDbUri, 107 | #dbUri { 108 | border-radius: 6px; 109 | border: 1px black solid; 110 | background-color: #daf5ff; 111 | } 112 | 113 | form input { 114 | margin-bottom: 5px; 115 | } 116 | 117 | form label { 118 | margin-bottom: 2px; 119 | } 120 | 121 | input, 122 | label { 123 | display: block; 124 | } 125 | 126 | .inputDiv { 127 | align-self: center; 128 | max-width: 235px; 129 | height: 60px; 130 | } 131 | 132 | /* test comment */ 133 | 134 | .main-container { 135 | position: relative; 136 | display: grid; 137 | grid-template-columns: [first] 250px [line2] 40px [line3] auto [end]; 138 | grid-template-rows: [top] 40px [row1-end] auto [row2-end] 30px [graphiql-top] 300px [bottom]; 139 | height: 100%; 140 | min-width: 100%; 141 | } 142 | 143 | .query-speed-box { 144 | border: 1px solid #c792ea; 145 | height: auto; 146 | width: 150px; 147 | text-align: center; 148 | margin: 0 auto; 149 | /* margin-top: 10rem; */ 150 | } 151 | 152 | h4 { 153 | margin: 0; 154 | background-color: #0f111a; 155 | } 156 | 157 | .query-speed-box p { 158 | background-color: #0f111a; 159 | height: auto; 160 | margin: 0; 161 | } 162 | 163 | .heading { 164 | font-family: "Poiret One"; 165 | text-align: center; 166 | align-content: center; 167 | max-width: 235px; 168 | } 169 | 170 | p { 171 | font-size: 2rem; 172 | } 173 | 174 | .milliseconds-display { 175 | font-size: 1rem; 176 | } 177 | 178 | .column-chart { 179 | display: inline-block; 180 | width: 100%; 181 | } 182 | 183 | .label-text { 184 | color: #89ddff; 185 | } 186 | 187 | .input-div { 188 | color: #89ddff; 189 | } 190 | 191 | li { 192 | display: inline; 193 | } 194 | 195 | .schemaInput { 196 | color: #89ddff; 197 | margin: 0; 198 | padding: 0; 199 | } 200 | 201 | .schema-button-list { 202 | display: grid; 203 | grid-column: 2 / 3; 204 | grid-row: 1 / 2; 205 | justify-content: end; 206 | } 207 | 208 | /* naming is confusing here - suggest changing schemaButton to 'schema-list-element' */ 209 | 210 | .schema-list-element { 211 | display: grid; 212 | transform: rotate(90deg); 213 | height: auto; 214 | width: auto; 215 | padding-bottom: 20px; 216 | margin-bottom: 15%; 217 | } 218 | 219 | .schema-button { 220 | border: 2px solid #c792ea; 221 | border-top-left-radius: 20px; 222 | border-top-right-radius: 20px; 223 | color: #c3e88d; 224 | background-color: rgba(4, 0, 53, 0.548); 225 | font-size: medium; 226 | padding-top: 10px; 227 | padding-bottom: 5px; 228 | } 229 | 230 | .schema-button-selected { 231 | border: 2px solid red; 232 | } 233 | 234 | .graphiql-container { 235 | /* display: grid; */ 236 | grid-column: 2 / 4; 237 | grid-row: 4 / 5; 238 | margin-top: 1vh; 239 | position: relative; 240 | } 241 | 242 | .graphiql-container { 243 | color: #c3e88d; 244 | font-family: "Lato", sans-serif; 245 | } 246 | 247 | div.editorWrap { 248 | overflow-x: scroll; 249 | } 250 | 251 | div.graphiql-container div.topBar { 252 | background: linear-gradient(#090d33, #000); 253 | border-bottom: 1px solid goldenrod; 254 | } 255 | 256 | div.topBar div.title { 257 | font-size: 28px; 258 | font-family: "Poiret One"; 259 | } 260 | 261 | div.execute-button-wrap button.execute-button { 262 | background: linear-gradient(#657099, #0f111a); 263 | box-shadow: none; 264 | border: 1px solid #0f111a; 265 | fill: #eeffff; 266 | } 267 | 268 | div.toolbar button.toolbar-button { 269 | background: linear-gradient(#657099, #0f111a); 270 | color: #eeffff; 271 | box-shadow: none; 272 | } 273 | 274 | .graphiql-container .docExplorerShow { 275 | background: linear-gradient(#090d33, #000); 276 | border-bottom: 1px solid goldenrod; 277 | color: #c3e88d; 278 | } 279 | 280 | .graphiql-container .resultWrap { 281 | border-left: 1px solid goldenrod; 282 | } 283 | 284 | .graphiql-container .result-window .CodeMirror-gutters { 285 | background: linear-gradient(#090d33, #000); 286 | color: #eeffff; 287 | border-color: goldenrod; 288 | } 289 | 290 | .graphiql-container .secondary-editor-title { 291 | background: linear-gradient(#090d33, #000); 292 | border-bottom: 1px solid goldenrod; 293 | border-top: none; 294 | font-variant: small-caps; 295 | font-weight: 500; 296 | letter-spacing: 1px; 297 | } 298 | 299 | .graphiql-container .secondary-editor-title div { 300 | color: #89ddff !important; 301 | } 302 | 303 | .graphiql-container .docExplorerWrap, 304 | .graphiql-container .historyPaneWrap { 305 | background: #0f111a; 306 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); 307 | border-right: 1px solid goldenrod; 308 | } 309 | 310 | .graphiql-container .doc-explorer { 311 | background: linear-gradient(#090d33, #000); 312 | font-family: "Poiret One"; 313 | /* font-variant: small-caps; */ 314 | letter-spacing: 1px; 315 | font-size: 20px; 316 | } 317 | 318 | .graphiql-container button, 319 | .graphiql-container input { 320 | color: #ff5370; 321 | } 322 | 323 | .graphiql-container .doc-explorer-contents, 324 | .graphiql-container .history-contents { 325 | background-color: #0f111a; 326 | border-top: 1px solid goldenrod; 327 | } 328 | 329 | .graphiql-container .search-box { 330 | border-bottom: 1px solid #c3e88d; 331 | } 332 | 333 | .graphiql-container .search-box > input { 334 | background-color: #0f111a; 335 | } 336 | 337 | .vertical-bar { 338 | margin: 0 auto; 339 | /* margin-top: 3rem; */ 340 | } 341 | 342 | .sparkle-hr { 343 | text-align: center; 344 | margin: 8px 0; 345 | max-width: 235px; 346 | } 347 | 348 | .no-schema-selected-prompt { 349 | font-size: medium; 350 | } 351 | 352 | .none-selected-span { 353 | color: #3b6b7e; 354 | margin: 0; 355 | padding: 0; 356 | } 357 | 358 | .none-selected-text { 359 | color: #c3e88d; 360 | } 361 | 362 | .metrics { 363 | grid-column: 3/4; 364 | grid-row: 2/3; 365 | position: relative; 366 | max-width: 500px; 367 | } 368 | 369 | .chartjs-render-monitor { 370 | max-height: 50vh !important; 371 | /* width: 90vh !important; */ 372 | margin: 0 auto; 373 | } 374 | 375 | @media screen and (max-height: 950px) { 376 | .query-speed-box { 377 | border: 1px solid #c792ea; 378 | height: 171px; 379 | width: 100px; 380 | text-align: center; 381 | position: absolute; 382 | bottom: 0px; 383 | left: 0px; 384 | font-size: 0.75rem; 385 | } 386 | 387 | .milliseconds-display { 388 | font-size: 0.75rem; 389 | } 390 | 391 | .chartjs-render-monitor { 392 | font-size: 10px !important; 393 | position: absolute !important; 394 | bottom: 0px; 395 | margin-left: 135px; 396 | } 397 | 398 | .chartjs-render-monitor { 399 | position: absolute !important; 400 | bottom: 0px; 401 | max-width: calc(100% - 135px); 402 | margin-left: 135px; 403 | } 404 | 405 | 406 | 407 | } 408 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const http = require("http"); 3 | 4 | const pg = require("pg"); 5 | const path = require("path"); 6 | const { ApolloServer } = require("apollo-server-express"); 7 | const { makeSchemaAndPlugin } = require("postgraphile-apollo-server"); 8 | const { ApolloLogPlugin } = require("apollo-log"); 9 | const cors = require("cors"); 10 | const util = require("util"); 11 | const multer = require("multer"); 12 | const fs = require("fs"); 13 | const servicesModule = require("./src/modules/services"); 14 | const exec = util.promisify(require("child_process").exec); 15 | 16 | const { services } = servicesModule; 17 | const upload = multer({ dest: `${__dirname}/public/uploads/` }); 18 | 19 | // EXPRESS SERVER + CORS 20 | const app = express(); 21 | app.use(express.static("dist")); 22 | app.use(express.json()); 23 | app.use(express.urlencoded({ extended: true })); 24 | app.use(cors()); 25 | 26 | // DYNAMIC SERVER SWITCHING 27 | app.post("/newServer", (req, res) => { 28 | console.log("inside the /newServer route"); 29 | console.log(req.body); 30 | // please note - logging services on the backend will not be accurate! 31 | // console.log(services); 32 | createNewApolloServer(req.body) 33 | .then((data) => myServers.push(data)) 34 | .catch((err) => console.log(err)); 35 | }); 36 | 37 | app.post( 38 | "/uploadFile", 39 | upload.single("myFile"), 40 | (req, res, next) => { 41 | // console.log("FILE", req.file); 42 | // console.log("BODY", req.body); 43 | fs.renameSync( 44 | req.file.destination + req.file.filename, 45 | req.file.destination + req.file.originalname 46 | ); 47 | // req.file.filename = req.file.originalname; 48 | req.fileExtension = req.file.originalname.slice(-4); 49 | req.p = req.file.destination + req.file.originalname; 50 | req.label = JSON.stringify(req.body).slice(26, -2); 51 | next(); 52 | }, 53 | async (req, res, next) => { 54 | const promisify = async (cmd) => { 55 | try { 56 | const { stdout, stderr } = await exec(cmd); 57 | // console.log("stdout:", stdout); 58 | // console.log("stderr:", stderr); 59 | } catch (e) { 60 | console.error(e); 61 | } 62 | }; 63 | 64 | await promisify(`createdb -U postgres '${req.label}'`); 65 | if (req.fileExtension === ".sql") { 66 | await promisify(`psql -U postgres -d ${req.label} < '${req.p}'`); 67 | } else if (req.fileExtension === ".tar") { 68 | await promisify(`pg_restore -U postgres -d ${req.label} < '${req.p}'`); 69 | } 70 | next(); 71 | }, 72 | 73 | async (req, res, next) => { 74 | const port = services[services.length - 1].port + 1; 75 | 76 | const newServiceFromFile = { 77 | label: req.label, 78 | db_uri: `postgres:///${req.label}`, 79 | port: port, 80 | fromFile: req.file.originalname, 81 | }; 82 | 83 | services.push(newServiceFromFile); 84 | 85 | console.log("new service", newServiceFromFile); 86 | 87 | createNewApolloServer(newServiceFromFile).then((result) => 88 | myServers.push(result) 89 | ); 90 | res.locals.service = newServiceFromFile; 91 | res.status(200).json(res.locals.service); 92 | } 93 | ); 94 | 95 | app.delete("/deleteServer/:port", (req, res) => { 96 | // console.log("***IN DELETE****"); 97 | console.log(services); 98 | const myPort = req.params.port; 99 | // console.log('myPort', myPort); 100 | const connectionKey = `6::::${myPort}`; 101 | myServers.forEach(async (server) => { 102 | if (myPort == 4000) { 103 | console.log( 104 | "You may not close port 4000. Graphiql must be provided an active GraphQL API (of which there will always be one running on 4000)" 105 | ); 106 | } else if (server._connectionKey == connectionKey) { 107 | console.log(`server on ${myPort} is about to be shut down`); 108 | await server.close(); 109 | } else { 110 | console.log("nothing got hit!"); 111 | } 112 | }); 113 | // console.log(services); 114 | 115 | services.forEach(async (service, index) => { 116 | console.log("in the loop", service.port); 117 | if (service.port == myPort) { 118 | if (service.fromFile) { 119 | try { 120 | const { stdout, stderr } = await exec( 121 | `dropdb -U postgres ${service.label};` 122 | ); 123 | console.log(stdout); 124 | console.log(stderr); 125 | } catch (e) { 126 | console.error(e); 127 | } 128 | 129 | try { 130 | fs.unlinkSync( 131 | path.resolve(__dirname, `./public/uploads/${service.fromFile}`) 132 | ); 133 | // console.log('FILE REMOVED') 134 | } catch (err) { 135 | console.error(err); 136 | } 137 | } 138 | } 139 | }); 140 | 141 | console.log(services); 142 | }); 143 | 144 | // REDIS 145 | const { 146 | redisController, 147 | cachePlugin, 148 | updater, 149 | } = require("./redis/redis-commands"); 150 | 151 | // SOCKET.IO 152 | const server = http.createServer(app); 153 | 154 | const socketIo = require("socket.io")(server, { 155 | cors: { 156 | origin: "*", 157 | methods: ["GET", "POST", "DELETE"], 158 | }, 159 | }); 160 | 161 | const getApiAndEmit = (socket) => { 162 | // console.log(updater); 163 | const response = updater; 164 | socket.emit("FromAPI", response); 165 | }; 166 | 167 | let interval; 168 | socketIo.on("connection", (socket) => { 169 | console.log("New client connected"); 170 | if (interval) { 171 | clearInterval(interval); 172 | } 173 | interval = setInterval(() => getApiAndEmit(socket), 1000); 174 | socket.on("disconnect", () => { 175 | console.log("Client disconnected"); 176 | clearInterval(interval); 177 | }); 178 | }); 179 | 180 | server.listen(3333, () => { 181 | console.log("Success!"); 182 | }); 183 | 184 | // APOLLO SERVER + POSTGRAPHILE 185 | // We need some logic in here to handle if the string is malformed. App crashes otherwise. 186 | const createNewApolloServer = (service) => { 187 | const pgPool = new pg.Pool({ 188 | // do this via an environment variable 189 | connectionString: service.db_uri, 190 | }); 191 | 192 | async function startApolloServer() { 193 | const app = express(); 194 | 195 | const { schema, plugin } = await makeSchemaAndPlugin( 196 | pgPool, 197 | "public", // PostgreSQL schema to use 198 | { 199 | // PostGraphile options, see: 200 | // https://www.graphile.org/postgraphile/usage-library/ 201 | // watchPg: true, 202 | graphiql: true, 203 | graphlqlRoute: "/graphql", 204 | // These are not the same! 205 | // not using the graphiql route below 206 | graphiqlRoute: "/test", 207 | enhanceGraphiql: true, 208 | } 209 | ); 210 | 211 | const options = {}; 212 | 213 | const server = new ApolloServer({ 214 | schema, 215 | plugins: [plugin, cachePlugin, ApolloLogPlugin(options)], 216 | tracing: true, 217 | introspection: true, 218 | }); 219 | 220 | await server.start(); 221 | server.applyMiddleware({ app }); 222 | 223 | app.use(express.json()); 224 | app.use( 225 | express.urlencoded({ 226 | extended: true, 227 | }) 228 | ); 229 | 230 | const corsOptions = { 231 | origin: "*", 232 | optionsSuccessStatus: 200, 233 | }; 234 | app.use(cors(corsOptions)); 235 | 236 | // REDIS CACHED METRICS 237 | app.get("/redis/:hash", redisController.serveMetrics, (req, res) => { 238 | console.log("Result from Redis cache: "); 239 | console.log(res.locals.metrics); 240 | return res.status(200).send(res.locals.metrics); 241 | }); 242 | 243 | // EXPRESS UNKNOWN ROUTE HANDLER 244 | app.use("*", (req, res) => res.status(404).send("404 Not Found")); 245 | 246 | // EXPRESS GLOBAL ERROR HANDLER 247 | app.use((err, req, res, next) => { 248 | console.log(err); 249 | return res.status(500).send("Internal Server Error ", err); 250 | }); 251 | 252 | const myApp = app.listen({ port: service.port }); 253 | console.log("\x1b[32m", ` .:: :: .:::: .:: `); 254 | console.log("\x1b[32m", `.:: .:: .:: .:: .:: .: `); 255 | console.log("\x1b[32m", ` .:: .: .:: .:: .:: .::.:: .:: .:: `); 256 | console.log("\x1b[32m", ` .:: .: .:: .: .:: .:: .::.:: .:: .: `); 257 | console.log("\x1b[32m", ` .:: .: .::.::::: .::.:: .::.:: .:: .: `); 258 | console.log("\x1b[32m", `.:: .::.:: .:: .: .:: .: .:: .:: .:: .::`); 259 | console.log("\x1b[32m", ` .:: :: .:: .:::: .:: :: .:::::::: .:::: `); 260 | console.log("\x1b[32m", ` .:: .: `); 261 | console.log("\x1b[35m", `Port ${service.port} active`); 262 | console.log("\x1b[35m", 263 | `🔮 Fortunes being told at http://localhost:3333 ✨`); 264 | return myApp; 265 | } 266 | 267 | // CALL APOLLO SERVER FOR GRAPHIQL 268 | return startApolloServer().catch((e) => { 269 | console.error(e); 270 | // process.exit(1); 271 | }); 272 | }; 273 | 274 | // NEW APOLLO SERVER PER SCHEMA 275 | const myServers = []; 276 | 277 | services.forEach((service) => { 278 | createNewApolloServer(service) 279 | .then((data) => myServers.push(data)) 280 | .catch((err) => console.log(err)); 281 | }); 282 | --------------------------------------------------------------------------------