Pārlūkot izejas kodu

feat: Added basic GraphQL foundation

Owen Diffey 3 gadi atpakaļ
vecāks
revīzija
38ea36b0a7

+ 4 - 0
backend/logic/app.js

@@ -7,7 +7,9 @@ import bodyParser from "body-parser";
 import express from "express";
 import oauth from "oauth";
 import http from "http";
+import { graphqlHTTP } from "express-graphql";
 import CoreClass from "../core";
+import { schema } from "./schemas/test";
 
 const { OAuth2 } = oauth;
 
@@ -62,6 +64,8 @@ class _AppModule extends CoreClass {
 			app.use(cors(corsOptions));
 			app.options("*", cors(corsOptions));
 
+			app.use("/graphql", graphqlHTTP({ schema }));
+
 			const oauth2 = new OAuth2(
 				config.get("apis.github.client"),
 				config.get("apis.github.secret"),

+ 38 - 0
backend/logic/schemas/test.js

@@ -0,0 +1,38 @@
+import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql";
+
+/**
+ * Construct a GraphQL schema and define the necessary resolvers.
+ *
+ * type Query {
+ *   hello: String
+ * }
+ * type Subscription {
+ *   greetings: String
+ * }
+ */
+// eslint-disable-next-line
+export const schema = new GraphQLSchema({
+	query: new GraphQLObjectType({
+		name: "Query",
+		fields: {
+			hello: {
+				type: GraphQLString,
+				resolve: () => "world"
+			}
+		}
+	}),
+	subscription: new GraphQLObjectType({
+		name: "Subscription",
+		fields: {
+			greetings: {
+				type: GraphQLString,
+				async *subscribe() {
+					// eslint-disable-next-line
+					for (const hi of ["Hi", "Bonjour", "Hola", "Ciao", "Zdravo"]) {
+						yield { greetings: hi };
+					}
+				}
+			}
+		}
+	})
+});

+ 22 - 1
backend/logic/ws.js

@@ -6,6 +6,10 @@ import config from "config";
 import async from "async";
 import { WebSocketServer } from "ws";
 import { EventEmitter } from "events";
+import { parse } from "url";
+
+import { useServer } from "graphql-ws/lib/use/ws";
+import { schema } from "./schemas/test";
 
 import CoreClass from "../core";
 
@@ -49,7 +53,9 @@ class _WSModule extends CoreClass {
 		// TODO: Check every 30s/, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
 		const server = await AppModule.runJob("SERVER");
 
-		this._io = new WebSocketServer({ server, path: "/ws" });
+		this._io = new WebSocketServer({ path: "/ws", noServer: true });
+		this._graphql = new WebSocketServer({ path: "/graphql", noServer: true });
+		useServer({ schema }, this._graphql);
 
 		this.rooms = {};
 
@@ -73,6 +79,21 @@ class _WSModule extends CoreClass {
 				});
 			});
 
+			server.on("upgrade", (req, socket, head) => {
+				const { pathname } = parse(req.url);
+				if (pathname === "/ws") {
+					this._io.handleUpgrade(req, socket, head, ws => {
+						this._io.emit("connection", ws, req);
+					});
+				} else if (pathname === "/graphql") {
+					this._graphql.handleUpgrade(req, socket, head, ws => {
+						this._graphql.emit("connection", ws, req);
+					});
+				} else {
+					socket.destroy();
+				}
+			});
+
 			const keepAliveInterval = setInterval(() => {
 				this._io.clients.forEach(socket => {
 					if (socket.isAlive === false) return socket.terminate();

+ 129 - 0
backend/package-lock.json

@@ -18,6 +18,9 @@
         "cookie-parser": "^1.4.6",
         "cors": "^2.8.5",
         "express": "^4.18.1",
+        "express-graphql": "^0.12.0",
+        "graphql": "^16.5.0",
+        "graphql-ws": "^5.9.0",
         "moment": "^2.29.3",
         "mongoose": "^6.3.5",
         "nodemailer": "^6.7.5",
@@ -1303,6 +1306,62 @@
         "node": ">= 0.10.0"
       }
     },
+    "node_modules/express-graphql": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz",
+      "integrity": "sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==",
+      "dependencies": {
+        "accepts": "^1.3.7",
+        "content-type": "^1.0.4",
+        "http-errors": "1.8.0",
+        "raw-body": "^2.4.1"
+      },
+      "engines": {
+        "node": ">= 10.x"
+      },
+      "peerDependencies": {
+        "graphql": "^14.7.0 || ^15.3.0"
+      }
+    },
+    "node_modules/express-graphql/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-graphql/node_modules/http-errors": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
+      "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-graphql/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express-graphql/node_modules/toidentifier": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+      "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
     "node_modules/express/node_modules/cookie": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@@ -1604,6 +1663,25 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/graphql": {
+      "version": "16.5.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz",
+      "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==",
+      "engines": {
+        "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+      }
+    },
+    "node_modules/graphql-ws": {
+      "version": "5.9.0",
+      "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.9.0.tgz",
+      "integrity": "sha512-CXv0l0nI1bgChwl4Rm+BqNOAKwL/C9T2N8RfmTkhQ38YLFdUXCi2WNW4oFp8BJP+t75nCLzjHHgR04sP1oF02w==",
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "graphql": ">=0.11 <=16"
+      }
+    },
     "node_modules/has": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -4492,6 +4570,46 @@
         }
       }
     },
