diff --git a/.gitignore b/.gitignore
index 4d29575de80483b005c29bfcac5061cd2f45313e..2fa56e4996c1e603ab14990da32b5c2353cfe189 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,7 @@
 /coverage
 
 # production
-/build
+/dist
 
 # misc
 .DS_Store
diff --git a/grpc.txt b/grpc.txt
deleted file mode 100644
index eb0d24e4ff75a1d4c15694e7efa040a3ba8433b7..0000000000000000000000000000000000000000
--- a/grpc.txt
+++ /dev/null
@@ -1,21 +0,0 @@
------GRPC TESTS-----
-randomProduct
-39.50119981765747
-product
-37.52583360671997
-allProducts
-31.22728362083435
-categories
-29.51584138870239
-allOrders
-52.35934963226318
-ordersByUser
-42.86946668624878
-user
-31.468091630935668
-allUsers
-31.84824995994568
-postOrder
-32.73759984970093
-patchUser
-34.883433198928834
\ No newline at end of file
diff --git a/grpcTests.js b/grpcTests.js
deleted file mode 100644
index 97c907400290883e5fdc27c4aacd3b0f7e6b7a7f..0000000000000000000000000000000000000000
--- a/grpcTests.js
+++ /dev/null
@@ -1,183 +0,0 @@
-const grpc = require("@grpc/grpc-js");
-const protoLoader = require("@grpc/proto-loader");
-
-const PROTO_PATH = "./app.proto";
-
-const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
-  keepCase: true,
-  longs: String,
-  enums: String,
-  defaults: true,
-  oneofs: true,
-});
-
-const app = grpc.loadPackageDefinition(packageDefinition);
-
-const getTests = (endpoint, user, product) => {
-  const client = new app.App(endpoint, grpc.credentials.createInsecure());
-
-  return [
-    {
-      name: "randomProduct",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getRandomProduct({}, function (_err, response) {
-            if (_err) reject(_err);
-            resolve(response);
-          });
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "product",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getProduct(
-            { product_id: product },
-            function (_err, response) {
-              if (_err) reject(_err);
-              resolve(response);
-            },
-          );
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "allProducts",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getAllProducts({}, function (_err, response) {
-            if (_err) reject(_err);
-            resolve(response);
-          });
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "categories",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getAllCategories({}, function (_err, response) {
-            if (_err) reject(_err);
-            resolve(response);
-          });
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "allOrders",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getAllOrders({}, function (_err, response) {
-            if (_err) reject(_err);
-            resolve(response);
-          });
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "ordersByUser",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getAllUserOrders(
-            { id: user },
-            function (_err, response) {
-              if (_err) reject(_err);
-              resolve(response);
-            },
-          );
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "user",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getUser(
-            { id: user },
-            function (_err, response) {
-              if (_err) reject(_err);
-              resolve(response);
-            },
-          );
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "allUsers",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.getAllUsers({}, function (_err, response) {
-            if (_err) reject(_err);
-            resolve(response);
-          });
-        });
-
-        return [0, response];
-      },
-    },
-    {
-      name: "postOrder",
-      run: async () => {
-        {
-          const response = await new Promise((resolve, reject) => {
-            client.postOrder(
-              {
-                user_id: user,
-                products: [
-                  {
-                    product_id: product,
-                    quantity: 2,
-                  },
-                ],
-                total_amount: 600.0,
-              },
-              function (_err, response) {
-                if (_err) reject(_err);
-                resolve(response);
-              },
-            );
-          });
-
-          return [0, response];
-        }
-      },
-    },
-    {
-      name: "patchUser",
-      run: async () => {
-        const response = await new Promise((resolve, reject) => {
-          client.patchAccountDetails(
-            {
-              id: user,
-              email: "update@test.com",
-              name: "update test",
-            },
-            function (_err, response) {
-              if (_err) reject(_err);
-              resolve(response);
-            },
-          );
-        });
-
-        return [0, response];
-      },
-    },
-  ];
-};
-
-module.exports = { getTests };
diff --git a/main.js b/main.js
deleted file mode 100644
index 0705a5651cece1cda982c73c7c14e15994d2e3ec..0000000000000000000000000000000000000000
--- a/main.js
+++ /dev/null
@@ -1,55 +0,0 @@
-const rest = require("./restTests");
-const grpc = require("./grpcTests");
-const { performance } = require("perf_hooks");
-
-const REST_ENDPOINT = "http://18.209.23.10:3000";
-const GRPC_ENDPOINT = "18.209.23.10:3001";
-
-const runTest = async (func) => {
-  const start = performance.now();
-  await func();
-  const end = performance.now();
-  return end-start;
-}
-
-const avgRuntime = async (func, times) => {
-  const promises = [];
-  for (let i = 0; i < times; i++) {
-    promises.push(runTest(func));
-  }
-
-  const results = await Promise.all(promises);
-  return results.reduce((acc, curr) => acc + curr) / times;
-};
-
-async function getExamples() {
-  let resp = await fetch(REST_ENDPOINT + "/randomproduct");
-  let body = await resp.json();
-  const prodId = body.id;
-
-  resp = await fetch(REST_ENDPOINT + "/users");
-  body = await resp.json();
-  const userId = body[0].id;
-
-  return { product: prodId, user: userId };
-}
-
-async function runTests() {
-  const {user, product} = await getExamples();
-
-  const ITERATIONS = 5;
-
-  console.log("-----GRPC TESTS-----");
-  for (const test of grpc.getTests(GRPC_ENDPOINT, user, product)) {
-    console.log(test.name);
-    console.log(await avgRuntime(test.run, ITERATIONS));
-  }
-
-  console.log("-----REST TESTS-----");
-  for (const test of rest.getTests(REST_ENDPOINT, user, product)) {
-    console.log(test.name);
-    console.log(await avgRuntime(test.run, ITERATIONS));
-  }
-}
-
-runTests();
diff --git a/package-lock.json b/package-lock.json
index 855c6688ae4d581fe7486b7d8add1e4b513d4898..8925fd523b1c1e083529793da6a5cc87580f7ee0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,22 @@
             "version": "1.0.0",
             "license": "ISC",
             "dependencies": {
-                "@grpc/grpc-js": "^1.9.0",
+                "@grpc/grpc-js": "1.9.0",
                 "@grpc/proto-loader": "^0.7.8",
+                "@types/express": "^4.17.21",
                 "benchmark": "^2.1.4",
-                "google-protobuf": "^3.21.2"
+                "express": "^4.18.2",
+                "express-ws": "^5.0.2",
+                "google-protobuf": "^3.21.2",
+                "nice-grpc": "^2.1.7",
+                "path": "^0.12.7",
+                "typescript": "^5.3.3",
+                "uuidv4": "^6.2.13"
+            },
+            "devDependencies": {
+                "@types/express-ws": "^3.0.4",
+                "grpc-tools": "^1.12.4",
+                "ts-proto": "^1.167.2"
             }
         },
         "node_modules/@grpc/grpc-js": {
@@ -28,13 +40,12 @@
             }
         },
         "node_modules/@grpc/proto-loader": {
-            "version": "0.7.8",
-            "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.8.tgz",
-            "integrity": "sha512-GU12e2c8dmdXb7XUlOgYWZ2o2i+z9/VeACkxTA/zzAe2IjclC5PnVL0lpgjhrqfpDYHzM8B1TF6pqWegMYAzlA==",
+            "version": "0.7.9",
+            "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.9.tgz",
+            "integrity": "sha512-YJsOehVXzgurc+lLAxYnlSMc1p/Gu6VAvnfx0ATi2nzvr0YZcjhmZDeY8SeAKv1M7zE3aEJH0Xo9mK1iZ8GYoQ==",
             "dependencies": {
-                "@types/long": "^4.0.1",
                 "lodash.camelcase": "^4.3.0",
-                "long": "^4.0.0",
+                "long": "^5.0.0",
                 "protobufjs": "^7.2.4",
                 "yargs": "^17.7.2"
             },
@@ -45,6 +56,26 @@
                 "node": ">=6"
             }
         },
+        "node_modules/@mapbox/node-pre-gyp": {
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+            "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+            "dev": true,
+            "dependencies": {
+                "detect-libc": "^2.0.0",
+                "https-proxy-agent": "^5.0.0",
+                "make-dir": "^3.1.0",
+                "node-fetch": "^2.6.7",
+                "nopt": "^5.0.0",
+                "npmlog": "^5.0.1",
+                "rimraf": "^3.0.2",
+                "semver": "^7.3.5",
+                "tar": "^6.1.11"
+            },
+            "bin": {
+                "node-pre-gyp": "bin/node-pre-gyp"
+            }
+        },
         "node_modules/@protobufjs/aspromise": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -99,15 +130,148 @@
             "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
             "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
         },
-        "node_modules/@types/long": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
-            "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
+        "node_modules/@types/body-parser": {
+            "version": "1.19.5",
+            "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+            "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+            "dependencies": {
+                "@types/connect": "*",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/connect": {
+            "version": "3.4.38",
+            "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+            "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/express": {
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+            "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+            "dependencies": {
+                "@types/body-parser": "*",
+                "@types/express-serve-static-core": "^4.17.33",
+                "@types/qs": "*",
+                "@types/serve-static": "*"
+            }
+        },
+        "node_modules/@types/express-serve-static-core": {
+            "version": "4.17.43",
+            "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
+            "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
+            "dependencies": {
+                "@types/node": "*",
+                "@types/qs": "*",
+                "@types/range-parser": "*",
+                "@types/send": "*"
+            }
+        },
+        "node_modules/@types/express-ws": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.4.tgz",
+            "integrity": "sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==",
+            "dev": true,
+            "dependencies": {
+                "@types/express": "*",
+                "@types/express-serve-static-core": "*",
+                "@types/ws": "*"
+            }
+        },
+        "node_modules/@types/http-errors": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+            "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
+        },
+        "node_modules/@types/mime": {
+            "version": "1.3.5",
+            "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+            "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
         },
         "node_modules/@types/node": {
-            "version": "20.4.7",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.7.tgz",
-            "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g=="
+            "version": "20.6.0",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz",
+            "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg=="
+        },
+        "node_modules/@types/qs": {
+            "version": "6.9.11",
+            "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
+            "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ=="
+        },
+        "node_modules/@types/range-parser": {
+            "version": "1.2.7",
+            "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+            "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
+        },
+        "node_modules/@types/send": {
+            "version": "0.17.4",
+            "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+            "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+            "dependencies": {
+                "@types/mime": "^1",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/serve-static": {
+            "version": "1.15.5",
+            "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
+            "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
+            "dependencies": {
+                "@types/http-errors": "*",
+                "@types/mime": "*",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/uuid": {
+            "version": "8.3.4",
+            "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
+            "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="
+        },
+        "node_modules/@types/ws": {
+            "version": "8.5.10",
+            "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
+            "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
+            "dev": true,
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/abbrev": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+            "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+            "dev": true
+        },
+        "node_modules/abort-controller-x": {
+            "version": "0.4.3",
+            "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz",
+            "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA=="
+        },
+        "node_modules/accepts": {
+            "version": "1.3.8",
+            "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+            "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+            "dependencies": {
+                "mime-types": "~2.1.34",
+                "negotiator": "0.6.3"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/agent-base": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "dev": true,
+            "dependencies": {
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6.0.0"
+            }
         },
         "node_modules/ansi-regex": {
             "version": "5.0.1",
@@ -131,6 +295,36 @@
                 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
             }
         },
+        "node_modules/aproba": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+            "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+            "dev": true
+        },
+        "node_modules/are-we-there-yet": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+            "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+            "dev": true,
+            "dependencies": {
+                "delegates": "^1.0.0",
+                "readable-stream": "^3.6.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/array-flatten": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+            "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+        },
+        "node_modules/balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+            "dev": true
+        },
         "node_modules/benchmark": {
             "version": "2.1.4",
             "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
@@ -140,6 +334,62 @@
                 "platform": "^1.3.3"
             }
         },
