├── 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 ;
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 |
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 |
179 | ...or upload .sql or .tar file
180 |
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 |
--------------------------------------------------------------------------------