+    "express-graphql": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz",
+      "integrity": "sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==",
+      "requires": {
+        "accepts": "^1.3.7",
+        "content-type": "^1.0.4",
+        "http-errors": "1.8.0",
+        "raw-body": "^2.4.1"
+      },
+      "dependencies": {
+        "depd": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+          "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="
+        },
+        "http-errors": {
+          "version": "1.8.0",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
+          "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
+          "requires": {
+            "depd": "~1.1.2",
+            "inherits": "2.0.4",
+            "setprototypeof": "1.2.0",
+            "statuses": ">= 1.5.0 < 2",
+            "toidentifier": "1.0.0"
+          }
+        },
+        "statuses": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+          "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
+        },
+        "toidentifier": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+          "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+        }
+      }
+    },
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4708,6 +4826,17 @@
         "type-fest": "^0.20.2"
       }
     },
+    "graphql": {
+      "version": "16.5.0",
+      "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz",
+      "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA=="
+    },
+    "graphql-ws": {
+      "version": "5.9.0",
+      "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.9.0.tgz",
+      "integrity": "sha512-CXv0l0nI1bgChwl4Rm+BqNOAKwL/C9T2N8RfmTkhQ38YLFdUXCi2WNW4oFp8BJP+t75nCLzjHHgR04sP1oF02w==",
+      "requires": {}
+    },
     "has": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",

+ 3 - 0
backend/package.json

@@ -24,6 +24,9 @@
     "cookie-parser": "^1.4.6",
     "cors": "^2.8.5",
     "express": "^4.18.1",
+    "express-graphql": "^0.12.0",
+    "graphql": "^16.5.0",
+    "graphql-ws": "^5.9.0",
     "moment": "^2.29.3",
     "mongoose": "^6.3.5",
     "nodemailer": "^6.7.5",

+ 76 - 0
frontend/dist/graphiql.html

@@ -0,0 +1,76 @@
+<!--
+ *  Copyright (c) 2021 GraphQL Contributors
+ *  All rights reserved.
+ *
+ *  This source code is licensed under the license found in the
+ *  LICENSE file in the root directory of this source tree.
+-->
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      body {
+        height: 100%;
+        margin: 0;
+        width: 100%;
+        overflow: hidden;
+      }
+
+      #graphiql {
+        height: 100vh;
+      }
+    </style>
+
+    <script
+    type="text/javascript"
+    src="https://unpkg.com/graphql-ws/umd/graphql-ws.min.js"
+    ></script>
+
+    <!--
+      This GraphiQL example depends on Promise and fetch, which are available in
+      modern browsers, but can be "polyfilled" for older browsers.
+      GraphiQL itself depends on React DOM.
+      If you do not want to rely on a CDN, you can host these files locally or
+      include them directly in your favored resource bunder.
+    -->
+    <script
+      crossorigin
+      src="https://unpkg.com/react@17/umd/react.development.js"
+    ></script>
+    <script
+      crossorigin
+      src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
+    ></script>
+
+    <!--
+      These two files can be found in the npm module, however you may wish to
+      copy them directly into your environment, or perhaps include them in your
+      favored resource bundler.
+     -->
+    <link rel="stylesheet" href="https://unpkg.com/graphiql/graphiql.min.css" />
+  </head>
+
+  <body>
+    <div id="graphiql">Loading...</div>
+    <script
+      src="https://unpkg.com/graphiql/graphiql.min.js"
+      type="application/javascript"
+    ></script>
+    <script>
+      const fetcher = GraphiQL.createFetcher({
+        url: 'https://whaley.diffey.dev/backend/graphql',
+        wsClient: graphqlWs.createClient({
+          url: 'wss://whaley.diffey.dev/backend/graphql',
+        }),
+      });
+
+      ReactDOM.render(
+        React.createElement(GraphiQL, {
+          fetcher,
+          defaultVariableEditorOpen: true,
+        }),
+        document.getElementById('graphiql'),
+      );
+    </script>
+  </body>
+</html>