+        "node_modules/brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dev": true,
+            "dependencies": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "node_modules/bytes": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+            "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/call-bind": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz",
+            "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "function-bind": "^1.1.2",
+                "get-intrinsic": "^1.2.3",
+                "set-function-length": "^1.2.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/case-anything": {
+            "version": "2.1.13",
+            "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz",
+            "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==",
+            "dev": true,
+            "engines": {
+                "node": ">=12.13"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/mesqueeb"
+            }
+        },
+        "node_modules/chownr": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+            "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/cliui": {
             "version": "8.0.1",
             "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -169,11 +419,169 @@
             "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
             "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
         },
+        "node_modules/color-support": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+            "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+            "dev": true,
+            "bin": {
+                "color-support": "bin.js"
+            }
+        },
+        "node_modules/concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+            "dev": true
+        },
+        "node_modules/console-control-strings": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+            "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+            "dev": true
+        },
+        "node_modules/content-disposition": {
+            "version": "0.5.4",
+            "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+            "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+            "dependencies": {
+                "safe-buffer": "5.2.1"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/content-type": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+            "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/cookie": {
+            "version": "0.5.0",
+            "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+            "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/cookie-signature": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+            "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+        },
+        "node_modules/debug": {
+            "version": "4.3.4",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+            "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+            "dev": true,
+            "dependencies": {
+                "ms": "2.1.2"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/define-data-property": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz",
+            "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "get-intrinsic": "^1.2.2",
+                "gopd": "^1.0.1",
+                "has-property-descriptors": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/delegates": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+            "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+            "dev": true
+        },
+        "node_modules/depd": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+            "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/destroy": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+            "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+            "engines": {
+                "node": ">= 0.8",
+                "npm": "1.2.8000 || >= 1.4.16"
+            }
+        },
+        "node_modules/detect-libc": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+            "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/dprint-node": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz",
+            "integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==",
+            "dev": true,
+            "dependencies": {
+                "detect-libc": "^1.0.3"
+            }
+        },
+        "node_modules/dprint-node/node_modules/detect-libc": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+            "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+            "dev": true,
+            "bin": {
+                "detect-libc": "bin/detect-libc.js"
+            },
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
+        "node_modules/ee-first": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+            "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+        },
         "node_modules/emoji-regex": {
             "version": "8.0.0",
             "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
             "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
         },
+        "node_modules/encodeurl": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+            "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/es-errors": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+            "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
         "node_modules/escalade": {
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -182,131 +590,1270 @@
                 "node": ">=6"
             }
         },
-        "node_modules/get-caller-file": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+        "node_modules/escape-html": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+            "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+        },
+        "node_modules/etag": {
+            "version": "1.8.1",
+            "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+            "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
             "engines": {
-                "node": "6.* || 8.* || >= 10.*"
+                "node": ">= 0.6"
             }
         },
-        "node_modules/google-protobuf": {
-            "version": "3.21.2",
-            "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz",
-            "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA=="
+        "node_modules/express": {
+            "version": "4.18.2",
+            "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+            "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+            "dependencies": {
+                "accepts": "~1.3.8",
+                "array-flatten": "1.1.1",
+                "body-parser": "1.20.1",
+                "content-disposition": "0.5.4",
+                "content-type": "~1.0.4",
+                "cookie": "0.5.0",
+                "cookie-signature": "1.0.6",
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "encodeurl": "~1.0.2",
+                "escape-html": "~1.0.3",
+                "etag": "~1.8.1",
+                "finalhandler": "1.2.0",
+                "fresh": "0.5.2",
+                "http-errors": "2.0.0",
+                "merge-descriptors": "1.0.1",
+                "methods": "~1.1.2",
+                "on-finished": "2.4.1",
+                "parseurl": "~1.3.3",
+                "path-to-regexp": "0.1.7",
+                "proxy-addr": "~2.0.7",
+                "qs": "6.11.0",
+                "range-parser": "~1.2.1",
+                "safe-buffer": "5.2.1",
+                "send": "0.18.0",
+                "serve-static": "1.15.0",
+                "setprototypeof": "1.2.0",
+                "statuses": "2.0.1",
+                "type-is": "~1.6.18",
+                "utils-merge": "1.0.1",
+                "vary": "~1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.10.0"
+            }
         },
-        "node_modules/is-fullwidth-code-point": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+        "node_modules/express-ws": {
+            "version": "5.0.2",
+            "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz",
+            "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==",
+            "dependencies": {
+                "ws": "^7.4.6"
+            },
             "engines": {
-                "node": ">=8"
+                "node": ">=4.5.0"
+            },
+            "peerDependencies": {
+                "express": "^4.0.0 || ^5.0.0-alpha.1"
             }
         },
-        "node_modules/lodash": {
-            "version": "4.17.21",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+        "node_modules/express/node_modules/body-parser": {
+            "version": "1.20.1",
+            "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+            "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+            "dependencies": {
+                "bytes": "3.1.2",
+                "content-type": "~1.0.4",
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "destroy": "1.2.0",
+                "http-errors": "2.0.0",
+                "iconv-lite": "0.4.24",
+                "on-finished": "2.4.1",
+                "qs": "6.11.0",
+                "raw-body": "2.5.1",
+                "type-is": "~1.6.18",
+                "unpipe": "1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8",
+                "npm": "1.2.8000 || >= 1.4.16"
+            }
         },
-        "node_modules/lodash.camelcase": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
-            "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
+        "node_modules/express/node_modules/debug": {
+            "version": "2.6.9",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+            "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+            "dependencies": {
+                "ms": "2.0.0"
+            }
         },
-        "node_modules/long": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
-            "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+        "node_modules/express/node_modules/ms": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+            "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
         },
-        "node_modules/platform": {
-            "version": "1.3.6",
-            "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
-            "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
+        "node_modules/express/node_modules/raw-body": {
+            "version": "2.5.1",
+            "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+            "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+            "dependencies": {
+                "bytes": "3.1.2",
+                "http-errors": "2.0.0",
+                "iconv-lite": "0.4.24",
+                "unpipe": "1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
         },
-        "node_modules/protobufjs": {
-            "version": "7.2.4",
-            "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz",
-            "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==",
-            "hasInstallScript": true,
+        "node_modules/finalhandler": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+            "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
             "dependencies": {
-                "@protobufjs/aspromise": "^1.1.2",
-                "@protobufjs/base64": "^1.1.2",
-                "@protobufjs/codegen": "^2.0.4",
-                "@protobufjs/eventemitter": "^1.1.0",
-                "@protobufjs/fetch": "^1.1.0",
-                "@protobufjs/float": "^1.0.2",
-                "@protobufjs/inquire": "^1.1.0",
-                "@protobufjs/path": "^1.1.2",
-                "@protobufjs/pool": "^1.1.0",
-                "@protobufjs/utf8": "^1.1.0",
-                "@types/node": ">=13.7.0",
-                "long": "^5.0.0"
+                "debug": "2.6.9",
+                "encodeurl": "~1.0.2",
+                "escape-html": "~1.0.3",
+                "on-finished": "2.4.1",
+                "parseurl": "~1.3.3",
+                "statuses": "2.0.1",
+                "unpipe": "~1.0.0"
             },
             "engines": {
-                "node": ">=12.0.0"
+                "node": ">= 0.8"
             }
         },
-        "node_modules/protobufjs/node_modules/long": {
-            "version": "5.2.3",
-            "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
-            "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+        "node_modules/finalhandler/node_modules/debug": {
+            "version": "2.6.9",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+            "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+            "dependencies": {
+                "ms": "2.0.0"
+            }
         },
-        "node_modules/require-directory": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-            "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+        "node_modules/finalhandler/node_modules/ms": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+            "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+        },
+        "node_modules/forwarded": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+            "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
             "engines": {
-                "node": ">=0.10.0"
+                "node": ">= 0.6"
             }
         },
-        "node_modules/string-width": {
-            "version": "4.2.3",
-            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+        "node_modules/fresh": {
+            "version": "0.5.2",
+            "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+            "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/fs-minipass": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+            "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+            "dev": true,
             "dependencies": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
+                "minipass": "^3.0.0"
             },
             "engines": {
-                "node": ">=8"
+                "node": ">= 8"
             }
         },
-        "node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+        "node_modules/fs-minipass/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+            "dev": true,
             "dependencies": {
-                "ansi-regex": "^5.0.1"
+                "yallist": "^4.0.0"
             },
             "engines": {
                 "node": ">=8"
             }
         },
-        "node_modules/wrap-ansi": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+        "node_modules/fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+            "dev": true
+        },
+        "node_modules/function-bind": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+            "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/gauge": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+            "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+            "dev": true,
             "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "string-width": "^4.1.0",
-                "strip-ansi": "^6.0.0"
+                "aproba": "^1.0.3 || ^2.0.0",
+                "color-support": "^1.1.2",
+                "console-control-strings": "^1.0.0",
+                "has-unicode": "^2.0.1",
+                "object-assign": "^4.1.1",
+                "signal-exit": "^3.0.0",
+                "string-width": "^4.2.3",
+                "strip-ansi": "^6.0.1",
+                "wide-align": "^1.1.2"
             },
             "engines": {
                 "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
             }
         },
-        "node_modules/y18n": {
-            "version": "5.0.8",
-            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
-            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+        "node_modules/get-caller-file": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
             "engines": {
-                "node": ">=10"
+                "node": "6.* || 8.* || >= 10.*"
             }
         },
+        "node_modules/get-intrinsic": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+            "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "function-bind": "^1.1.2",
+                "has-proto": "^1.0.1",
+                "has-symbols": "^1.0.3",
+                "hasown": "^2.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/glob": {
+            "version": "7.2.3",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+            "dev": true,
+            "dependencies": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.1.1",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            },
+            "engines": {
+                "node": "*"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/google-protobuf": {
+            "version": "3.21.2",
+            "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz",
+            "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA=="
+        },
+        "node_modules/gopd": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+            "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+            "dependencies": {
+                "get-intrinsic": "^1.1.3"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/grpc-tools": {
+            "version": "1.12.4",
+            "resolved": "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.12.4.tgz",
+            "integrity": "sha512-5+mLAJJma3BjnW/KQp6JBjUMgvu7Mu3dBvBPd1dcbNIb+qiR0817zDpgPjS7gRb+l/8EVNIa3cB02xI9JLToKg==",
+            "dev": true,
+            "hasInstallScript": true,
+            "dependencies": {
+                "@mapbox/node-pre-gyp": "^1.0.5"
+            },
+            "bin": {
+                "grpc_tools_node_protoc": "bin/protoc.js",
+                "grpc_tools_node_protoc_plugin": "bin/protoc_plugin.js"
+            }
+        },
+        "node_modules/has-property-descriptors": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+            "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+            "dependencies": {
+                "get-intrinsic": "^1.2.2"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-proto": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+            "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-symbols": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+            "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/has-unicode": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+            "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+            "dev": true
+        },
+        "node_modules/hasown": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+            "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+            "dependencies": {
+                "function-bind": "^1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/http-errors": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+            "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+            "dependencies": {
+                "depd": "2.0.0",
+                "inherits": "2.0.4",
+                "setprototypeof": "1.2.0",
+                "statuses": "2.0.1",
+                "toidentifier": "1.0.1"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/https-proxy-agent": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+            "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+            "dev": true,
+            "dependencies": {
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/iconv-lite": {
+            "version": "0.4.24",
+            "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+            "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+            "dependencies": {
+                "safer-buffer": ">= 2.1.2 < 3"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+            "dev": true,
+            "dependencies": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "node_modules/inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+        },
+        "node_modules/ipaddr.js": {
+            "version": "1.9.1",
+            "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+            "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+            "engines": {
+                "node": ">= 0.10"
+            }
+        },
+        "node_modules/is-fullwidth-code-point": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/lodash": {
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+        },
+        "node_modules/lodash.camelcase": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+            "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
+        },
+        "node_modules/long": {
+            "version": "5.2.3",
+            "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+            "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+        },
+        "node_modules/lru-cache": {
+            "version": "6.0.0",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+            "dev": true,
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/make-dir": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+            "dev": true,
+            "dependencies": {
+                "semver": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/make-dir/node_modules/semver": {
+            "version": "6.3.1",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+            "dev": true,
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
+        "node_modules/media-typer": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+            "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/merge-descriptors": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+            "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
+        },
+        "node_modules/methods": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+            "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mime": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+            "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+            "bin": {
+                "mime": "cli.js"
+            },
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/mime-db": {
+            "version": "1.52.0",
+            "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+            "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mime-types": {
+            "version": "2.1.35",
+            "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+            "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+            "dependencies": {
+                "mime-db": "1.52.0"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "dev": true,
+            "dependencies": {
+                "brace-expansion": "^1.1.7"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/minipass": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+            "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/minizlib": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+            "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+            "dev": true,
+            "dependencies": {
+                "minipass": "^3.0.0",
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/minizlib/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+            "dev": true,
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/mkdirp": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+            "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+            "dev": true,
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/ms": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+            "dev": true
+        },
+        "node_modules/negotiator": {
+            "version": "0.6.3",
+            "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+            "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/nice-grpc": {
+            "version": "2.1.7",
+            "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.7.tgz",
+            "integrity": "sha512-pSaZk5Y3PHGAPObOSXTrANgimA6T//szxlcKOnnyttpYwO0gyOpX2WsaFK4fbGJizPVxXjwqrXpPOSHMwM2vlg==",
+            "dependencies": {
+                "@grpc/grpc-js": "^1.9.5",
+                "abort-controller-x": "^0.4.0",
+                "nice-grpc-common": "^2.0.2"
+            }
+        },
+        "node_modules/nice-grpc-common": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz",
+            "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==",
+            "dependencies": {
+                "ts-error": "^1.0.6"
+            }
+        },
+        "node_modules/nice-grpc/node_modules/@grpc/grpc-js": {
+            "version": "1.9.14",
+            "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.14.tgz",
+            "integrity": "sha512-nOpuzZ2G3IuMFN+UPPpKrC6NsLmWsTqSsm66IRfnBt1D4pwTqE27lmbpcPM+l2Ua4gE7PfjRHI6uedAy7hoXUw==",
+            "dependencies": {
+                "@grpc/proto-loader": "^0.7.8",
+                "@types/node": ">=12.12.47"
+            },
+            "engines": {
+                "node": "^8.13.0 || >=10.10.0"
+            }
+        },
+        "node_modules/node-fetch": {
+            "version": "2.7.0",
+            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+            "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+            "dev": true,
+            "dependencies": {
+                "whatwg-url": "^5.0.0"
+            },
+            "engines": {
+                "node": "4.x || >=6.0.0"
+            },
+            "peerDependencies": {
+                "encoding": "^0.1.0"
+            },
+            "peerDependenciesMeta": {
+                "encoding": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/nopt": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+            "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+            "dev": true,
+            "dependencies": {
+                "abbrev": "1"
+            },
+            "bin": {
+                "nopt": "bin/nopt.js"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/npmlog": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+            "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+            "dev": true,
+            "dependencies": {
+                "are-we-there-yet": "^2.0.0",
+                "console-control-strings": "^1.1.0",
+                "gauge": "^3.0.0",
+                "set-blocking": "^2.0.0"
+            }
+        },
+        "node_modules/object-assign": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+            "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/object-inspect": {
+            "version": "1.13.1",
+            "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+            "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/on-finished": {
+            "version": "2.4.1",
+            "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+            "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+            "dependencies": {
+                "ee-first": "1.1.1"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+            "dev": true,
+            "dependencies": {
+                "wrappy": "1"
+            }
+        },
+        "node_modules/parseurl": {
+            "version": "1.3.3",
+            "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+            "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/path": {
+            "version": "0.12.7",
+            "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+            "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
+            "dependencies": {
+                "process": "^0.11.1",
+                "util": "^0.10.3"
+            }
+        },
+        "node_modules/path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/path-to-regexp": {
+            "version": "0.1.7",
+            "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+            "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
+        },
+        "node_modules/platform": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+            "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
+        },
+        "node_modules/process": {
+            "version": "0.11.10",
+            "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+            "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+            "engines": {
+                "node": ">= 0.6.0"
+            }
+        },
+        "node_modules/protobufjs": {
+            "version": "7.2.5",
+            "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz",
+            "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==",
+            "hasInstallScript": true,
+            "dependencies": {
+                "@protobufjs/aspromise": "^1.1.2",
+                "@protobufjs/base64": "^1.1.2",
+                "@protobufjs/codegen": "^2.0.4",
+                "@protobufjs/eventemitter": "^1.1.0",
+                "@protobufjs/fetch": "^1.1.0",
+                "@protobufjs/float": "^1.0.2",
+                "@protobufjs/inquire": "^1.1.0",
+                "@protobufjs/path": "^1.1.2",
+                "@protobufjs/pool": "^1.1.0",
+                "@protobufjs/utf8": "^1.1.0",
+                "@types/node": ">=13.7.0",
+                "long": "^5.0.0"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
+        "node_modules/proxy-addr": {
+            "version": "2.0.7",
+            "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+            "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+            "dependencies": {
+                "forwarded": "0.2.0",
+                "ipaddr.js": "1.9.1"
+            },
+            "engines": {
+                "node": ">= 0.10"
+            }
+        },
+        "node_modules/qs": {
+            "version": "6.11.0",
+            "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+            "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+            "dependencies": {
+                "side-channel": "^1.0.4"
+            },
+            "engines": {
+                "node": ">=0.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/range-parser": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+            "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/require-directory": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+            "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "dev": true,
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/safer-buffer": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+            "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+        },
+        "node_modules/semver": {
+            "version": "7.5.4",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+            "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+            "dev": true,
+            "dependencies": {
+                "lru-cache": "^6.0.0"
+            },
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/send": {
+            "version": "0.18.0",
+            "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+            "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+            "dependencies": {
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "destroy": "1.2.0",
+                "encodeurl": "~1.0.2",
+                "escape-html": "~1.0.3",
+                "etag": "~1.8.1",
+                "fresh": "0.5.2",
+                "http-errors": "2.0.0",
+                "mime": "1.6.0",
+                "ms": "2.1.3",
+                "on-finished": "2.4.1",
+                "range-parser": "~1.2.1",
+                "statuses": "2.0.1"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/send/node_modules/debug": {
+            "version": "2.6.9",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+            "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+            "dependencies": {
+                "ms": "2.0.0"
+            }
+        },
+        "node_modules/send/node_modules/debug/node_modules/ms": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+            "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+        },
+        "node_modules/send/node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        },
+        "node_modules/serve-static": {
+            "version": "1.15.0",
+            "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+            "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+            "dependencies": {
+                "encodeurl": "~1.0.2",
+                "escape-html": "~1.0.3",
+                "parseurl": "~1.3.3",
+                "send": "0.18.0"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/set-blocking": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+            "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+            "dev": true
+        },
+        "node_modules/set-function-length": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
+            "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+            "dependencies": {
+                "define-data-property": "^1.1.2",
+                "es-errors": "^1.3.0",
+                "function-bind": "^1.1.2",
+                "get-intrinsic": "^1.2.3",
+                "gopd": "^1.0.1",
+                "has-property-descriptors": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            }
+        },
+        "node_modules/setprototypeof": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+            "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+        },
+        "node_modules/side-channel": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
+            "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
+            "dependencies": {
+                "call-bind": "^1.0.6",
+                "es-errors": "^1.3.0",
+                "get-intrinsic": "^1.2.4",
+                "object-inspect": "^1.13.1"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+            "dev": true
+        },
+        "node_modules/statuses": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+            "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "dev": true,
+            "dependencies": {
+                "safe-buffer": "~5.2.0"
+            }
+        },
+        "node_modules/string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/tar": {
+            "version": "6.2.0",
+            "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
+            "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+            "dev": true,
+            "dependencies": {
+                "chownr": "^2.0.0",
+                "fs-minipass": "^2.0.0",
+                "minipass": "^5.0.0",
+                "minizlib": "^2.1.1",
+                "mkdirp": "^1.0.3",
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/toidentifier": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+            "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+            "engines": {
+                "node": ">=0.6"
+            }
+        },
+        "node_modules/tr46": {
+            "version": "0.0.3",
+            "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+            "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+            "dev": true
+        },
+        "node_modules/ts-error": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz",
+            "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="
+        },
+        "node_modules/ts-poet": {
+            "version": "6.6.0",
+            "resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.6.0.tgz",
+            "integrity": "sha512-4vEH/wkhcjRPFOdBwIh9ItO6jOoumVLRF4aABDX5JSNEubSqwOulihxQPqai+OkuygJm3WYMInxXQX4QwVNMuw==",
+            "dev": true,
+            "dependencies": {
+                "dprint-node": "^1.0.7"
+            }
+        },
+        "node_modules/ts-proto": {
+            "version": "1.167.2",
+            "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.167.2.tgz",
+            "integrity": "sha512-7y/BLjiUZphgCe+SZBEG20DP94VK7BHpHcl5fkeN8lRCeABNIsiI54FkUQ8pe7PsHLVpFKqMO5aRLx74FX+4iA==",
+            "dev": true,
+            "dependencies": {
+                "case-anything": "^2.1.13",
+                "protobufjs": "^7.2.4",
+                "ts-poet": "^6.5.0",
+                "ts-proto-descriptors": "1.15.0"
+            },
+            "bin": {
+                "protoc-gen-ts_proto": "protoc-gen-ts_proto"
+            }
+        },
+        "node_modules/ts-proto-descriptors": {
+            "version": "1.15.0",
+            "resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.15.0.tgz",
+            "integrity": "sha512-TYyJ7+H+7Jsqawdv+mfsEpZPTIj9siDHS6EMCzG/z3b/PZiphsX+mWtqFfFVe5/N0Th6V3elK9lQqjnrgTOfrg==",
+            "dev": true,
+            "dependencies": {
+                "long": "^5.2.3",
+                "protobufjs": "^7.2.4"
+            }
+        },
+        "node_modules/type-is": {
+            "version": "1.6.18",
+            "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+            "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+            "dependencies": {
+                "media-typer": "0.3.0",
+                "mime-types": "~2.1.24"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/typescript": {
+            "version": "5.3.3",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+            "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
+            },
+            "engines": {
+                "node": ">=14.17"
+            }
+        },
+        "node_modules/unpipe": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+            "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/util": {
+            "version": "0.10.4",
+            "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+            "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+            "dependencies": {
+                "inherits": "2.0.3"
+            }
+        },
+        "node_modules/util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+            "dev": true
+        },
+        "node_modules/util/node_modules/inherits": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+            "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
+        },
+        "node_modules/utils-merge": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+            "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+            "engines": {
+                "node": ">= 0.4.0"
+            }
+        },
+        "node_modules/uuid": {
+            "version": "8.3.2",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+            "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+            "bin": {
+                "uuid": "dist/bin/uuid"
+            }
+        },
+        "node_modules/uuidv4": {
+            "version": "6.2.13",
+            "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz",
+            "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==",
+            "dependencies": {
+                "@types/uuid": "8.3.4",
+                "uuid": "8.3.2"
+            }
+        },
+        "node_modules/vary": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+            "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/webidl-conversions": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+            "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+            "dev": true
+        },
+        "node_modules/whatwg-url": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+            "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+            "dev": true,
+            "dependencies": {
+                "tr46": "~0.0.3",
+                "webidl-conversions": "^3.0.0"
+            }
+        },
+        "node_modules/wide-align": {
+            "version": "1.1.5",
+            "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+            "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+            "dev": true,
+            "dependencies": {
+                "string-width": "^1.0.2 || 2 || 3 || 4"
+            }
+        },
+        "node_modules/wrap-ansi": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
+        "node_modules/wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+            "dev": true
+        },
+        "node_modules/ws": {
+            "version": "7.5.9",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
+            "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
+            "engines": {
+                "node": ">=8.3.0"
+            },
+            "peerDependencies": {
+                "bufferutil": "^4.0.1",
+                "utf-8-validate": "^5.0.2"
+            },
+            "peerDependenciesMeta": {
+                "bufferutil": {
+                    "optional": true
+                },
+                "utf-8-validate": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/y18n": {
+            "version": "5.0.8",
+            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+            "dev": true
+        },
         "node_modules/yargs": {
             "version": "17.7.2",
             "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
diff --git a/package.json b/package.json
index 80ce57316af5813942b949269e4f2098dab165f9..25c8beeb07b6d4b0856324908fcaf29c3b2e847b 100644
--- a/package.json
+++ b/package.json
@@ -2,17 +2,29 @@
     "name": "lab1-measure",
     "version": "1.0.0",
     "description": "",
-    "main": "main.js",
+    "main": "dist/main.js",
     "scripts": {
-        "start": "node main.js",
+        "start": "tsc && cp public/* dist && node dist/main.js",
         "test": "echo \"Error: no test specified\" && exit 1"
     },
     "author": "based JAML",
     "license": "ISC",
     "dependencies": {
-        "@grpc/grpc-js": "^1.9.0",
+        "@grpc/grpc-js": "1.9.0",
         "@grpc/proto-loader": "^0.7.8",
+        "@types/express": "^4.17.21",
         "benchmark": "^2.1.4",
-        "google-protobuf": "^3.21.2"
+        "express": "^4.18.2",
+        "express-ws": "^5.0.2",
+        "google-protobuf": "^3.21.2",
+        "nice-grpc": "^2.1.7",
+        "path": "^0.12.7",
+        "typescript": "^5.3.3",
+        "uuidv4": "^6.2.13"
+    },
+    "devDependencies": {
+        "@types/express-ws": "^3.0.4",
+        "grpc-tools": "^1.12.4",
+        "ts-proto": "^1.167.2"
     }
-}
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..cdc6e56604d2eaacba279ae513722eb08dbcf9f6
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>SE464 Lab Testing</title>
+</head>
+
+<body>
+    <h1>SE 464 Benchmarker</h1>
+    <noscript>
+        For full functionality of this site it is necessary to enable JavaScript.
+        Here are the <a href="https://www.enable-javascript.com/">
+            instructions how to enable JavaScript in your web browser</a>.
+    </noscript>
+    <p>Input your AWS EC2/LoadBalancer URL</p>
+    <form action="viewResults" method="post">
+        <input type="text" name="url" id="url" required>
+        <input type="submit" value="Submit">
+</body>
+
+</html>
\ No newline at end of file
diff --git a/public/results.html b/public/results.html
new file mode 100644
index 0000000000000000000000000000000000000000..d7b4c560c5964c24905f3616bae682ad0e5632b5
--- /dev/null
+++ b/public/results.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>SE464 Lab Test Results</title>
+    <style>
+        table {
+            border-collapse: collapse;
+        }
+
+        th,
+        td {
+            border: 1px solid #dddddd;
+            text-align: left;
+            padding: 8px;
+        }
+
+        .success {
+            color: green;
+        }
+
+        .fail {
+            color: orange;
+        }
+
+        tr:nth-child(even) {
+            background-color: #f2f2f2;
+        }
+    </style>
+</head>
+
+<body>
+    <h1>Results for SE464 <span id="aws-url"></span></h1>
+    <div>
+        <label for="iterations">Rerun with number of iterations:</label>
+        <input type="text" id="iterations">
+        <button type="button">Submit</button>
+    </div>
+    <div id="results"></div>
+</body>
+
+<script>
+    const urlParts = window.location.href.split('/');
+    const id = urlParts[urlParts.length - 1]
+    const awsUrl = atob(id);
+    const titleHeading = document.getElementById('aws-url');
+    titleHeading.innerHTML = `${awsUrl}`;
+
+    const socket = new WebSocket(`/results/${id}`);
+
+    // Listen for messages
+    socket.addEventListener("message", (event) => {
+        try {
+            const data = JSON.parse(event.data);
+            const restJsonData = data["rest"];
+            const grpcJsonData = data["grpc"];
+            document.getElementById('results').replaceChildren(jsonToTable("REST", restJsonData));
+            document.getElementById('results').appendChild(jsonToTable("gRPC", grpcJsonData));
+        } catch (e) {
+            console.error(e);
+        }
+    });
+
+    document.querySelector('button').addEventListener('click', () => {
+        const iterations = document.getElementById('iterations').value;
+        socket.send(iterations);
+    });
+
+    function jsonToTable(title, jsonData) {
+        // Create a table
+        const div = document.createElement('div');
+        const titleElement = document.createElement('h2');
+        titleElement.textContent = title;
+        div.appendChild(titleElement);
+
+        const table = document.createElement('table');
+
+        for (const [k, v] of Object.entries(jsonData)) {
+            const row = document.createElement('tr');
+            const td1 = document.createElement('td');
+            const td2 = document.createElement('td');
+            td1.textContent = k;
+            td2.textContent = v["time"] + ' ms';
+            td2.className = v["ok"] ? 'success' : 'fail';
+            row.appendChild(td1);
+            row.appendChild(td2);
+
+            table.appendChild(row);
+        }
+
+        div.appendChild(table);
+
+        // Return the table
+        return div;
+    }
+
+</script>
+
+</html>
\ No newline at end of file
diff --git a/rest.txt b/rest.txt
deleted file mode 100644
index 05c8fe891c839b2f452431ed9336801e5b59adfc..0000000000000000000000000000000000000000
--- a/rest.txt
+++ /dev/null
@@ -1,21 +0,0 @@
------REST TESTS-----
-randomProduct
-50.12581658363342
-product
-33.34259166717529
-allProducts
-34.9744167804718
-categories
-32.45142498016357
-allOrders
-62.56571636199951
-ordersByUser
-52.71434187889099
-user
-31.619408416748048
-allUsers
-31.58545837402344
-postOrder
-36.04820818901062
-patchUser
-35.95475001335144
\ No newline at end of file
diff --git a/restTests.js b/restTests.js
deleted file mode 100644
index 00dd82324658522847e929204e1df9308420e3aa..0000000000000000000000000000000000000000
--- a/restTests.js
+++ /dev/null
@@ -1,129 +0,0 @@
-const getTests = (endpoint, user, product) => [
-  //  {
-  //    run: async () => {
-  //      const resp = await fetch(endpoint + "/");
-  //      const body = await resp.json();
-  //      return [resp.status, body];
-  //    },
-  //  },
-  {
-    name: "randomProduct",
-    run: async () => {
-      const resp = await fetch(endpoint + "/randomproduct");
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "product",
-    run: async () => {
-      const resp = await fetch(
-        `${endpoint}/product/${product}`,
-      );
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "allProducts",
-    run: async () => {
-      const resp = await fetch(endpoint + "/products");
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "categories",
-    run: async () => {
-      const resp = await fetch(endpoint + "/categories");
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "allOrders",
-    run: async () => {
-      const resp = await fetch(endpoint + "/allorders");
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "ordersByUser",
-    run: async () => {
-      const resp = await fetch(`${endpoint}/orders?userId=${user}`);
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "user",
-    run: async () => {
-      const resp = await fetch(
-        `${endpoint}/user/${user}`,
-      );
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "allUsers",
-    run: async () => {
-      const resp = await fetch(endpoint + "/users");
-      const body = await resp.json();
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "postOrder",
-    run: async () => {
-      const resp = await fetch(endpoint + "/orders", {
-        method: "POST",
-        headers: {
-          Accept: "application/json",
-          "Content-Type": "application/json",
-        },
-        body: JSON.stringify({
-          user_id: user,
-          products: [
-            {
-              product_id: product,
-              quantity: 2,
-            },
-          ],
-          total_amount: 600.0,
-        }),
-      });
-
-      const body = null;
-
-      return [resp.status, body];
-    },
-  },
-  {
-    name: "patchUser",
-    run: async () => {
-      const resp = await fetch(
-        endpoint + `/user/${user}`,
-        {
-          method: "PATCH",
-          headers: {
-            Accept: "application/json",
-            "Content-Type": "application/json",
-          },
-          body: JSON.stringify({
-            id: user,
-            email: "update@test.com",
-            name: "update test",
-          }),
-        },
-      );
-
-      const body = null;
-
-      return [resp.status, body];
-    },
-  },
-];
-
-module.exports = { getTests };
diff --git a/src/compiled_proto/app.ts b/src/compiled_proto/app.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3662a5cac1f73daa3195ba33d2eedc46ca69c8c4
--- /dev/null
+++ b/src/compiled_proto/app.ts
@@ -0,0 +1,1316 @@
+/* eslint-disable */
+import type { CallContext, CallOptions } from "nice-grpc-common";
+import * as _m0 from "protobufjs/minimal";
+import Long = require("long");
+
+export const protobufPackage = "";
+
+export interface EmptyRequest {
+}
+
+export interface AllProductsRequest {
+  categoryId?: string | undefined;
+}
+
+export interface ProductRequest {
+  productId: string;
+}
+
+export interface Product {
+  categoryId: string;
+  stock: number;
+  price: number;
+  description: string;
+  id: string;
+  name: string;
+}
+
+export interface Products {
+  products: Product[];
+}
+
+export interface OrderProduct {
+  productId: string;
+  quantity: number;
+}
+
+export interface Order {
+  userId: string;
+  id: string;
+  products: OrderProduct[];
+  totalAmount: number;
+}
+
+export interface OrderRequest {
+  id: string;
+}
+
+export interface Orders {
+  orders: Order[];
+}
+
+export interface Category {
+  description: string;
+  id: string;
+  price: number;
+  name: string;
+}
+
+export interface Categories {
+  categories: Category[];
+}
+
+/** Users */
+export interface UserRequest {
+  id: string;
+}
+
+export interface UserPatchRequest {
+  /** We assign user a unique ID, this shouldn't be changed. Following fields can be changed */
+  id: string;
+  email: string;
+  password: string;
+}
+
+export interface User {
+  id: string;
+  email: string;
+}
+
+export interface Users {
+  users: User[];
+}
+
+function createBaseEmptyRequest(): EmptyRequest {
+  return {};
+}
+
+export const EmptyRequest = {
+  encode(_: EmptyRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): EmptyRequest {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseEmptyRequest();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(_: any): EmptyRequest {
+    return {};
+  },
+
+  toJSON(_: EmptyRequest): unknown {
+    const obj: any = {};
+    return obj;
+  },
+
+  create(base?: DeepPartial<EmptyRequest>): EmptyRequest {
+    return EmptyRequest.fromPartial(base ?? {});
+  },
+  fromPartial(_: DeepPartial<EmptyRequest>): EmptyRequest {
+    const message = createBaseEmptyRequest();
+    return message;
+  },
+};
+
+function createBaseAllProductsRequest(): AllProductsRequest {
+  return { categoryId: undefined };
+}
+
+export const AllProductsRequest = {
+  encode(message: AllProductsRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.categoryId !== undefined) {
+      writer.uint32(10).string(message.categoryId);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): AllProductsRequest {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseAllProductsRequest();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.categoryId = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): AllProductsRequest {
+    return { categoryId: isSet(object.categoryId) ? globalThis.String(object.categoryId) : undefined };
+  },
+
+  toJSON(message: AllProductsRequest): unknown {
+    const obj: any = {};
+    if (message.categoryId !== undefined) {
+      obj.categoryId = message.categoryId;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<AllProductsRequest>): AllProductsRequest {
+    return AllProductsRequest.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<AllProductsRequest>): AllProductsRequest {
+    const message = createBaseAllProductsRequest();
+    message.categoryId = object.categoryId ?? undefined;
+    return message;
+  },
+};
+
+function createBaseProductRequest(): ProductRequest {
+  return { productId: "" };
+}
+
+export const ProductRequest = {
+  encode(message: ProductRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.productId !== "") {
+      writer.uint32(10).string(message.productId);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): ProductRequest {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseProductRequest();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.productId = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): ProductRequest {
+    return { productId: isSet(object.productId) ? globalThis.String(object.productId) : "" };
+  },
+
+  toJSON(message: ProductRequest): unknown {
+    const obj: any = {};
+    if (message.productId !== "") {
+      obj.productId = message.productId;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<ProductRequest>): ProductRequest {
+    return ProductRequest.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<ProductRequest>): ProductRequest {
+    const message = createBaseProductRequest();
+    message.productId = object.productId ?? "";
+    return message;
+  },
+};
+
+function createBaseProduct(): Product {
+  return { categoryId: "", stock: 0, price: 0, description: "", id: "", name: "" };
+}
+
+export const Product = {
+  encode(message: Product, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.categoryId !== "") {
+      writer.uint32(10).string(message.categoryId);
+    }
+    if (message.stock !== 0) {
+      writer.uint32(24).int64(message.stock);
+    }
+    if (message.price !== 0) {
+      writer.uint32(32).int64(message.price);
+    }
+    if (message.description !== "") {
+      writer.uint32(42).string(message.description);
+    }
+    if (message.id !== "") {
+      writer.uint32(50).string(message.id);
+    }
+    if (message.name !== "") {
+      writer.uint32(58).string(message.name);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): Product {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseProduct();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.categoryId = reader.string();
+          continue;
+        case 3:
+          if (tag !== 24) {
+            break;
+          }
+
+          message.stock = longToNumber(reader.int64() as Long);
+          continue;
+        case 4:
+          if (tag !== 32) {
+            break;
+          }
+
+          message.price = longToNumber(reader.int64() as Long);
+          continue;
+        case 5:
+          if (tag !== 42) {
+            break;
+          }
+
+          message.description = reader.string();
+          continue;
+        case 6:
+          if (tag !== 50) {
+            break;
+          }
+
+          message.id = reader.string();
+          continue;
+        case 7:
+          if (tag !== 58) {
+            break;
+          }
+
+          message.name = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): Product {
+    return {
+      categoryId: isSet(object.categoryId) ? globalThis.String(object.categoryId) : "",
+      stock: isSet(object.stock) ? globalThis.Number(object.stock) : 0,
+      price: isSet(object.price) ? globalThis.Number(object.price) : 0,
+      description: isSet(object.description) ? globalThis.String(object.description) : "",
+      id: isSet(object.id) ? globalThis.String(object.id) : "",
+      name: isSet(object.name) ? globalThis.String(object.name) : "",
+    };
+  },
+
+  toJSON(message: Product): unknown {
+    const obj: any = {};
+    if (message.categoryId !== "") {
+      obj.categoryId = message.categoryId;
+    }
+    if (message.stock !== 0) {
+      obj.stock = Math.round(message.stock);
+    }
+    if (message.price !== 0) {
+      obj.price = Math.round(message.price);
+    }
+    if (message.description !== "") {
+      obj.description = message.description;
+    }
+    if (message.id !== "") {
+      obj.id = message.id;
+    }
+    if (message.name !== "") {
+      obj.name = message.name;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<Product>): Product {
+    return Product.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<Product>): Product {
+    const message = createBaseProduct();
+    message.categoryId = object.categoryId ?? "";
+    message.stock = object.stock ?? 0;
+    message.price = object.price ?? 0;
+    message.description = object.description ?? "";
+    message.id = object.id ?? "";
+    message.name = object.name ?? "";
+    return message;
+  },
+};
+
+function createBaseProducts(): Products {
+  return { products: [] };
+}
+
+export const Products = {
+  encode(message: Products, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    for (const v of message.products) {
+      Product.encode(v!, writer.uint32(10).fork()).ldelim();
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): Products {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseProducts();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.products.push(Product.decode(reader, reader.uint32()));
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): Products {
+    return {
+      products: globalThis.Array.isArray(object?.products) ? object.products.map((e: any) => Product.fromJSON(e)) : [],
+    };
+  },
+
+  toJSON(message: Products): unknown {
+    const obj: any = {};
+    if (message.products?.length) {
+      obj.products = message.products.map((e) => Product.toJSON(e));
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<Products>): Products {
+    return Products.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<Products>): Products {
+    const message = createBaseProducts();
+    message.products = object.products?.map((e) => Product.fromPartial(e)) || [];
+    return message;
+  },
+};
+
+function createBaseOrderProduct(): OrderProduct {
+  return { productId: "", quantity: 0 };
+}
+
+export const OrderProduct = {
+  encode(message: OrderProduct, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.productId !== "") {
+      writer.uint32(10).string(message.productId);
+    }
+    if (message.quantity !== 0) {
+      writer.uint32(16).int64(message.quantity);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): OrderProduct {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseOrderProduct();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.productId = reader.string();
+          continue;
+        case 2:
+          if (tag !== 16) {
+            break;
+          }
+
+          message.quantity = longToNumber(reader.int64() as Long);
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): OrderProduct {
+    return {
+      productId: isSet(object.productId) ? globalThis.String(object.productId) : "",
+      quantity: isSet(object.quantity) ? globalThis.Number(object.quantity) : 0,
+    };
+  },
+
+  toJSON(message: OrderProduct): unknown {
+    const obj: any = {};
+    if (message.productId !== "") {
+      obj.productId = message.productId;
+    }
+    if (message.quantity !== 0) {
+      obj.quantity = Math.round(message.quantity);
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<OrderProduct>): OrderProduct {
+    return OrderProduct.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<OrderProduct>): OrderProduct {
+    const message = createBaseOrderProduct();
+    message.productId = object.productId ?? "";
+    message.quantity = object.quantity ?? 0;
+    return message;
+  },
+};
+
+function createBaseOrder(): Order {
+  return { userId: "", id: "", products: [], totalAmount: 0 };
+}
+
+export const Order = {
+  encode(message: Order, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.userId !== "") {
+      writer.uint32(10).string(message.userId);
+    }
+    if (message.id !== "") {
+      writer.uint32(18).string(message.id);
+    }
+    for (const v of message.products) {
+      OrderProduct.encode(v!, writer.uint32(26).fork()).ldelim();
+    }
+    if (message.totalAmount !== 0) {
+      writer.uint32(32).int64(message.totalAmount);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): Order {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseOrder();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.userId = reader.string();
+          continue;
+        case 2:
+          if (tag !== 18) {
+            break;
+          }
+
+          message.id = reader.string();
+          continue;
+        case 3:
+          if (tag !== 26) {
+            break;
+          }
+
+          message.products.push(OrderProduct.decode(reader, reader.uint32()));
+          continue;
+        case 4:
+          if (tag !== 32) {
+            break;
+          }
+
+          message.totalAmount = longToNumber(reader.int64() as Long);
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): Order {
+    return {
+      userId: isSet(object.userId) ? globalThis.String(object.userId) : "",
+      id: isSet(object.id) ? globalThis.String(object.id) : "",
+      products: globalThis.Array.isArray(object?.products)
+        ? object.products.map((e: any) => OrderProduct.fromJSON(e))
+        : [],
+      totalAmount: isSet(object.totalAmount) ? globalThis.Number(object.totalAmount) : 0,
+    };
+  },
+
+  toJSON(message: Order): unknown {
+    const obj: any = {};
+    if (message.userId !== "") {
+      obj.userId = message.userId;
+    }
+    if (message.id !== "") {
+      obj.id = message.id;
+    }
+    if (message.products?.length) {
+      obj.products = message.products.map((e) => OrderProduct.toJSON(e));
+    }
+    if (message.totalAmount !== 0) {
+      obj.totalAmount = Math.round(message.totalAmount);
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<Order>): Order {
+    return Order.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<Order>): Order {
+    const message = createBaseOrder();
+    message.userId = object.userId ?? "";
+    message.id = object.id ?? "";
+    message.products = object.products?.map((e) => OrderProduct.fromPartial(e)) || [];
+    message.totalAmount = object.totalAmount ?? 0;
+    return message;
+  },
+};
+
+function createBaseOrderRequest(): OrderRequest {
+  return { id: "" };
+}
+
+export const OrderRequest = {
+  encode(message: OrderRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.id !== "") {
+      writer.uint32(10).string(message.id);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): OrderRequest {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseOrderRequest();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.id = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): OrderRequest {
+    return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
+  },
+
+  toJSON(message: OrderRequest): unknown {
+    const obj: any = {};
+    if (message.id !== "") {
+      obj.id = message.id;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<OrderRequest>): OrderRequest {
+    return OrderRequest.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<OrderRequest>): OrderRequest {
+    const message = createBaseOrderRequest();
+    message.id = object.id ?? "";
+    return message;
+  },
+};
+
+function createBaseOrders(): Orders {
+  return { orders: [] };
+}
+
+export const Orders = {
+  encode(message: Orders, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    for (const v of message.orders) {
+      Order.encode(v!, writer.uint32(10).fork()).ldelim();
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): Orders {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseOrders();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.orders.push(Order.decode(reader, reader.uint32()));
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): Orders {
+    return { orders: globalThis.Array.isArray(object?.orders) ? object.orders.map((e: any) => Order.fromJSON(e)) : [] };
+  },
+
+  toJSON(message: Orders): unknown {
+    const obj: any = {};
+    if (message.orders?.length) {
+      obj.orders = message.orders.map((e) => Order.toJSON(e));
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<Orders>): Orders {
+    return Orders.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<Orders>): Orders {
+    const message = createBaseOrders();
+    message.orders = object.orders?.map((e) => Order.fromPartial(e)) || [];
+    return message;
+  },
+};
+
+function createBaseCategory(): Category {
+  return { description: "", id: "", price: 0, name: "" };
+}
+
+export const Category = {
+  encode(message: Category, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.description !== "") {
+      writer.uint32(10).string(message.description);
+    }
+    if (message.id !== "") {
+      writer.uint32(18).string(message.id);
+    }
+    if (message.price !== 0) {
+      writer.uint32(24).int64(message.price);
+    }
+    if (message.name !== "") {
+      writer.uint32(34).string(message.name);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): Category {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseCategory();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.description = reader.string();
+          continue;
+        case 2:
+          if (tag !== 18) {
+            break;
+          }
+
+          message.id = reader.string();
+          continue;
+        case 3:
+          if (tag !== 24) {
+            break;
+          }
+
+          message.price = longToNumber(reader.int64() as Long);
+          continue;
+        case 4:
+          if (tag !== 34) {
+            break;
+          }
+
+          message.name = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): Category {
+    return {
+      description: isSet(object.description) ? globalThis.String(object.description) : "",
+      id: isSet(object.id) ? globalThis.String(object.id) : "",
+      price: isSet(object.price) ? globalThis.Number(object.price) : 0,
+      name: isSet(object.name) ? globalThis.String(object.name) : "",
+    };
+  },
+
+  toJSON(message: Category): unknown {
+    const obj: any = {};
+    if (message.description !== "") {
+      obj.description = message.description;
+    }
+    if (message.id !== "") {
+      obj.id = message.id;
+    }
+    if (message.price !== 0) {
+      obj.price = Math.round(message.price);
+    }
+    if (message.name !== "") {
+      obj.name = message.name;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<Category>): Category {
+    return Category.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<Category>): Category {
+    const message = createBaseCategory();
+    message.description = object.description ?? "";
+    message.id = object.id ?? "";
+    message.price = object.price ?? 0;
+    message.name = object.name ?? "";
+    return message;
+  },
+};
+
+function createBaseCategories(): Categories {
+  return { categories: [] };
+}
+
+export const Categories = {
+  encode(message: Categories, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    for (const v of message.categories) {
+      Category.encode(v!, writer.uint32(10).fork()).ldelim();
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): Categories {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseCategories();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.categories.push(Category.decode(reader, reader.uint32()));
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): Categories {
+    return {
+      categories: globalThis.Array.isArray(object?.categories)
+        ? object.categories.map((e: any) => Category.fromJSON(e))
+        : [],
+    };
+  },
+
+  toJSON(message: Categories): unknown {
+    const obj: any = {};
+    if (message.categories?.length) {
+      obj.categories = message.categories.map((e) => Category.toJSON(e));
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<Categories>): Categories {
+    return Categories.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<Categories>): Categories {
+    const message = createBaseCategories();
+    message.categories = object.categories?.map((e) => Category.fromPartial(e)) || [];
+    return message;
+  },
+};
+
+function createBaseUserRequest(): UserRequest {
+  return { id: "" };
+}
+
+export const UserRequest = {
+  encode(message: UserRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.id !== "") {
+      writer.uint32(10).string(message.id);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): UserRequest {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseUserRequest();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.id = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): UserRequest {
+    return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
+  },
+
+  toJSON(message: UserRequest): unknown {
+    const obj: any = {};
+    if (message.id !== "") {
+      obj.id = message.id;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<UserRequest>): UserRequest {
+    return UserRequest.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<UserRequest>): UserRequest {
+    const message = createBaseUserRequest();
+    message.id = object.id ?? "";
+    return message;
+  },
+};
+
+function createBaseUserPatchRequest(): UserPatchRequest {
+  return { id: "", email: "", password: "" };
+}
+
+export const UserPatchRequest = {
+  encode(message: UserPatchRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.id !== "") {
+      writer.uint32(10).string(message.id);
+    }
+    if (message.email !== "") {
+      writer.uint32(18).string(message.email);
+    }
+    if (message.password !== "") {
+      writer.uint32(26).string(message.password);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): UserPatchRequest {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseUserPatchRequest();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.id = reader.string();
+          continue;
+        case 2:
+          if (tag !== 18) {
+            break;
+          }
+
+          message.email = reader.string();
+          continue;
+        case 3:
+          if (tag !== 26) {
+            break;
+          }
+
+          message.password = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): UserPatchRequest {
+    return {
+      id: isSet(object.id) ? globalThis.String(object.id) : "",
+      email: isSet(object.email) ? globalThis.String(object.email) : "",
+      password: isSet(object.password) ? globalThis.String(object.password) : "",
+    };
+  },
+
+  toJSON(message: UserPatchRequest): unknown {
+    const obj: any = {};
+    if (message.id !== "") {
+      obj.id = message.id;
+    }
+    if (message.email !== "") {
+      obj.email = message.email;
+    }
+    if (message.password !== "") {
+      obj.password = message.password;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<UserPatchRequest>): UserPatchRequest {
+    return UserPatchRequest.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<UserPatchRequest>): UserPatchRequest {
+    const message = createBaseUserPatchRequest();
+    message.id = object.id ?? "";
+    message.email = object.email ?? "";
+    message.password = object.password ?? "";
+    return message;
+  },
+};
+
+function createBaseUser(): User {
+  return { id: "", email: "" };
+}
+
+export const User = {
+  encode(message: User, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.id !== "") {
+      writer.uint32(10).string(message.id);
+    }
+    if (message.email !== "") {
+      writer.uint32(18).string(message.email);
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): User {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseUser();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.id = reader.string();
+          continue;
+        case 2:
+          if (tag !== 18) {
+            break;
+          }
+
+          message.email = reader.string();
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): User {
+    return {
+      id: isSet(object.id) ? globalThis.String(object.id) : "",
+      email: isSet(object.email) ? globalThis.String(object.email) : "",
+    };
+  },
+
+  toJSON(message: User): unknown {
+    const obj: any = {};
+    if (message.id !== "") {
+      obj.id = message.id;
+    }
+    if (message.email !== "") {
+      obj.email = message.email;
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<User>): User {
+    return User.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<User>): User {
+    const message = createBaseUser();
+    message.id = object.id ?? "";
+    message.email = object.email ?? "";
+    return message;
+  },
+};
+
+function createBaseUsers(): Users {
+  return { users: [] };
+}
+
+export const Users = {
+  encode(message: Users, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    for (const v of message.users) {
+      User.encode(v!, writer.uint32(10).fork()).ldelim();
+    }
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): Users {
+    const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseUsers();
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:
+          if (tag !== 10) {
+            break;
+          }
+
+          message.users.push(User.decode(reader, reader.uint32()));
+          continue;
+      }
+      if ((tag & 7) === 4 || tag === 0) {
+        break;
+      }
+      reader.skipType(tag & 7);
+    }
+    return message;
+  },
+
+  fromJSON(object: any): Users {
+    return { users: globalThis.Array.isArray(object?.users) ? object.users.map((e: any) => User.fromJSON(e)) : [] };
+  },
+
+  toJSON(message: Users): unknown {
+    const obj: any = {};
+    if (message.users?.length) {
+      obj.users = message.users.map((e) => User.toJSON(e));
+    }
+    return obj;
+  },
+
+  create(base?: DeepPartial<Users>): Users {
+    return Users.fromPartial(base ?? {});
+  },
+  fromPartial(object: DeepPartial<Users>): Users {
+    const message = createBaseUsers();
+    message.users = object.users?.map((e) => User.fromPartial(e)) || [];
+    return message;
+  },
+};
+
+export type AppDefinition = typeof AppDefinition;
+export const AppDefinition = {
+  name: "App",
+  fullName: "App",
+  methods: {
+    getProduct: {
+      name: "GetProduct",
+      requestType: ProductRequest,
+      requestStream: false,
+      responseType: Product,
+      responseStream: false,
+      options: {},
+    },
+    getRandomProduct: {
+      name: "GetRandomProduct",
+      requestType: EmptyRequest,
+      requestStream: false,
+      responseType: Product,
+      responseStream: false,
+      options: {},
+    },
+    getAllProducts: {
+      name: "GetAllProducts",
+      requestType: AllProductsRequest,
+      requestStream: false,
+      responseType: Products,
+      responseStream: false,
+      options: {},
+    },
+    getAllCategories: {
+      name: "GetAllCategories",
+      requestType: EmptyRequest,
+      requestStream: false,
+      responseType: Categories,
+      responseStream: false,
+      options: {},
+    },
+    getAllOrders: {
+      name: "GetAllOrders",
+      requestType: EmptyRequest,
+      requestStream: false,
+      responseType: Orders,
+      responseStream: false,
+      options: {},
+    },
+    getAllUserOrders: {
+      name: "GetAllUserOrders",
+      requestType: UserRequest,
+      requestStream: false,
+      responseType: Orders,
+      responseStream: false,
+      options: {},
+    },
+    getOrder: {
+      name: "GetOrder",
+      requestType: OrderRequest,
+      requestStream: false,
+      responseType: Order,
+      responseStream: false,
+      options: {},
+    },
+    getUser: {
+      name: "GetUser",
+      requestType: UserRequest,
+      requestStream: false,
+      responseType: User,
+      responseStream: false,
+      options: {},
+    },
+    getAllUsers: {
+      name: "GetAllUsers",
+      requestType: EmptyRequest,
+      requestStream: false,
+      responseType: Users,
+      responseStream: false,
+      options: {},
+    },
+    postOrder: {
+      name: "PostOrder",
+      requestType: Order,
+      requestStream: false,
+      responseType: Order,
+      responseStream: false,
+      options: {},
+    },
+    patchAccountDetails: {
+      name: "PatchAccountDetails",
+      requestType: UserPatchRequest,
+      requestStream: false,
+      responseType: User,
+      responseStream: false,
+      options: {},
+    },
+  },
+} as const;
+
+export interface AppServiceImplementation<CallContextExt = {}> {
+  getProduct(request: ProductRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Product>>;
+  getRandomProduct(request: EmptyRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Product>>;
+  getAllProducts(request: AllProductsRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Products>>;
+  getAllCategories(request: EmptyRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Categories>>;
+  getAllOrders(request: EmptyRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Orders>>;
+  getAllUserOrders(request: UserRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Orders>>;
+  getOrder(request: OrderRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Order>>;
+  getUser(request: UserRequest, context: CallContext & CallContextExt): Promise<DeepPartial<User>>;
+  getAllUsers(request: EmptyRequest, context: CallContext & CallContextExt): Promise<DeepPartial<Users>>;
+  postOrder(request: Order, context: CallContext & CallContextExt): Promise<DeepPartial<Order>>;
+  patchAccountDetails(request: UserPatchRequest, context: CallContext & CallContextExt): Promise<DeepPartial<User>>;
+}
+
+export interface AppClient<CallOptionsExt = {}> {
+  getProduct(request: DeepPartial<ProductRequest>, options?: CallOptions & CallOptionsExt): Promise<Product>;
+  getRandomProduct(request: DeepPartial<EmptyRequest>, options?: CallOptions & CallOptionsExt): Promise<Product>;
+  getAllProducts(request: DeepPartial<AllProductsRequest>, options?: CallOptions & CallOptionsExt): Promise<Products>;
+  getAllCategories(request: DeepPartial<EmptyRequest>, options?: CallOptions & CallOptionsExt): Promise<Categories>;
+  getAllOrders(request: DeepPartial<EmptyRequest>, options?: CallOptions & CallOptionsExt): Promise<Orders>;
+  getAllUserOrders(request: DeepPartial<UserRequest>, options?: CallOptions & CallOptionsExt): Promise<Orders>;
+  getOrder(request: DeepPartial<OrderRequest>, options?: CallOptions & CallOptionsExt): Promise<Order>;
+  getUser(request: DeepPartial<UserRequest>, options?: CallOptions & CallOptionsExt): Promise<User>;
+  getAllUsers(request: DeepPartial<EmptyRequest>, options?: CallOptions & CallOptionsExt): Promise<Users>;
+  postOrder(request: DeepPartial<Order>, options?: CallOptions & CallOptionsExt): Promise<Order>;
+  patchAccountDetails(request: DeepPartial<UserPatchRequest>, options?: CallOptions & CallOptionsExt): Promise<User>;
+}
+
+type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
+
+export type DeepPartial<T> = T extends Builtin ? T
+  : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
+  : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
+  : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
+  : Partial<T>;
+
+function longToNumber(long: Long): number {
+  if (long.gt(globalThis.Number.MAX_SAFE_INTEGER)) {
+    throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
+  }
+  return long.toNumber();
+}
+
+if (_m0.util.Long !== Long) {
+  _m0.util.Long = Long as any;
+  _m0.configure();
+}
+
+function isSet(value: any): boolean {
+  return value !== null && value !== undefined;
+}
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c662cb677bd9f09c87dc722011ef1ea77add850f
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,33 @@
+const constants = {
+    TEST_USER_ID: "5b4b67e2-3b09-4309-8de2-82cc469fc1cc",
+    TEST_PRODUCT_ID: "dc878081-060b-4a6c-9038-02069f1ecef8",
+    TEST_ORDER_ID: "f38c26bf-8583-4e6f-88e9-0492490e47be",
+    TEST_ORDER: {
+        userId: "05843a96-1d34-43fb-88ca-0567e2f39ef6",
+        products: [{ productId: "0d00fb9d-0733-4c57-956f-85d52c1d14ee", quantity: 1 }],
+        totalAmount: 8
+    },
+    TEST_UPDATE: {
+        id: "05843a96-1d34-43fb-88ca-0567e2f39ef6",
+        password: "test123"
+    },
+
+    EXPECTED_USER: {
+        id: "5b4b67e2-3b09-4309-8de2-82cc469fc1cc",
+        email: "JamesWilliams@gmail.com",
+    },
+    EXPECTED_PRODUCT: {
+        id: "dc878081-060b-4a6c-9038-02069f1ecef8",
+        stock: 167,
+        price: 12,
+        name: "Shoes",
+        description: "Shoes for sale",
+        categoryId: "44adf4f9-cdd3-49ba-9788-eaed531580de"
+    },
+    EXPECTED_ORDER: {
+        id: "f38c26bf-8583-4e6f-88e9-0492490e47be",
+        products: [{ "productId": "d3e11810-8b5a-4468-a781-84109dd7569a", "quantity": 2 }]
+    },
+};
+
+export default constants;
\ No newline at end of file
diff --git a/src/interfaces.ts b/src/interfaces.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df0f3d35995ee390d9481262737dc82f11299541
--- /dev/null
+++ b/src/interfaces.ts
@@ -0,0 +1,17 @@
+import { UserPatchRequest } from "./compiled_proto/app";
+import { Category, Order, Product, TestResults, User } from "./types";
+
+export interface ITestSuite {
+    testRandomProduct(): Promise<Product>;
+    testUserById(id: string): Promise<User>;
+    testAllProducts(categoryId?: string): Promise<Product[]>;
+    testProductById(productId: string): Promise<Product>;
+    testAllCategories(): Promise<Category[]>;
+    testAllOrders(): Promise<Order[]>;
+    testOrdersByUser(id: string): Promise<Order[]>;
+    testOrderById(id: string): Promise<Order>;
+    testAllUsers(): Promise<User[]>;
+    testInsertOrder(order: Order): Promise<void>;
+    updateUser(patch: UserPatchRequest): Promise<void>;
+    runSuite(iterations: number): Promise<TestResults>;
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2b9802ff828363d6d224b42ed6c53a9e54408e64
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,43 @@
+import express from 'express';
+import expressWs from 'express-ws';
+import path from 'path';
+import { runTests } from './testing';
+
+const { app } = expressWs(express());
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+
+const resultsCache = {};
+
+app.get('/', (req, res) => {
+    res.sendFile(path.join(__dirname, 'index.html'));
+});
+
+app.post('/viewResults', (req, res) => {
+    const url = req.body.url;
+    const id = btoa(url);
+    res.redirect(`/viewResults/${id}`);
+});
+
+app.get('/viewResults/:id', (req, res) => {
+    res.sendFile(path.join(__dirname, 'results.html'));
+});
+
+app.ws('/results/:id', async (ws, req) => {
+    const awsUrl = atob(req.params.id);
+    const iterations: number = Number(req.query.iterations) || 1;
+    if (resultsCache[awsUrl]) {
+        ws.send(JSON.stringify(resultsCache[awsUrl]));
+    } else {
+        runTests(awsUrl, resultsCache, iterations, ws);
+    }
+
+    ws.onmessage = (msg) => {
+        const iterations = Number(msg.data);
+        runTests(awsUrl, resultsCache, iterations, ws);
+    };
+});
+
+app.listen(80, () => {
+    console.log('Server is running on port 80');
+});
diff --git a/app.proto b/src/proto/app.proto
similarity index 92%
rename from app.proto
rename to src/proto/app.proto
index 98ff9b97eff6d0ffc36c7859208e93a205db2b96..f223b1646d677346864f9f6b20002ee7c8876f72 100644
--- a/app.proto
+++ b/src/proto/app.proto
@@ -5,7 +5,7 @@ service App {
 
     rpc GetRandomProduct (EmptyRequest) returns (Product) {}
 
-    rpc GetAllProducts (EmptyRequest) returns (Products) {}
+    rpc GetAllProducts (AllProductsRequest) returns (Products) {}
 
     rpc GetAllCategories (EmptyRequest) returns (Categories) {}
 
@@ -28,13 +28,16 @@ message EmptyRequest {}
 
 // Products
 
+message AllProductsRequest {
+    optional string category_id = 1;
+}
+
 message ProductRequest {
     string product_id = 1;
 }
 
 message Product {
     string category_id = 1;
-    string images = 2;
     int64 stock = 3;
     int64 price = 4;
     string description = 5;
diff --git a/src/testing.ts b/src/testing.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4971c74a78f3eed43ad2d5f15e46f1607b757ada
--- /dev/null
+++ b/src/testing.ts
@@ -0,0 +1,44 @@
+import RestTestSuite from "./tests/restTests";
+import GrpcTestSuite from "./tests/grpcTests";
+import { performance } from "perf_hooks";
+import { TestResult } from "./types";
+
+const runTest: (func: () => any, expected?: any) => Promise<TestResult> = async (func: () => any, expected?: any) => {
+  const start = performance.now();
+  const result = await func();
+  const end = performance.now();
+
+  return {
+    ok: expected != null ? JSON.stringify(result) === JSON.stringify(expected) : true,
+    time: end - start
+  };
+}
+
+export const avgRuntime: (func: () => any, iterations: number, expected?: any) => Promise<TestResult> = async (func: () => any, iterations: number, expected?: any) => {
+  const promises = [];
+  for (let i = 0; i < iterations; i++) {
+    promises.push(runTest(func, expected));
+  }
+
+  const results = await Promise.all(promises);
+  return {
+    ok: results.reduce((acc, curr) => acc && curr.ok, true),
+    time: results.reduce((acc, curr) => acc + curr.time, 0) / iterations
+  };
+};
+
+
+export async function runTests(awsUrl: string, resultCache: any, iterations: number, ws?: any) {
+  const restTestSuite = new RestTestSuite(awsUrl);
+  const grpcTestSuite = new GrpcTestSuite(awsUrl);
+
+  // Warmup
+  await restTestSuite.runSuite(1);
+  await grpcTestSuite.runSuite(1);
+  resultCache[awsUrl] = {};
+  resultCache[awsUrl]["rest"] = await restTestSuite.runSuite(iterations);
+  resultCache[awsUrl]["grpc"] = await grpcTestSuite.runSuite(iterations);
+
+  if (ws)
+    ws.send(JSON.stringify(resultCache[awsUrl]));
+}
diff --git a/src/tests/grpcTests.ts b/src/tests/grpcTests.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0fc200b5b6e52494935ca8cf4a8f9c6486ddf95a
--- /dev/null
+++ b/src/tests/grpcTests.ts
@@ -0,0 +1,189 @@
+import { ITestSuite } from "../interfaces";
+import { AppClient, AppDefinition } from "../compiled_proto/app";
+import { createChannel, createClient } from 'nice-grpc';
+import { Order, TestResults, UserPatchRequest } from "../types";
+import { avgRuntime } from "../testing";
+import constants from "../constants";
+import { uuid } from "uuidv4";
+
+export default class GrpcTestSuite implements ITestSuite {
+    private channel: any;
+    private client: AppClient;
+
+    constructor(ip: string) {
+        this.channel = createChannel(`${ip}:3001`);
+        this.client = createClient(AppDefinition, this.channel);
+    }
+
+    public async testRandomProduct(): Promise<any> {
+        try {
+            const data = await this.client.getRandomProduct({});
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testUserById(id: string): Promise<any> {
+        try {
+            const data = await this.client.getUser({ id });
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testAllProducts(categoryId?: string): Promise<any> {
+        try {
+            const data = await this.client.getAllProducts({ categoryId });
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testProductById(productId: string): Promise<any> {
+        try {
+            const data = await this.client.getProduct({ productId });
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+
+    }
+
+    public async testAllCategories(): Promise<any> {
+        try {
+            const data = await this.client.getAllCategories({});
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testAllOrders(): Promise<any> {
+        try {
+            const data = await this.client.getAllOrders({});
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testOrdersByUser(id: string): Promise<any> {
+        try {
+            const data = await this.client.getAllUserOrders({ id });
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testOrderById(id: string): Promise<any> {
+        try {
+            const data = await this.client.getOrder({ id });
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testAllUsers(): Promise<any> {
+        try {
+            const data = await this.client.getAllUsers({});
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testInsertOrder(order: Order): Promise<any> {
+        try {
+            const data = await this.client.postOrder(order);
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async updateUser(patch: UserPatchRequest): Promise<any> {
+        try {
+            const data = await this.client.patchAccountDetails(patch);
+            return data;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async runSuite(iterations: number): Promise<TestResults> {
+        const results: TestResults = {
+            randomProduct: {
+                ok: false,
+                time: 0
+            },
+            userById: {
+                ok: false,
+                time: 0
+            },
+            allProducts: {
+                ok: false,
+                time: 0
+            },
+            productById: {
+                ok: false,
+                time: 0
+            },
+            allCategories: {
+                ok: false,
+                time: 0
+            },
+            allOrders: {
+                ok: false,
+                time: 0
+            },
+            ordersByUser: {
+                ok: false,
+                time: 0
+            },
+            orderById: {
+                ok: false,
+                time: 0
+            },
+            allUsers: {
+                ok: false,
+                time: 0
+            },
+            insertOrder: {
+                ok: false,
+                time: 0
+            },
+            updateUser: {
+                ok: false,
+                time: 0
+            }
+        };
+        results.randomProduct = await avgRuntime(async () => await this.testRandomProduct(), iterations);
+        results.userById = await avgRuntime(async () => await this.testUserById(constants.TEST_USER_ID), iterations, constants.EXPECTED_USER);
+        results.allProducts = await avgRuntime(async () => await this.testAllProducts(), iterations);
+        results.productById = await avgRuntime(async () => await this.testProductById(constants.TEST_PRODUCT_ID), iterations, constants.EXPECTED_PRODUCT);
+        results.allCategories = await avgRuntime(async () => await this.testAllCategories(), iterations);
+        results.allOrders = await avgRuntime(async () => await this.testAllOrders(), iterations);
+        results.ordersByUser = await avgRuntime(async () => await this.testOrdersByUser(constants.TEST_USER_ID), iterations);
+        results.orderById = await avgRuntime(async () => await this.testOrderById(constants.TEST_ORDER_ID), iterations, constants.EXPECTED_ORDER);
+        results.allUsers = await avgRuntime(async () => await this.testAllUsers(), iterations);
+        results.insertOrder = await avgRuntime(async () => await this.testInsertOrder({ id: uuid(), ...constants.TEST_ORDER }), iterations);
+        results.updateUser = await avgRuntime(async () => await this.updateUser(constants.TEST_UPDATE), iterations);
+        return results;
+    }
+}
\ No newline at end of file
diff --git a/src/tests/restTests.ts b/src/tests/restTests.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e96ae176a6f981f8e531f1e817af464a685e127
--- /dev/null
+++ b/src/tests/restTests.ts
@@ -0,0 +1,213 @@
+import { ITestSuite } from '../interfaces';
+import { Category, Order, Product, TestResults, User, UserPatchRequest } from '../types';
+import { avgRuntime } from '../testing';
+import { uuid } from 'uuidv4';
+import constants from '../constants';
+
+export default class RestTestSuite implements ITestSuite {
+    private endpoint: string;
+
+    constructor(ip: string) {
+        this.endpoint = `http://${ip}:3000`;
+    }
+
+    public async testRandomProduct(): Promise<Product> {
+        try {
+            const resp = await fetch(this.endpoint + "/randomproduct");
+            if (!resp.ok) throw new Error("Failed to fetch random product");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testUserById(id: string): Promise<User> {
+        try {
+            const resp = await fetch(this.endpoint + "/user/" + id);
+            if (!resp.ok) throw new Error("Failed to fetch user by id");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error("error");
+            return null;
+        }
+    }
+
+    public async testAllProducts(categoryId?: string): Promise<Product[]> {
+        try {
+            const resp = await fetch(this.endpoint + "/products" + (categoryId ? "?categoryId=" + categoryId : ""));
+            if (!resp.ok) throw new Error("Failed to fetch all products");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testProductById(productId: string): Promise<Product> {
+        try {
+            const resp = await fetch(this.endpoint + "/product/" + productId);
+            if (!resp.ok) throw new Error("Failed to fetch product by id");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testAllCategories(): Promise<Category[]> {
+        try {
+            const resp = await fetch(this.endpoint + "/categories");
+            if (!resp.ok) throw new Error("Failed to fetch all categories");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testAllOrders(): Promise<Order[]> {
+        try {
+            const resp = await fetch(this.endpoint + "/allorders");
+            if (!resp.ok) throw new Error("Failed to fetch all orders");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testOrdersByUser(id: string): Promise<Order[]> {
+        try {
+            const resp = await fetch(this.endpoint + "/orders?id=" + id);
+            if (!resp.ok) throw new Error("Failed to fetch orders by user");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testOrderById(id: string): Promise<Order> {
+        try {
+            const resp = await fetch(this.endpoint + "/order/" + id);
+            if (!resp.ok) throw new Error("Failed to fetch order by id");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testAllUsers(): Promise<User[]> {
+        try {
+            const resp = await fetch(this.endpoint + "/users");
+            if (!resp.ok) throw new Error("Failed to fetch all users");
+            const json = await resp.json();
+            return json;
+        } catch (e) {
+            console.error(e);
+            return null;
+        }
+    }
+
+    public async testInsertOrder(order: Order): Promise<void> {
+        try {
+            const resp = await fetch(this.endpoint + "/orders", {
+                method: "POST",
+                headers: {
+                    "Content-Type": "application/json"
+                },
+                body: JSON.stringify(order)
+            });
+            if (!resp.ok) throw new Error("Failed to insert order");
+        } catch (e) {
+            console.error(e);
+        }
+    }
+
+    public async updateUser(patch: UserPatchRequest): Promise<void> {
+        try {
+            const resp = await fetch(this.endpoint + `/user/${patch.id}`, {
+                method: "PATCH",
+                headers: {
+                    "Content-Type": "application/json"
+                },
+                body: JSON.stringify(patch)
+            });
+            if (!resp.ok) throw new Error("Failed to update user");
+        } catch (e) {
+            console.error(e);
+        }
+    }
+
+    public async runSuite(iterations: number): Promise<TestResults> {
+        const results: TestResults = {
+            randomProduct: {
+                ok: false,
+                time: 0
+            },
+            userById: {
+                ok: false,
+                time: 0
+            },
+            allProducts: {
+                ok: false,
+                time: 0
+            },
+            productById: {
+                ok: false,
+                time: 0
+            },
+            allCategories: {
+                ok: false,
+                time: 0
+            },
+            allOrders: {
+                ok: false,
+                time: 0
+            },
+            ordersByUser: {
+                ok: false,
+                time: 0
+            },
+            orderById: {
+                ok: false,
+                time: 0
+            },
+            allUsers: {
+                ok: false,
+                time: 0
+            },
+            insertOrder: {
+                ok: false,
+                time: 0
+            },
+            updateUser: {
+                ok: false,
+                time: 0
+            }
+        };
+        results.randomProduct = await avgRuntime(async () => await this.testRandomProduct(), iterations);
+        results.userById = await avgRuntime(async () => await this.testUserById(constants.TEST_USER_ID), iterations, constants.EXPECTED_USER);
+        results.allProducts = await avgRuntime(async () => await this.testAllProducts(), iterations);
+        results.productById = await avgRuntime(async () => await this.testProductById(constants.TEST_PRODUCT_ID), iterations, constants.EXPECTED_PRODUCT);
+        results.allCategories = await avgRuntime(async () => await this.testAllCategories(), iterations);
+        results.allOrders = await avgRuntime(async () => await this.testAllOrders(), iterations);
+        results.ordersByUser = await avgRuntime(async () => await this.testOrdersByUser(constants.TEST_USER_ID), iterations);
+        results.orderById = await avgRuntime(async () => await this.testOrderById(constants.TEST_ORDER_ID), iterations, constants.EXPECTED_ORDER);
+        results.allUsers = await avgRuntime(async () => await this.testAllUsers(), iterations);
+        results.insertOrder = await avgRuntime(async () => await this.testInsertOrder({ id: uuid(), ...constants.TEST_ORDER }), iterations);
+        results.updateUser = await avgRuntime(async () => await this.updateUser(constants.TEST_UPDATE), iterations);
+
+        return results;
+    }
+}
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..90a277511149f3a11f53896119c71cbf82d40a09
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,73 @@
+export type ProductRequest = {
+    productId: string;
+};
+
+export type AllProductsRequest = {
+    categoryId: string;
+}
+
+export type Product = {
+    categoryId: string;
+    stock: number;
+    price: number;
+    description: string;
+    id: string;
+    name: string;
+};
+
+export type Order = {
+    userId: string;
+    id: string;
+    products: OrderProduct[];
+    totalAmount: number;
+};
+
+export type OrderRequest = {
+    id: string;
+}
+
+export type OrderProduct = {
+    productId: string;
+    quantity: number;
+};
+
+export type Category = {
+    description: string;
+    id: string;
+    price: number;
+    name: string;
+};
+
+export type UserRequest = {
+    id: string;
+};
+
+export type UserPatchRequest = {
+    id: string;
+    email?: string;
+    password?: string;
+}
+
+export type User = {
+    id: string;
+    email: string;
+}
+
+export type TestResult = {
+    ok: boolean;
+    time: number;
+}
+
+export type TestResults = {
+    randomProduct: TestResult;
+    userById: TestResult;
+    allProducts: TestResult;
+    productById: TestResult;
+    allCategories: TestResult;
+    allOrders: TestResult;
+    ordersByUser: TestResult;
+    orderById: TestResult;
+    allUsers: TestResult;
+    insertOrder: TestResult;
+    updateUser: TestResult;
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..e5a4f8f9000664f93c457dbecc4618dcdfd1c78e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+    "compilerOptions": {
+        "module": "commonjs",
+        "esModuleInterop": true,
+        "target": "es6",
+        "moduleResolution": "node",
+        "sourceMap": true,
+        "outDir": "dist",
+    },
+    "include": [
+        "src/**/*"
+    ],
+    "lib": [
+        "es2015"
+    ]
+}
\ No newline at end of file