diff --git a/.env b/.env
index 1cffbc4b95eb097e0946fccca58289f793363bea..42ea09e9c15f2c9de7c8d54cebfe1c3da4053cf6 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,5 @@
 NODE_ENV=development
+NODE_OPTIONS="--max_old_space_size=4096"
 
 TYPEORM_CONNECTION=postgres
 TYPEORM_HOST=localhost
@@ -91,4 +92,7 @@ SWAGGER_SERVER=https://localhost:8080
 SWAGGER_CHANNEL_ID=a8b1b6db-2543-4549-b143-442735739fed
 
 # Set to True in order to deploy an internal backend (without authentication)
-EXPOSE_UNAUTHENTICATED_ROUTES=False
\ No newline at end of file
+EXPOSE_UNAUTHENTICATED_ROUTES=False
+
+APPSIGNAL_PUSH_API_KEY=fill-me
+APPSIGNAL_ENV=master
diff --git a/appsignal.js b/appsignal.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad7583858473965f39c7bbe536c6bb9ca9a34830
--- /dev/null
+++ b/appsignal.js
@@ -0,0 +1,12 @@
+// appsignal.js or src/appsignal.js
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { Appsignal } = require('@appsignal/nodejs');
+
+const appsignal = new Appsignal({
+  active: true,
+  name: 'notifications',
+  pushApiKey: process.env.APPSIGNAL_PUSH_API_KEY, // Note: renamed from `apiKey` in version 2.2.5
+  environment: process.env.APPSIGNAL_ENV,
+});
+
+module.exports = { appsignal };
diff --git a/package-lock.json b/package-lock.json
index 31fd35669abb720fc0757fcc2b9139cc82185650..1aa10adcbcbf32a99f9e561f9c74facbe513c1cf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,136 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@appsignal/core": {
+      "version": "1.1.18",
+      "resolved": "https://registry.npmjs.org/@appsignal/core/-/core-1.1.18.tgz",
+      "integrity": "sha512-PfFxEn6CNiX6PR6P582N5cV+5/j3tCmn1v6G+0nHrj8GY4Dir+vqJtGbgUxda0kbtCa6B4OHMH7Pe/SGL92rKw==",
+      "requires": {
+        "@appsignal/types": "=3.0.0",
+        "isomorphic-unfetch": "^3.1.0",
+        "tslib": "^2.3.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+          "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+        }
+      }
+    },
+    "@appsignal/express": {
+      "version": "1.0.33",
+      "resolved": "https://registry.npmjs.org/@appsignal/express/-/express-1.0.33.tgz",
+      "integrity": "sha512-tPrWIMtflR2+YsqoMd7TwGGSWyVLggvxkwimrKhX358KoYPymjldLaeSB2CDcOeskcVcLyj0VMeM57FXqs4CHQ==",
+      "requires": {
+        "@appsignal/nodejs": "=2.4.2",
+        "tslib": "^2.0.3"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+          "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+        }
+      }
+    },
+    "@appsignal/nodejs": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@appsignal/nodejs/-/nodejs-2.4.2.tgz",
+      "integrity": "sha512-Tjn6Tz2T/BwZQMrbBdatI1FvgEuBhtsQcAEv7zVAZ1v/ylhK3/spGsEEsOIrSHMxbBTqPAC8GBkqyXy2CZTdFA==",
+      "requires": {
+        "@appsignal/core": "^1.1.4",
+        "@appsignal/types": "^3.0.0",
+        "@opentelemetry/api": "^1.0.4",
+        "@opentelemetry/sdk-trace-base": "^1.2.0",
+        "node-addon-api": "^3.1.0",
+        "node-gyp": "^9.0.0",
+        "require-in-the-middle": "^5.1.0",
+        "semver": "^7.3.4",
+        "shimmer": "^1.2.1",
+        "tslib": "^2.0.3",
+        "winston": "^3.6.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "3.2.4",
+          "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+          "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+        },
+        "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==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "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=="
+        },
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "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==",
+          "requires": {
+            "safe-buffer": "~5.2.0"
+          }
+        },
+        "tslib": {
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+          "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+        },
+        "winston": {
+          "version": "3.8.2",
+          "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",
+          "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==",
+          "requires": {
+            "@colors/colors": "1.5.0",
+            "@dabh/diagnostics": "^2.0.2",
+            "async": "^3.2.3",
+            "is-stream": "^2.0.0",
+            "logform": "^2.4.0",
+            "one-time": "^1.0.0",
+            "readable-stream": "^3.4.0",
+            "safe-stable-stringify": "^2.3.1",
+            "stack-trace": "0.0.x",
+            "triple-beam": "^1.3.0",
+            "winston-transport": "^4.5.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
+      }
+    },
+    "@appsignal/types": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@appsignal/types/-/types-3.0.0.tgz",
+      "integrity": "sha512-L0OAKaro7209Du39UF2ZWU8CzUJxeeKaW6Zyu2FA2SGdW3HK0PoJxY7A8R52KdZiXzMlCAlrOjvoDvT/ct0RIg=="
+    },
     "@babel/code-frame": {
       "version": "7.12.13",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
@@ -454,6 +584,21 @@
         "minimist": "^1.2.0"
       }
     },
+    "@colors/colors": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+      "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="
+    },
+    "@dabh/diagnostics": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
+      "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
+      "requires": {
+        "colorspace": "1.1.x",
+        "enabled": "2.0.x",
+        "kuler": "^2.0.0"
+      }
+    },
     "@eslint/eslintrc": {
       "version": "0.4.2",
       "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz",
@@ -563,6 +708,11 @@
         "tslib": "^1.11.1"
       }
     },
+    "@gar/promisify": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
+      "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="
+    },
     "@google-cloud/common": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz",
@@ -1196,6 +1346,120 @@
         "fastq": "^1.6.0"
       }
     },
+    "@npmcli/fs": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
+      "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==",
+      "requires": {
+        "@gar/promisify": "^1.1.3",
+        "semver": "^7.3.5"
+      },
+      "dependencies": {
+        "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==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
+      }
+    },
+    "@npmcli/move-file": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
+      "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==",
+      "requires": {
+        "mkdirp": "^1.0.4",
+        "rimraf": "^3.0.2"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "requires": {
+            "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"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "mkdirp": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+        },
+        "rimraf": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        }
+      }
+    },
+    "@opentelemetry/api": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.2.0.tgz",
+      "integrity": "sha512-0nBr+VZNKm9tvNDZFstI3Pq1fCTEDK5OZTnVKNvBNAKgd0yIvmwsP4m61rEv7ZP+tOUjWJhROpxK5MsnlF911g=="
+    },
+    "@opentelemetry/core": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.7.0.tgz",
+      "integrity": "sha512-AVqAi5uc8DrKJBimCTFUT4iFI+5eXpo4sYmGbQ0CypG0piOTHE2g9c5aSoTGYXu3CzOmJZf7pT6Xh+nwm5d6yQ==",
+      "requires": {
+        "@opentelemetry/semantic-conventions": "1.7.0"
+      }
+    },
+    "@opentelemetry/resources": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.7.0.tgz",
+      "integrity": "sha512-u1M0yZotkjyKx8dj+46Sg5thwtOTBmtRieNXqdCRiWUp6SfFiIP0bI+1XK3LhuXqXkBXA1awJZaTqKduNMStRg==",
+      "requires": {
+        "@opentelemetry/core": "1.7.0",
+        "@opentelemetry/semantic-conventions": "1.7.0"
+      }
+    },
+    "@opentelemetry/sdk-trace-base": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.7.0.tgz",
+      "integrity": "sha512-Iz84C+FVOskmauh9FNnj4+VrA+hG5o+tkMzXuoesvSfunVSioXib0syVFeNXwOm4+M5GdWCuW632LVjqEXStIg==",
+      "requires": {
+        "@opentelemetry/core": "1.7.0",
+        "@opentelemetry/resources": "1.7.0",
+        "@opentelemetry/semantic-conventions": "1.7.0"
+      }
+    },
+    "@opentelemetry/semantic-conventions": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.7.0.tgz",
+      "integrity": "sha512-FGBx/Qd09lMaqQcogCHyYrFEpTx4cAjeS+48lMIR12z7LdH+zofGDVQSubN59nL6IpubfKqTeIDu9rNO28iHVA=="
+    },
     "@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -1556,11 +1820,6 @@
       "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==",
       "dev": true
     },
-    "@types/zen-observable": {
-      "version": "0.8.3",
-      "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz",
-      "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw=="
-    },
     "@typescript-eslint/eslint-plugin": {
       "version": "4.28.1",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.1.tgz",
@@ -1826,6 +2085,11 @@
         "merge-options": "^1.0.0"
       }
     },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA=="
+    },
     "agent-base": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -1849,11 +2113,35 @@
         }
       }
     },
+    "agentkeepalive": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz",
+      "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==",
+      "requires": {
+        "debug": "^4.1.0",
+        "depd": "^1.1.2",
+        "humanize-ms": "^1.2.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
     "aggregate-error": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
       "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
-      "dev": true,
       "requires": {
         "clean-stack": "^2.0.0",
         "indent-string": "^4.0.0"
@@ -1990,9 +2278,9 @@
       }
     },
     "app-root-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
-      "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw=="
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
+      "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA=="
     },
     "append-field": {
       "version": "0.1.0",
@@ -2000,6 +2288,45 @@
       "integrity": "sha1-bdxY+gg8e8VF08WZWygwzCNm1Eo=",
       "optional": true
     },
+    "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=="
+    },
+    "are-we-there-yet": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+      "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+      "requires": {
+        "delegates": "^1.0.0",
+        "readable-stream": "^3.6.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "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=="
+        },
+        "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==",
+          "requires": {
+            "safe-buffer": "~5.2.0"
+          }
+        }
+      }
+    },
     "arg": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -2056,6 +2383,11 @@
       "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
       "dev": true
     },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+    },
     "arrify": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
@@ -2133,6 +2465,14 @@
       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
     },
+    "axios": {
+      "version": "0.26.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
+      "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
+      "requires": {
+        "follow-redirects": "^1.14.8"
+      }
+    },
     "babel-jest": {
       "version": "26.6.3",
       "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz",
@@ -2231,6 +2571,11 @@
         "babel-preset-current-node-syntax": "^1.0.0"
       }
     },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA=="
+    },
     "backoff": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
@@ -2299,11 +2644,21 @@
         }
       }
     },
+    "base64-arraybuffer": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
+      "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg=="
+    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
     },
+    "base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
+    },
     "bcrypt-pbkdf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -2338,6 +2693,11 @@
         "buffer-more-ints": "0.0.2"
       }
     },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
+    },
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -2492,6 +2852,111 @@
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
       "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
     },
+    "cacache": {
+      "version": "16.1.3",
+      "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
+      "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==",
+      "requires": {
+        "@npmcli/fs": "^2.1.0",
+        "@npmcli/move-file": "^2.0.0",
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.1.0",
+        "glob": "^8.0.1",
+        "infer-owner": "^1.0.4",
+        "lru-cache": "^7.7.1",
+        "minipass": "^3.1.6",
+        "minipass-collect": "^1.0.2",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "mkdirp": "^1.0.4",
+        "p-map": "^4.0.0",
+        "promise-inflight": "^1.0.1",
+        "rimraf": "^3.0.2",
+        "ssri": "^9.0.0",
+        "tar": "^6.1.11",
+        "unique-filename": "^2.0.0"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+          "requires": {
+            "balanced-match": "^1.0.0"
+          }
+        },
+        "glob": {
+          "version": "8.0.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
+          "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^5.0.1",
+            "once": "^1.3.0"
+          }
+        },
+        "lru-cache": {
+          "version": "7.14.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz",
+          "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ=="
+        },
+        "minimatch": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
+          "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
+          "requires": {
+            "brace-expansion": "^2.0.1"
+          }
+        },
+        "mkdirp": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+        },
+        "rimraf": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+          "requires": {
+            "glob": "^7.1.3"
+          },
+          "dependencies": {
+            "brace-expansion": {
+              "version": "1.1.11",
+              "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+              "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+              "requires": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+              }
+            },
+            "glob": {
+              "version": "7.2.3",
+              "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+              "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+              "requires": {
+                "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"
+              }
+            },
+            "minimatch": {
+              "version": "3.1.2",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+              "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+              "requires": {
+                "brace-expansion": "^1.1.7"
+              }
+            }
+          }
+        }
+      }
+    },
     "cache-base": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@@ -2634,6 +3099,11 @@
         "readdirp": "~3.5.0"
       }
     },
+    "chownr": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
+    },
     "ci-info": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@@ -2705,8 +3175,7 @@
     "clean-stack": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
-      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
-      "dev": true
+      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
     },
     "cli-boxes": {
       "version": "2.2.1",
@@ -2878,6 +3347,30 @@
         "object-visit": "^1.0.0"
       }
     },
+    "color": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
+      "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+      "requires": {
+        "color-convert": "^1.9.3",
+        "color-string": "^1.6.0"
+      },
+      "dependencies": {
+        "color-convert": {
+          "version": "1.9.3",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+          "requires": {
+            "color-name": "1.1.3"
+          }
+        },
+        "color-name": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+        }
+      }
+    },
     "color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2891,6 +3384,20 @@
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
+    "color-string": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+      "requires": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "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=="
+    },
     "colorette": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
@@ -2902,6 +3409,15 @@
       "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
       "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs="
     },
+    "colorspace": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
+      "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+      "requires": {
+        "color": "^3.1.3",
+        "text-hex": "1.0.x"
+      }
+    },
     "combined-stream": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2922,11 +3438,20 @@
       "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
       "dev": true
     },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw=="
+    },
     "component-emitter": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
-      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
-      "dev": true
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA=="
     },
     "compressible": {
       "version": "2.0.18",
@@ -2995,6 +3520,11 @@
         "xdg-basedir": "^4.0.0"
       }
     },
+    "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=="
+    },
     "content-disposition": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@@ -3172,6 +3702,11 @@
       "integrity": "sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw==",
       "optional": true
     },
+    "date-fns": {
+      "version": "2.29.3",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
+      "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
+    },
     "debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -3323,8 +3858,7 @@
     "delegates": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
-      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
-      "optional": true
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
     },
     "depd": {
       "version": "1.1.2",
@@ -3408,7 +3942,8 @@
     "dotenv": {
       "version": "8.2.0",
       "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
-      "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
+      "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
+      "dev": true
     },
     "dtrace-provider": {
       "version": "0.8.8",
@@ -3507,17 +4042,115 @@
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
       "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
     },
+    "enabled": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+      "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
+    },
     "encodeurl": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
       "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
     },
-    "end-of-stream": {
-      "version": "1.4.4",
-      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+    "encoding": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+      "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+      "optional": true,
+      "requires": {
+        "iconv-lite": "^0.6.2"
+      },
+      "dependencies": {
+        "iconv-lite": {
+          "version": "0.6.3",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+          "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+          "optional": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3.0.0"
+          }
+        }
+      }
+    },
+    "end-of-stream": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+      "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.0.tgz",
+      "integrity": "sha512-Kc8fo5bbg8F4a2f3HPHTEpGyq/IRIQpyeHu3H1ThR14XDD7VrLcsGBo16HUpahgp8YkHJDaU5gNxJZbuGcuueg==",
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.4.1",
+        "debug": "~4.1.0",
+        "engine.io-parser": "~2.2.0",
+        "ws": "~7.4.2"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+          "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
+        },
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.3.tgz",
+      "integrity": "sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==",
+      "requires": {
+        "component-emitter": "~1.3.0",
+        "component-inherit": "0.0.3",
+        "debug": "~3.1.0",
+        "engine.io-parser": "~2.2.0",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.6",
+        "parseuri": "0.0.6",
+        "ws": "~7.4.2",
+        "xmlhttprequest-ssl": "~1.6.2",
+        "yeast": "0.1.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
+      "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
       "requires": {
-        "once": "^1.4.0"
+        "after": "0.8.2",
+        "arraybuffer.slice": "~0.0.7",
+        "base64-arraybuffer": "0.1.4",
+        "blob": "0.0.5",
+        "has-binary2": "~1.0.2"
       }
     },
     "enquirer": {
@@ -3535,6 +4168,16 @@
       "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
       "optional": true
     },
+    "env-paths": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+      "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="
+    },
+    "err-code": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+      "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="
+    },
     "error-ex": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -4119,6 +4762,15 @@
         "es5-ext": "~0.10.14"
       }
     },
+    "event-loop-stats": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/event-loop-stats/-/event-loop-stats-1.2.0.tgz",
+      "integrity": "sha512-h/leAlXqoEf+D9w1dnFG0srR5vfIq59rLm9PHzcl3/GwFppd+UR46UMuLdp/mvJvuA+MjSd/dNShmuM2/dPFFw==",
+      "optional": true,
+      "requires": {
+        "nan": "^2.14.0"
+      }
+    },
     "event-target-shim": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@@ -4278,6 +4930,35 @@
         }
       }
     },
+    "express-status-monitor": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/express-status-monitor/-/express-status-monitor-1.3.4.tgz",
+      "integrity": "sha512-EyqHvgX57ujN4fqfUT+x6Bv2xwRyzQdv3AJvWQxcG+jK4TcF9vhrKVqGcE0T6bhT4rstpvOKRuxHBwC/Q6AXQg==",
+      "requires": {
+        "axios": "0.26.0",
+        "debug": "4.1.1",
+        "event-loop-stats": "1.2.0",
+        "handlebars": "^4.7.7",
+        "on-headers": "1.0.2",
+        "pidusage": "2.0.18",
+        "socket.io": "^2.4.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
     "ext": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
@@ -4488,6 +5169,11 @@
         "bser": "2.1.1"
       }
     },
+    "fecha": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+      "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
+    },
     "file-entry-cache": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -4605,6 +5291,16 @@
       "integrity": "sha512-XprP7lDrVT+kE2c2YlfiV+IfS9zxukiIOvNamPNsImNhXadSsQEbosItdL9bUQlCZXR13SvPk20BjWSWLA7m4A==",
       "dev": true
     },
+    "fn.name": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+      "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
+    },
+    "follow-redirects": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+    },
     "for-in": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -4651,6 +5347,14 @@
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
       "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
     },
+    "fs-minipass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "requires": {
+        "minipass": "^3.0.0"
+      }
+    },
     "fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4672,6 +5376,61 @@
       "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
       "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
     },
+    "gauge": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+      "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+      "requires": {
+        "aproba": "^1.0.3 || ^2.0.0",
+        "color-support": "^1.1.3",
+        "console-control-strings": "^1.1.0",
+        "has-unicode": "^2.0.1",
+        "signal-exit": "^3.0.7",
+        "string-width": "^4.2.3",
+        "strip-ansi": "^6.0.1",
+        "wide-align": "^1.1.5"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+          "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+        },
+        "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=="
+        },
+        "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=="
+        },
+        "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=="
+        },
+        "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==",
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.1"
+          }
+        },
+        "strip-ansi": {
+          "version": "6.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+          "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+          "requires": {
+            "ansi-regex": "^5.0.1"
+          }
+        }
+      }
+    },
     "gaxios": {
       "version": "2.3.4",
       "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz",
@@ -4929,6 +5688,18 @@
         }
       }
     },
+    "handlebars": {
+      "version": "4.7.7",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
+      "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
+      "requires": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.0",
+        "source-map": "^0.6.1",
+        "uglify-js": "^3.1.4",
+        "wordwrap": "^1.0.0"
+      }
+    },
     "har-schema": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -4951,6 +5722,26 @@
         "function-bind": "^1.1.1"
       }
     },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+      "requires": {
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ=="
+        }
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA=="
+    },
     "has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -4962,6 +5753,11 @@
       "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
       "optional": true
     },
+    "has-unicode": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+      "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
+    },
     "has-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@@ -5178,6 +5974,14 @@
       "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
       "dev": true
     },
+    "humanize-ms": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+      "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+      "requires": {
+        "ms": "^2.0.0"
+      }
+    },
     "husky": {
       "version": "4.3.8",
       "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz",
@@ -5339,8 +6143,17 @@
     "indent-string": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
-      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
-      "dev": true
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg=="
+    },
+    "infer-owner": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
+      "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="
     },
     "inflation": {
       "version": "2.0.0",
@@ -5367,6 +6180,11 @@
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
       "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="
     },
+    "ip": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
+      "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
+    },
     "ip-regex": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
@@ -5562,6 +6380,11 @@
         "is-path-inside": "^3.0.1"
       }
     },
+    "is-lambda": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+      "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="
+    },
     "is-map": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
@@ -5737,8 +6560,7 @@
     "isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
-      "dev": true
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
     },
     "isobject": {
       "version": "3.0.1",
@@ -5746,6 +6568,15 @@
       "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
       "dev": true
     },
+    "isomorphic-unfetch": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz",
+      "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==",
+      "requires": {
+        "node-fetch": "^2.6.1",
+        "unfetch": "^4.2.0"
+      }
+    },
     "isstream": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -7422,6 +8253,11 @@
         }
       }
     },
+    "kuler": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+      "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
+    },
     "latest-version": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@@ -7866,6 +8702,25 @@
         }
       }
     },
+    "logform": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz",
+      "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==",
+      "requires": {
+        "@colors/colors": "1.5.0",
+        "fecha": "^4.2.0",
+        "ms": "^2.1.1",
+        "safe-stable-stringify": "^2.3.1",
+        "triple-beam": "^1.3.0"
+      },
+      "dependencies": {
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
     "long": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -7912,6 +8767,69 @@
       "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
       "dev": true
     },
+    "make-fetch-happen": {
+      "version": "10.2.1",
+      "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
+      "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==",
+      "requires": {
+        "agentkeepalive": "^4.2.1",
+        "cacache": "^16.1.0",
+        "http-cache-semantics": "^4.1.0",
+        "http-proxy-agent": "^5.0.0",
+        "https-proxy-agent": "^5.0.0",
+        "is-lambda": "^1.0.1",
+        "lru-cache": "^7.7.1",
+        "minipass": "^3.1.6",
+        "minipass-collect": "^1.0.2",
+        "minipass-fetch": "^2.0.3",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "negotiator": "^0.6.3",
+        "promise-retry": "^2.0.1",
+        "socks-proxy-agent": "^7.0.0",
+        "ssri": "^9.0.0"
+      },
+      "dependencies": {
+        "@tootallnate/once": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+          "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="
+        },
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "http-proxy-agent": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+          "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+          "requires": {
+            "@tootallnate/once": "2",
+            "agent-base": "6",
+            "debug": "4"
+          }
+        },
+        "lru-cache": {
+          "version": "7.14.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz",
+          "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ=="
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "negotiator": {
+          "version": "0.6.3",
+          "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+          "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
+        }
+      }
+    },
     "makeerror": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
@@ -8042,6 +8960,80 @@
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
+    "minipass": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz",
+      "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==",
+      "requires": {
+        "yallist": "^4.0.0"
+      },
+      "dependencies": {
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
+      }
+    },
+    "minipass-collect": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+      "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+      "requires": {
+        "minipass": "^3.0.0"
+      }
+    },
+    "minipass-fetch": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz",
+      "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==",
+      "requires": {
+        "encoding": "^0.1.13",
+        "minipass": "^3.1.6",
+        "minipass-sized": "^1.0.3",
+        "minizlib": "^2.1.2"
+      }
+    },
+    "minipass-flush": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+      "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+      "requires": {
+        "minipass": "^3.0.0"
+      }
+    },
+    "minipass-pipeline": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+      "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+      "requires": {
+        "minipass": "^3.0.0"
+      }
+    },
+    "minipass-sized": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+      "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+      "requires": {
+        "minipass": "^3.0.0"
+      }
+    },
+    "minizlib": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "requires": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "dependencies": {
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
+      }
+    },
     "mixin-deep": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
@@ -8072,6 +9064,11 @@
         "minimist": "^1.2.5"
       }
     },
+    "module-details-from-path": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
+      "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A=="
+    },
     "moment": {
       "version": "2.29.1",
       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@@ -8210,6 +9207,11 @@
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
       "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
     },
+    "neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
+    },
     "next-tick": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
@@ -8221,6 +9223,11 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "dev": true
     },
+    "node-addon-api": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
+      "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
+    },
     "node-cache": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
@@ -8229,17 +9236,106 @@
         "clone": "2.x"
       }
     },
-    "node-fetch": {
-      "version": "2.6.1",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
-      "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
-      "optional": true
-    },
-    "node-forge": {
-      "version": "0.7.6",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz",
-      "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw=="
-    },
+    "node-fetch": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+      "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
+    },
+    "node-forge": {
+      "version": "0.7.6",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz",
+      "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw=="
+    },
+    "node-gyp": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.1.0.tgz",
+      "integrity": "sha512-HkmN0ZpQJU7FLbJauJTHkHlSVAXlNGDAzH/VYFZGDOnFyn/Na3GlNJfkudmufOdS6/jNFhy88ObzL7ERz9es1g==",
+      "requires": {
+        "env-paths": "^2.2.0",
+        "glob": "^7.1.4",
+        "graceful-fs": "^4.2.6",
+        "make-fetch-happen": "^10.0.3",
+        "nopt": "^5.0.0",
+        "npmlog": "^6.0.0",
+        "rimraf": "^3.0.2",
+        "semver": "^7.3.5",
+        "tar": "^6.1.2",
+        "which": "^2.0.2"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "requires": {
+            "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"
+          }
+        },
+        "graceful-fs": {
+          "version": "4.2.10",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+          "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
+        },
+        "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==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "nopt": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+          "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+          "requires": {
+            "abbrev": "1"
+          }
+        },
+        "rimraf": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+          "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+          "requires": {
+            "glob": "^7.1.3"
+          }
+        },
+        "semver": {
+          "version": "7.3.7",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
+          "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "which": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+          "requires": {
+            "isexe": "^2.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
+      }
+    },
     "node-int64": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -8408,6 +9504,17 @@
         "path-key": "^2.0.0"
       }
     },
+    "npmlog": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+      "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+      "requires": {
+        "are-we-there-yet": "^3.0.0",
+        "console-control-strings": "^1.1.0",
+        "gauge": "^4.0.3",
+        "set-blocking": "^2.0.0"
+      }
+    },
     "nwsapi": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
@@ -8528,6 +9635,14 @@
         "wrappy": "1"
       }
     },
+    "one-time": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+      "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+      "requires": {
+        "fn.name": "1.x.x"
+      }
+    },
     "onetime": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -8608,7 +9723,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
       "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
-      "dev": true,
       "requires": {
         "aggregate-error": "^3.0.0"
       }
@@ -8680,6 +9794,16 @@
         }
       }
     },
+    "parseqs": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
+      "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
+    },
+    "parseuri": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
+      "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
+    },
     "parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -8789,6 +9913,14 @@
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
       "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
     },
+    "pidusage": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.18.tgz",
+      "integrity": "sha512-Y/VfKfh3poHjMEINxU+gJTeVOBjiThQeFAmzR7z56HSNiMx+etl+yBhk42nRPciPYt/VZl8DQLVXNC6P5vH11A==",
+      "requires": {
+        "safe-buffer": "^5.1.2"
+      }
+    },
     "pirates": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz",
@@ -8907,6 +10039,20 @@
       "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
       "dev": true
     },
+    "promise-inflight": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+      "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="
+    },
+    "promise-retry": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+      "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+      "requires": {
+        "err-code": "^2.0.2",
+        "retry": "^0.12.0"
+      }
+    },
     "prompts": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
@@ -9278,6 +10424,54 @@
       "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
       "dev": true
     },
+    "require-in-the-middle": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz",
+      "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==",
+      "requires": {
+        "debug": "^4.1.1",
+        "module-details-from-path": "^1.0.3",
+        "resolve": "^1.22.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "is-core-module": {
+          "version": "2.10.0",
+          "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
+          "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==",
+          "requires": {
+            "has": "^1.0.3"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        },
+        "path-parse": {
+          "version": "1.0.7",
+          "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+          "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+        },
+        "resolve": {
+          "version": "1.22.1",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+          "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+          "requires": {
+            "is-core-module": "^2.9.0",
+            "path-parse": "^1.0.7",
+            "supports-preserve-symlinks-flag": "^1.0.0"
+          }
+        }
+      }
+    },
     "require-main-filename": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -9339,6 +10533,11 @@
       "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
       "dev": true
     },
+    "retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="
+    },
     "retry-request": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz",
@@ -9492,6 +10691,11 @@
         "ret": "~0.1.10"
       }
     },
+    "safe-stable-stringify": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.0.tgz",
+      "integrity": "sha512-eehKHKpab6E741ud7ZIMcXhKcP6TSIezPkNZhy5U8xC6+VvrRdUA2tMgxGxaGl4cz7c2Ew5+mg5+wNB16KQqrA=="
+    },
     "safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -9718,8 +10922,7 @@
     "set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
-      "dev": true
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
     },
     "set-immediate-shim": {
       "version": "1.0.1",
@@ -9785,6 +10988,11 @@
       "dev": true,
       "optional": true
     },
+    "shimmer": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+      "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="
+    },
     "side-channel": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -9801,6 +11009,21 @@
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
       "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
     },
+    "simple-swizzle": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+      "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+      "requires": {
+        "is-arrayish": "^0.3.1"
+      },
+      "dependencies": {
+        "is-arrayish": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+          "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+        }
+      }
+    },
     "sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -9832,6 +11055,11 @@
         }
       }
     },
+    "smart-buffer": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+      "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
+    },
     "snakeize": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz",
@@ -9951,11 +11179,155 @@
         }
       }
     },
+    "socket.io": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.5.0.tgz",
+      "integrity": "sha512-gGunfS0od3VpwDBpGwVkzSZx6Aqo9uOcf1afJj2cKnKFAoyl16fvhpsUhmUFd4Ldbvl5JvRQed6eQw6oQp6n8w==",
+      "requires": {
+        "debug": "~4.1.0",
+        "engine.io": "~3.6.0",
+        "has-binary2": "~1.0.2",
+        "socket.io-adapter": "~1.1.0",
+        "socket.io-client": "2.5.0",
+        "socket.io-parser": "~3.4.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
+    "socket.io-adapter": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz",
+      "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g=="
+    },
+    "socket.io-client": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.5.0.tgz",
+      "integrity": "sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==",
+      "requires": {
+        "backo2": "1.0.2",
+        "component-bind": "1.0.0",
+        "component-emitter": "~1.3.0",
+        "debug": "~3.1.0",
+        "engine.io-client": "~3.5.0",
+        "has-binary2": "~1.0.2",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.6",
+        "parseuri": "0.0.6",
+        "socket.io-parser": "~3.3.0",
+        "to-array": "0.1.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ=="
+        },
+        "socket.io-parser": {
+          "version": "3.3.2",
+          "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
+          "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
+          "requires": {
+            "component-emitter": "~1.3.0",
+            "debug": "~3.1.0",
+            "isarray": "2.0.1"
+          }
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz",
+      "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "debug": "~4.1.0",
+        "isarray": "2.0.1"
+      },
+      "dependencies": {
+        "component-emitter": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+          "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA=="
+        },
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "isarray": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+          "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ=="
+        },
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
+    "socks": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
+      "integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
+      "requires": {
+        "ip": "^2.0.0",
+        "smart-buffer": "^4.2.0"
+      }
+    },
+    "socks-proxy-agent": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+      "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
+      "requires": {
+        "agent-base": "^6.0.2",
+        "debug": "^4.3.3",
+        "socks": "^2.6.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+        }
+      }
+    },
     "source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "dev": true
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
     },
     "source-map-resolve": {
       "version": "0.5.3",
@@ -10082,6 +11454,14 @@
         "tweetnacl": "~0.14.0"
       }
     },
+    "ssri": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
+      "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==",
+      "requires": {
+        "minipass": "^3.1.1"
+      }
+    },
     "stack-trace": {
       "version": "0.0.10",
       "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -10354,6 +11734,11 @@
         }
       }
     },
+    "supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
+    },
     "swagger-ui-dist": {
       "version": "3.49.0",
       "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.49.0.tgz",
@@ -10422,6 +11807,31 @@
         }
       }
     },
+    "tar": {
+      "version": "6.1.11",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
+      "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
+      "requires": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^3.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
+      }
+    },
     "teeny-request": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz",
@@ -10482,6 +11892,11 @@
         }
       }
     },
+    "text-hex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+      "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
+    },
     "text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -10499,7 +11914,7 @@
     "thenify-all": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
-      "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
       "requires": {
         "thenify": ">= 3.1.0 < 4"
       }
@@ -10569,6 +11984,11 @@
       "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
       "dev": true
     },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A=="
+    },
     "to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -10651,6 +12071,11 @@
         "punycode": "^2.1.1"
       }
     },
+    "triple-beam": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
+      "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
+    },
     "ts-node": {
       "version": "8.10.2",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
@@ -10746,26 +12171,27 @@
       }
     },
     "typeorm": {
-      "version": "0.2.41",
-      "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.41.tgz",
-      "integrity": "sha512-/d8CLJJxKPgsnrZWiMyPI0rz2MFZnBQrnQ5XP3Vu3mswv2WPexb58QM6BEtmRmlTMYN5KFWUz8SKluze+wS9xw==",
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.9.tgz",
+      "integrity": "sha512-xNcE44D4hn74n7pjuMog9hRgep+BiO3IBpjEaQZ8fb56zsDz7xHT1GAeWwmGuuU+4nDEELp2mIqgSCR+zxR7Jw==",
       "requires": {
         "@sqltools/formatter": "^1.2.2",
         "app-root-path": "^3.0.0",
         "buffer": "^6.0.3",
         "chalk": "^4.1.0",
         "cli-highlight": "^2.1.11",
-        "debug": "^4.3.1",
-        "dotenv": "^8.2.0",
-        "glob": "^7.1.6",
-        "js-yaml": "^4.0.0",
+        "date-fns": "^2.28.0",
+        "debug": "^4.3.3",
+        "dotenv": "^16.0.0",
+        "glob": "^7.2.0",
+        "js-yaml": "^4.1.0",
         "mkdirp": "^1.0.4",
         "reflect-metadata": "^0.1.13",
         "sha.js": "^2.4.11",
-        "tslib": "^2.1.0",
+        "tslib": "^2.3.1",
+        "uuid": "^8.3.2",
         "xml2js": "^0.4.23",
-        "yargs": "^17.0.1",
-        "zen-observable-ts": "^1.0.0"
+        "yargs": "^17.3.1"
       },
       "dependencies": {
         "argparse": {
@@ -10783,22 +12209,27 @@
           }
         },
         "debug": {
-          "version": "4.3.3",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
-          "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
           "requires": {
             "ms": "2.1.2"
           }
         },
+        "dotenv": {
+          "version": "16.0.2",
+          "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.2.tgz",
+          "integrity": "sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA=="
+        },
         "glob": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
-          "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
           "requires": {
             "fs.realpath": "^1.0.0",
             "inflight": "^1.0.4",
             "inherits": "2",
-            "minimatch": "^3.0.4",
+            "minimatch": "^3.1.1",
             "once": "^1.3.0",
             "path-is-absolute": "^1.0.0"
           }
@@ -10816,6 +12247,14 @@
             "argparse": "^2.0.1"
           }
         },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
         "mkdirp": {
           "version": "1.0.4",
           "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -10835,9 +12274,14 @@
           }
         },
         "tslib": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
-          "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
+          "version": "2.4.0",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+          "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
+        },
+        "uuid": {
+          "version": "8.3.2",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
         }
       }
     },
@@ -10847,6 +12291,12 @@
       "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
       "dev": true
     },
+    "uglify-js": {
+      "version": "3.17.0",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.0.tgz",
+      "integrity": "sha512-aTeNPVmgIMPpm1cxXr2Q/nEbvkmV8yq66F3om7X3P/cvOXQ0TMQ64Wk63iyT1gPlmdmGzjGpyLh1f3y8MZWXGg==",
+      "optional": true
+    },
     "uid-safe": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@@ -10863,6 +12313,11 @@
         "debug": "^2.2.0"
       }
     },
+    "unfetch": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
+      "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="
+    },
     "union-value": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -10875,6 +12330,22 @@
         "set-value": "^2.0.1"
       }
     },
+    "unique-filename": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
+      "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==",
+      "requires": {
+        "unique-slug": "^3.0.0"
+      }
+    },
+    "unique-slug": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz",
+      "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==",
+      "requires": {
+        "imurmurhash": "^0.1.4"
+      }
+    },
     "unique-string": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
@@ -11241,6 +12712,14 @@
         "is-typed-array": "^1.1.3"
       }
     },
+    "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==",
+      "requires": {
+        "string-width": "^1.0.2 || 2 || 3 || 4"
+      }
+    },
     "widest-line": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
@@ -11269,12 +12748,52 @@
         }
       }
     },
+    "winston-transport": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
+      "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
+      "requires": {
+        "logform": "^2.3.2",
+        "readable-stream": "^3.6.0",
+        "triple-beam": "^1.3.0"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "3.6.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+          "requires": {
+            "inherits": "^2.0.3",
+            "string_decoder": "^1.1.1",
+            "util-deprecate": "^1.0.1"
+          }
+        },
+        "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=="
+        },
+        "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==",
+          "requires": {
+            "safe-buffer": "~5.2.0"
+          }
+        }
+      }
+    },
     "word-wrap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
       "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
       "dev": true
     },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
+    },
     "wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -11319,8 +12838,7 @@
     "ws": {
       "version": "7.4.3",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz",
-      "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==",
-      "dev": true
+      "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA=="
     },
     "xdg-basedir": {
       "version": "4.0.0",
@@ -11353,6 +12871,11 @@
       "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
       "dev": true
     },
+    "xmlhttprequest-ssl": {
+      "version": "1.6.3",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz",
+      "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q=="
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -11375,9 +12898,9 @@
       "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
     },
     "yargs": {
-      "version": "17.3.0",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.0.tgz",
-      "integrity": "sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew==",
+      "version": "17.5.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
+      "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
       "requires": {
         "cliui": "^7.0.2",
         "escalade": "^3.1.1",
@@ -11422,9 +12945,9 @@
           }
         },
         "yargs-parser": {
-          "version": "21.0.0",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
-          "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA=="
+          "version": "21.1.1",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+          "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
         }
       }
     },
@@ -11433,6 +12956,11 @@
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
       "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
     },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg=="
+    },
     "ylru": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz",
@@ -11450,20 +12978,6 @@
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
       "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
       "dev": true
-    },
-    "zen-observable": {
-      "version": "0.8.15",
-      "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
-      "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
-    },
-    "zen-observable-ts": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz",
-      "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==",
-      "requires": {
-        "@types/zen-observable": "0.8.3",
-        "zen-observable": "0.8.15"
-      }
     }
   }
 }
diff --git a/package.json b/package.json
index dbd68ea68072197d00f3a449b8b822b8c1b62266..fd5fa43c346e40b277d6bd69c6b6ac73aad488ef 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,8 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
+    "@appsignal/express": "^1.0.33",
+    "@appsignal/nodejs": "^2.4.2",
     "@firebase/app-types": "0.x",
     "@sentry/integrations": "^6.17.9",
     "@sentry/node": "^6.17.9",
@@ -38,6 +40,7 @@
     "cors": "^2.8.5",
     "etcd3": "^1.1.0",
     "express": "^4.17.1",
+    "express-status-monitor": "^1.3.4",
     "firebase-admin": "^8.9.2",
     "jsonwebtoken": "^8.5.1",
     "ldap-escape": "^2.0.3",
@@ -54,7 +57,7 @@
     "safari-push-notifications": "^0.5.0",
     "stompit": "^1.0.0",
     "swagger-ui-express": "^4.1.6",
-    "typeorm": "^0.2.41",
+    "typeorm": "^0.3.9",
     "web-push": "^3.4.4"
   },
   "devDependencies": {
diff --git a/src/app.ts b/src/app.ts
index 31c5d9254643a793c91c24bbcbff9c70ade624f2..ae1e890c0d75072556d1c4459ded8c87ae7558f9 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -15,6 +15,9 @@ import { ChannelsController } from './controllers/channels/controller';
 import * as fs from 'fs';
 import * as https from 'https';
 
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { appsignal } = require('../appsignal');
+
 Configuration.load();
 
 // Note https://github.com/typestack/class-transformer/issues/563
@@ -50,10 +53,16 @@ const origins =
 //create express server
 const app = express();
 
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { expressMiddleware } = require('@appsignal/express');
+
 // Sentry initialize
 sentry.sentryInit(app);
 sentry.sentryHandleRequests(app);
 
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+// app.use(require('express-status-monitor')());
+
 let server;
 
 // Parse class-validator classes into JSON Schema:
@@ -112,6 +121,9 @@ const swaggerOptions = {
 app.get('/swagger/swagger.json', (req, res) => res.json(spec));
 app.use('/swagger', swaggerUiExpress.serveFiles(null, swaggerOptions), swaggerUiExpress.setup(null, swaggerOptions));
 
+// ADD THIS AFTER ANY OTHER EXPRESS MIDDLEWARE, BUT BEFORE ANY ROUTES!
+app.use(expressMiddleware(appsignal));
+
 // Render spec on root:
 app.get('/', (_req, res) => {
   res.json(spec);
diff --git a/src/controllers/channels/dto.ts b/src/controllers/channels/dto.ts
index 494ba1c495abe4f8c5ed091174fc154527bcd65d..ec47dc1db29284ac92b330f7b2304d80efc03162 100644
--- a/src/controllers/channels/dto.ts
+++ b/src/controllers/channels/dto.ts
@@ -23,6 +23,7 @@ import { User } from '../../models/user';
 import { AuthorizationBag } from '../../models/authorization-bag';
 import { v4 } from 'uuid';
 
+// TODO: fix relationship counters
 export class CategoryResponse {
   @IsString()
   id: string;
@@ -378,12 +379,12 @@ export class ChannelStatsResponse {
 
   constructor(channel: Channel) {
     this.id = channel.id;
-    this.members = channel.members.length;
+    // this.members = channel.members.length;
     this.unsubscribed = channel.unsubscribed.length;
     this.owner = channel.owner;
     this.creationDate = channel.creationDate;
     this.lastActivityDate = channel.lastActivityDate;
-    this.groups = channel.groups.length;
+    // this.groups = channel.groups.length;
   }
 }
 
@@ -509,8 +510,8 @@ export class GetChannelResponse extends GetChannelPublicResponse {
     this.send = send;
     this.relationships = new RelationshipResponse(
       channel.notificationCount,
-      channel.members?.length || 0,
-      channel.groups?.length || 0,
+      0, // channel.members?.length || 0,
+      0 // channel.groups?.length || 0,
     );
     this.sendPrivate = channel.sendPrivate;
   }
@@ -1335,10 +1336,8 @@ export class ChannelResponse {
     this.owner = channel.owner;
     this.name = channel.name;
     this.description = channel.description;
-    this.members = channel.members;
     this.adminGroup = channel.adminGroup;
     this.unsubscribed = channel.unsubscribed;
-    this.groups = channel.groups;
     //this.notifications = channel.notifications;
     this.visibility = channel.visibility;
     this.archive = channel.archive;
@@ -1353,20 +1352,9 @@ export class ChannelResponse {
     this.category = channel.category;
     this.tags = channel.tags;
     this.sendPrivate = channel.sendPrivate;
-    this.relationships = new RelationshipResponse(
-      channel.notificationCount,
-      channel.members?.length || 0,
-      channel.groups?.length || 0,
-    );
     this.channelFlags = channel.channelFlags;
     this.channelType = channel.channelType;
   }
-
-  async channelFilters(authBag: AuthorizationBag, chan: Channel): Promise<void> {
-    //this.subscribed = await chan.isSubscribedByName(authBag.userName);
-    this.manage = await chan.isAdmin(authBag);
-    this.send = await chan.canSendByForm(authBag);
-  }
 }
 
 @JSONSchema({
diff --git a/src/models/api-key-object.ts b/src/models/api-key-object.ts
index c2b3ea0a070a5bf0d299f74e82f1b96139d5bba4..fb19b9286bbb5680b6147bcae96fa094d09a584a 100644
--- a/src/models/api-key-object.ts
+++ b/src/models/api-key-object.ts
@@ -1,9 +1,12 @@
-import { Column } from 'typeorm';
+import { Column, PrimaryGeneratedColumn } from 'typeorm';
 import { HexBase64BinaryEncoding } from 'crypto';
 
 const crypto = require('crypto');
 
 export abstract class ApiKeyObject {
+  @PrimaryGeneratedColumn('uuid')
+  id: string;
+
   //this is a hash of the API key and the salt
   @Column({ nullable: true })
   APIKey: string;
diff --git a/src/models/category.ts b/src/models/category.ts
index 3b2e83d0231b691293cc4753702545c3a5d85a1c..0c5b309e774cf8198b4af75a3b8756216fc92c1b 100644
--- a/src/models/category.ts
+++ b/src/models/category.ts
@@ -1,12 +1,4 @@
-import {
-  Entity,
-  PrimaryGeneratedColumn,
-  Column,
-  Index,
-  Tree,
-  TreeChildren,
-  TreeParent,
-} from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, Index, Tree, TreeChildren, TreeParent, EntityManager } from 'typeorm';
 
 @Entity('Categories')
 @Tree('closure-table')
@@ -33,4 +25,12 @@ export class Category {
       this.info = category.info;
     }
   }
+
+  static async getDescendantIds(tm: EntityManager, categoryId: string): Promise<string[]> {
+    const searchParamCategory = new Category({});
+    searchParamCategory.id = categoryId;
+    const descendants = await tm.getTreeRepository(Category).findDescendants(searchParamCategory);
+    const descendantIds = descendants.map(cat => cat.id);
+    return descendantIds;
+  }
 }
diff --git a/src/models/cern-authorization-service.ts b/src/models/cern-authorization-service.ts
index 5ea176581f12111167d7a95b8ef842b880534e18..87ed0c6cfdbc5ba7448398d1d6df720f71ccd6b0 100644
--- a/src/models/cern-authorization-service.ts
+++ b/src/models/cern-authorization-service.ts
@@ -2,6 +2,11 @@ import requestPromise = require('request-promise-native');
 import * as memoize from 'memoizee';
 import { User } from '../models/user';
 
+export interface Group {
+  groupId: string; // uuid
+  groupIdentifier: string; //slug
+}
+
 export class CernAuthorizationService {
   static async getAuthTokenNoCache() {
     return requestPromise.post(process.env.AUTHORIZATION_SERVICE_API_TOKEN_URL, {
@@ -152,7 +157,7 @@ export class CernAuthorizationService {
   }
 
   // getCurrentUserGroups for current User
-  static async _getCurrentUserGroupsNoCache(userName: string): Promise<any[]> {
+  static async _getCurrentUserGroupsNoCache(userName: string): Promise<Group[]> {
     if (!userName) return [];
     console.debug(`getCurrentUserGroups miss cache: ${userName}`);
     // Using /precomputed endpoint for faster recursive resolution (uses a 15 minutes cached computed result)
@@ -166,7 +171,7 @@ export class CernAuthorizationService {
   }
 
   // Recursive paginated result handling. Takes absolutePath as parameter to align with next field value from response
-  static async _getCurrentUserGroupsNoCachePaginated(nextUri: string): Promise<any[]> {
+  static async _getCurrentUserGroupsNoCachePaginated(nextUri: string): Promise<Group[]> {
     let res = [];
     try {
       const grappadata = JSON.parse(
@@ -198,7 +203,7 @@ export class CernAuthorizationService {
     preFetch: true,
   });
 
-  static async getCurrentUserGroups(userName: string) {
+  static async getCurrentUserGroups(userName: string): Promise<Group[]> {
     return CernAuthorizationService._memoizedGetCurrentUserGroups(userName);
   }
 }
diff --git a/src/models/channel.ts b/src/models/channel.ts
index 68373e94dd405937879315cb7df65face2dcec89..c8c3b86b457dc74ea7f1bd19133197985ab5dc83 100644
--- a/src/models/channel.ts
+++ b/src/models/channel.ts
@@ -1,15 +1,20 @@
 import {
   BeforeInsert,
   BeforeUpdate,
+  Brackets,
   Column,
   DeleteDateColumn,
   Entity,
+  EntityManager,
+  FindRelationsNotFoundError,
+  In,
   Index,
   JoinTable,
   ManyToMany,
   ManyToOne,
   OneToMany,
   PrimaryGeneratedColumn,
+  Raw,
 } from 'typeorm';
 import { IsEmail, IsOptional, Validate } from 'class-validator';
 import { User } from './user';
@@ -31,6 +36,9 @@ import { ChannelHelpers } from './channel-helpers';
 import { Type } from 'class-transformer';
 import { ApiKeyObject } from './api-key-object';
 import { APIKeyTypeEnum } from '../middleware/api-key';
+import { SelectQueryBuilder } from 'typeorm/query-builder/SelectQueryBuilder';
+import { UserChannelCollection, UserChannelCollectionType } from './user-channel-collection';
+import { Group as AuthGroup } from './cern-authorization-service';
 
 @Entity('Channels')
 @Index(['slug', 'deleteDate'], { unique: true })
@@ -60,7 +68,7 @@ export class Channel extends ApiKeyObject {
   //  @JoinTable()
   @JoinTable({ name: 'channels_members__users' })
   @Type(() => User)
-  members: User[];
+  members: Promise<User[]>;
 
   @IsOptional()
   @ManyToOne(type => Group, { cascade: true })
@@ -76,7 +84,7 @@ export class Channel extends ApiKeyObject {
   @ManyToMany(type => Group)
   @JoinTable({ name: 'channels_groups__groups' })
   @Type(() => Group)
-  groups: Group[];
+  groups: Promise<Group[]>;
 
   @OneToMany(type => Notification, notification => notification.target, {
     cascade: true,
@@ -305,7 +313,7 @@ export class Channel extends ApiKeyObject {
 
   // Checks if user has authorization to send Notification to the channel
   // via Form/API
-  async canSendByFormWithCache(authorizationBag: AuthorizationBag, userGroups) {
+  async canSendByForm(transactionManager: EntityManager, authorizationBag: AuthorizationBag, userGroups: any[]) {
     // No permission to send by Form set
     if (!this.submissionByForm) return false;
 
@@ -313,118 +321,304 @@ export class Channel extends ApiKeyObject {
     if (!authorizationBag) return false;
 
     // If user is an APIKey and sendByForm APIKey is allowed
-    if (
-      this.submissionByForm.includes(SubmissionByForm.apikey) &&
-      this.APIKey &&
-      authorizationBag.isApiKey &&
-      authorizationBag.APIKey.hasRights(APIKeyTypeEnum.Channel, this.id)
-    ) {
-      return true;
-    }
+    const hasApiKeyAccess = this.hasApiKeyAccess(authorizationBag);
+    console.debug('hasApiKeyAccess', hasApiKeyAccess);
+    if (hasApiKeyAccess) return true;
+
     // If user is channel admin
-    if (
-      this.submissionByForm.includes(SubmissionByForm.administrators) &&
-      (await this.isAdminWithCache(authorizationBag, userGroups))
-    ) {
-      return true;
+    if (this.submissionByForm.includes(SubmissionByForm.administrators)) {
+      const hasAdminAccess = await this.hasAdminAccess(transactionManager, authorizationBag, userGroups);
+      console.debug('hasAdminAccess', hasAdminAccess);
+      if (hasAdminAccess) return true;
     }
+
     // If user is channel member
-    if (
-      this.submissionByForm.includes(SubmissionByForm.members) &&
-      (await this.isMemberWithCache(authorizationBag.user, userGroups))
-    ) {
-      return true;
+    if (this.submissionByForm.includes(SubmissionByForm.members)) {
+      const hasMemberAccess = await this.hasMemberAccess(transactionManager, authorizationBag, userGroups);
+      console.debug('hasMemberAccess', hasMemberAccess);
+      if (hasMemberAccess) return true;
     }
 
     // Or else you are not authorized
     return false;
   }
 
-  // Checks if user has authorization to send Notification to the channel
-  // via Form/API
-  async canSendByForm(authorizationBag: AuthorizationBag, targetedNotification = false): Promise<boolean> {
-    // No permission to send by Form set
-    if (!this.submissionByForm) return false;
+  // Get QB to match with tags
+  static qbMatchTags(tagIds: string[]): Brackets {
+    return new Brackets(tagsQB => {
+      tagsQB.where('tags.id In (:...tags)', { tags: tagIds });
+    });
+  }
 
-    // No authorizationBag means no send
-    if (!authorizationBag) return false;
+  // Get QB to match with search category ids
+  static qbMatchCategory(categoryIds: string[]): Brackets {
+    return new Brackets(categoryQB => {
+      categoryQB.where('channel.category.id In (:...categoryIds)', { categoryIds: categoryIds });
+    });
+  }
 
-    // If user is an APIKey and sendByForm APIKey is allowed
-    if (
-      this.submissionByForm.includes(SubmissionByForm.apikey) &&
-      this.APIKey &&
-      authorizationBag.isApiKey &&
-      authorizationBag.APIKey.hasRights(APIKeyTypeEnum.Channel, this.id)
-    ) {
-      return true;
-    }
-    // If user is channel admin
-    if (this.submissionByForm.includes(SubmissionByForm.administrators) && (await this.isAdmin(authorizationBag))) {
-      return true;
-    }
+  // Get QB Brackets query search text match
+  static qbSearchText(searchText: string): Brackets {
+    console.log('search texing');
+    return new Brackets(searchTextQB => {
+      searchTextQB.where(
+        'channel.description Like :searchText or channel.name Like :searchText or owner.username Like :searchText',
+        { searchText: `%${searchText}%` },
+      );
+    });
+  }
 
-    // Targeted notifications are not allowed for members
-    if (targetedNotification) {
-      return false;
+  static qbChannelsMemberViaUser(qb: SelectQueryBuilder<unknown>, userId: string, restricted = false): string {
+    const subQuery = qb
+      .subQuery()
+      .select('channel.id')
+      .from(Channel, 'channel')
+      .innerJoin('channel.members', 'members')
+      .where(':userId IN (members.id)', {
+        userId: userId,
+      })
+      .distinct();
+    if (restricted) {
+      qb.andWhere('channel.visibility = :visibility', {
+        visibility: Visibility.restricted,
+      });
     }
+    return 'channel.id IN ' + subQuery.getQuery();
+  }
 
-    // If user is channel member
-    if (this.submissionByForm.includes(SubmissionByForm.members) && (await this.isMember(authorizationBag.user))) {
-      return true;
+  static qbChannelsMemberViaGroup(
+    qb: SelectQueryBuilder<unknown>,
+    userGroups: AuthGroup[],
+    restricted = false,
+  ): string {
+    const subQuery = qb
+      .subQuery()
+      .select('channel.id')
+      .from(Channel, 'channel')
+      .innerJoin('channel.groups', 'groups')
+      .where('groups.id IN (:...userGroups)', {
+        userGroups: userGroups.map(g => g.groupId),
+      })
+      .distinct();
+
+    if (restricted) {
+      subQuery.andWhere('channel.visibility = :visibility', {
+        visibility: Visibility.restricted,
+      });
     }
 
-    // Or else you are not authorized
-    return false;
+    return 'channel.id IN ' + subQuery.getQuery();
   }
 
-  async canSendCritical(authorizationBag: AuthorizationBag) {
-    if (!authorizationBag) return false;
-    if (!this.channelFlags?.includes(ChannelFlags.critical)) return false;
+  // Filter channels by QB according to the subscribed/owned/favorite toggle
+  static filterChannels(
+    qb: SelectQueryBuilder<unknown>,
+    userId: string,
+    userGroups: AuthGroup[],
+    supporter: boolean,
+    isOwner: boolean,
+    isSubscribed: boolean,
+    isFavorite: boolean,
+  ): Brackets {
+    return new Brackets(filterQB => {
+      if (isFavorite) {
+        filterQB.andWhere(sqb => {
+          const subQuery = sqb
+            .subQuery()
+            .select('collection.channelId')
+            .from(UserChannelCollection, 'collection')
+            .where('collection.type = :type', {
+              type: UserChannelCollectionType.FAVORITE,
+            })
+            .andWhere('collection.userId = :userId', {
+              userId: userId,
+            })
+            .getQuery();
+          return 'channel.id IN ' + subQuery;
+        });
+
+        return;
+      }
 
-    if (
-      this.submissionByForm.includes(SubmissionByForm.apikey) &&
-      this.APIKey &&
-      authorizationBag.isApiKey &&
-      authorizationBag.APIKey.hasRights(APIKeyTypeEnum.Channel, this.id)
-    ) {
-      return true;
-    }
-    if (this.submissionByForm.includes(SubmissionByForm.administrators) && (await this.isAdmin(authorizationBag))) {
-      return true;
-    }
+      if (isOwner) {
+        filterQB.andWhere('owner.id = :userId or adminGroup.id IN (:...userGroups)', {
+          userId: userId,
+          userGroups: userGroups.map(g => g.groupId),
+        });
+
+        filterQB.andWhere(
+          new Brackets(qb => {
+            qb.where('owner.id = :userId', {
+              userId: userId,
+            });
+            if (userGroups && userGroups.length > 0)
+              qb.orWhere('adminGroup.id IN (:...userGroups)', {
+                userGroups: userGroups.map(g => g.groupId),
+              });
+          }),
+        );
+
+        return;
+      }
 
-    return false;
-  }
+      if (isSubscribed) {
+        filterQB.andWhere(sqb => {
+          const subQuery = sqb
+            .subQuery()
+            .select('channel.id')
+            .from(Channel, 'channel')
+            .leftJoin('channel.unsubscribed', 'unsubscribed')
+            .where(':userId IN (unsubscribed.id)', {
+              userId: userId,
+            })
+            .getQuery();
+          return 'channel.id NOT IN ' + subQuery;
+        });
+
+        filterQB.andWhere(
+          new Brackets(qb => {
+            qb.where(sqb => this.qbChannelsMemberViaUser(sqb, userId));
+            if (userGroups && userGroups.length > 0) {
+              qb.orWhere(sqb => this.qbChannelsMemberViaGroup(sqb, userGroups));
+            }
+          }),
+        );
+
+        return;
+      }
 
-  addGroup(group: Group) {
-    if (this.groups) {
-      if (!this.groups.find((g: Group) => g.groupIdentifier === group.groupIdentifier)) {
-        this.groups.push(group);
+      if (supporter) {
+        return;
       }
-    } else {
-      this.groups = [group];
-    }
+
+      // restricted, owned channels
+      filterQB.orWhere(
+        new Brackets(qb => {
+          qb.where('owner.id = :userId', {
+            userId: userId,
+          });
+          if (userGroups && userGroups.length > 0)
+            qb.orWhere('adminGroup.id IN (:...userGroups)', {
+              userGroups: userGroups.map(g => g.groupId),
+            });
+        }),
+      );
+
+      // restricted, subscribed channels
+      filterQB.orWhere(sqb => this.qbChannelsMemberViaUser(sqb, userId, true));
+
+      filterQB.orWhere(sqb => this.qbChannelsMemberViaGroup(sqb, userGroups, true));
+
+      filterQB.orWhere('owner.id = :userId', {
+        userId: userId,
+      });
+
+      filterQB.orWhere('channel.visibility In (:...visibleTypes)', {
+        visibleTypes: [Visibility.public, Visibility.internal],
+        userId: userId,
+      });
+    });
   }
 
-  removeGroup(group: Group) {
-    this.groups = this.groups.filter(g => g.id !== group.id);
+  // Get QB Brackets channels by access: where user is Admin
+  static qbUserIsAdmin(qb: SelectQueryBuilder<unknown>, userId: string, userGroups: any[]): Brackets {
+    return new Brackets(accessQB => {
+      accessQB.where('owner.id = :userId', { userId: userId });
+      if (userGroups && userGroups.length > 0) {
+        accessQB.orWhere('adminGroup.id IN (:...userGroups)', {
+          userGroups: userGroups.map(g => g.groupId),
+        });
+      }
+    });
   }
 
-  addMember(user: User) {
-    if (this.members) {
-      if (!this.members.find(u => u.compareTo(user))) {
-        this.members.push(user);
+  // Get QB Brackets channels by access: where user is Member
+  static qbUserIsMember(userId: string, userGroups: any[]): Brackets {
+    return new Brackets(accessQB => {
+      accessQB.where(':userId IN (members.id)', {
+        userId: userId,
+      });
+      if (userGroups && userGroups.length > 0) {
+        accessQB.orWhere('groups.id IN (:...userGroups)', {
+          userGroups: userGroups.map(g => g.groupId),
+        });
       }
-    } else {
-      this.members = [user];
-    }
-    if (this.unsubscribed && this.unsubscribed.length > 0) {
-      this.unsubscribed = this.unsubscribed.filter(u => !u.compareTo(user));
-    }
+    });
+  }
+
+  // Filter channels by access: if user has authorization to send Notification to the channel via Form/API
+  static qbFilterByCanSendByForm(
+    qb: SelectQueryBuilder<unknown>,
+    authorizationBag: AuthorizationBag,
+    userGroups: any[],
+    isTargetedNotification = false,
+    channelId: string,
+  ): void {
+    // Filter by access
+    qb.andWhere(
+      // Administrators
+      new Brackets(submissiontypeQB => {
+        submissiontypeQB.where(
+          new Brackets(adminQB => {
+            adminQB.where(':administrators = ANY(channel.submissionByForm)', {
+              administrators: SubmissionByForm.administrators,
+            });
+            adminQB.andWhere(
+              new Brackets(adminGroupQB => {
+                adminGroupQB.where('owner.id = :userId', { userId: authorizationBag.userId });
+                if (userGroups && userGroups.length > 0) {
+                  adminGroupQB.orWhere('adminGroup.id IN (:...userGroups)', {
+                    userGroups: userGroups.map(g => g.groupId),
+                  });
+                }
+              }),
+            );
+          }),
+        );
+
+        // Members
+        // Targeted notifications are not allowed for members
+        if (!isTargetedNotification) {
+          submissiontypeQB.orWhere(
+            new Brackets(memberQB => {
+              memberQB.where(':members = ANY(channel.submissionByForm)', {
+                members: SubmissionByForm.members,
+              });
+              memberQB.andWhere(
+                new Brackets(userQB => {
+                  userQB.where(':userId IN (members.id)', {
+                    userId: authorizationBag.userId,
+                  });
+                  if (userGroups && userGroups.length > 0) {
+                    userQB.orWhere('groups.id IN (:...userGroups)', {
+                      userGroups: userGroups.map(g => g.groupId),
+                    });
+                  }
+                }),
+              );
+            }),
+          );
+        }
+
+        // If user is an APIKey and sendByForm APIKey is allowed
+        if (
+          authorizationBag.isApiKey &&
+          authorizationBag.APIKey.type == APIKeyTypeEnum.Channel &&
+          authorizationBag.APIKey.id == channelId
+        ) {
+          submissiontypeQB.orWhere(':apikey = ANY(channel.submissionByForm)', {
+            apikey: SubmissionByForm.apikey,
+          });
+        }
+      }),
+    );
   }
 
-  removeMember(user: User) {
-    this.members = this.members.filter(u => !u.compareTo(user));
+  async canSendCritical(authorizationBag: AuthorizationBag) {
+    if (!authorizationBag) return false;
+    if (!this.channelFlags?.includes(ChannelFlags.critical)) return false;
+
+    return false;
   }
 
   unsubscribeMember(user: User) {
@@ -442,7 +636,8 @@ export class Channel extends ApiKeyObject {
   }
 
   private async getMembers(subscribedOnly: boolean): Promise<User[]> {
-    let members = this.members.map(
+    const channelMembers = await this.members;
+    let members = channelMembers.map(
       u =>
         new User({
           id: u.id,
@@ -455,7 +650,8 @@ export class Channel extends ApiKeyObject {
       members = members.filter(u => !this.isUnsubscribed(u), this);
     }
 
-    for (const group of this.groups) {
+    const channelGroups = await this.groups;
+    for (const group of channelGroups) {
       let groupMembers = await group.getMembers();
       if (subscribedOnly && this.unsubscribed) {
         groupMembers = groupMembers.filter(u => !this.isUnsubscribed(u), this);
@@ -508,14 +704,202 @@ export class Channel extends ApiKeyObject {
       return false;
     }
 
-    if (ChannelHelpers.memoizedIsInMembers(this.members, user)) {
+    if (ChannelHelpers.memoizedIsInMembers(await this.members, user)) {
       return true;
     }
 
-    if (ChannelHelpers.memoizedIsInGroups(this.groups, userGroups)) {
+    if (ChannelHelpers.memoizedIsInGroups(await this.groups, userGroups)) {
       return true;
     }
 
     return false;
   }
+
+  // --------- NEW TYPEORM ACCESS QUERIES -------------- //
+  //
+  // Most implementations use transactionManager.find instead of queryBuilder due to performance
+  // limitations in queryBuilder and relations.
+  // Guidelines:
+  // - avoid selecting relation arrays without implementing pagination
+  // - avoid using queryBuilder with multiple relation selects
+  // - avoid returning array relations in transactionManager find methods
+  // ---------------------------------------------------
+  hasApiKeyAccess(authorizationBag: AuthorizationBag): boolean {
+    return (
+      this.submissionByForm.includes(SubmissionByForm.apikey) &&
+      !!this.APIKey &&
+      authorizationBag.isApiKey &&
+      authorizationBag.APIKey.hasRights(APIKeyTypeEnum.Channel, this.id)
+    );
+  }
+
+  async hasMemberAccess(
+    transactionManager: EntityManager,
+    authorizationBag: AuthorizationBag,
+    userGroups: any[],
+  ): Promise<boolean> {
+    // check is member
+    const hasMemberAccessViaUser = await this.isMemberViaUser(transactionManager, authorizationBag.userId);
+    console.debug('hasMemberAccessViaUser', hasMemberAccessViaUser);
+    if (hasMemberAccessViaUser) return true;
+
+    const hasMemberAccessViaGroup = await this.isMemberViaGroup(transactionManager, userGroups);
+    console.debug('hasMemberAccessViaGroup', hasMemberAccessViaGroup);
+    return hasMemberAccessViaGroup;
+  }
+
+  async isMemberViaUser(transactionManager: EntityManager, userId: string): Promise<boolean> {
+    const isMemberViaUser = await transactionManager.findOne(Channel, {
+      relations: {
+        members: true,
+      },
+      where: {
+        id: this.id,
+        members: {
+          id: Raw(alias => `:userId IN (${alias})`, { userId: userId }),
+        },
+      },
+      select: {
+        id: true,
+        members: {},
+      },
+    });
+
+    return !!isMemberViaUser;
+  }
+
+  async isMemberViaGroup(transactionManager: EntityManager, userGroups: any[]): Promise<boolean> {
+    const isMemberViaGroup = await transactionManager.findOne(Channel, {
+      relations: {
+        groups: true,
+      },
+      where: {
+        id: this.id,
+        groups: {
+          id: In(userGroups.map(g => g.groupId)),
+        },
+      },
+      select: {
+        id: true,
+        groups: {},
+      },
+    });
+
+    return !!isMemberViaGroup;
+  }
+
+  async isGroupSubscribed(transactionManager: EntityManager, group: Group): Promise<boolean> {
+    const isGroupSubscribed = await transactionManager.findOne(Channel, {
+      relations: {
+        groups: true,
+      },
+      where: {
+        id: this.id,
+        groups: {
+          id: Raw(alias => `:groupId IN (${alias})`, { groupId: group.id }),
+        },
+      },
+      select: {
+        id: true,
+        groups: {},
+      },
+    });
+
+    return !!isGroupSubscribed;
+  }
+
+  async isUserSubscribed(transactionManager: EntityManager, user: User): Promise<boolean> {
+    const isMemberSubscribed = await transactionManager.findOne(Channel, {
+      relations: {
+        members: true,
+      },
+      where: {
+        id: this.id,
+        members: {
+          id: Raw(alias => `:userId IN (${alias})`, { userId: user.id }),
+        },
+      },
+      select: {
+        id: true,
+        members: {},
+      },
+    });
+
+    return !!isMemberSubscribed;
+  }
+
+  async isOwner(transactionManager: EntityManager, authorizationBag: AuthorizationBag): Promise<boolean> {
+    const isOwner = await transactionManager.findOne(Channel, {
+      relations: {
+        owner: true,
+      },
+      where: {
+        id: this.id,
+        owner: {
+          id: authorizationBag.userId,
+        },
+      },
+    });
+
+    return !!isOwner;
+  }
+
+  async isAdminViaGroups(
+    transactionManager: EntityManager,
+    authorizationBag: AuthorizationBag,
+    userGroups: any[],
+  ): Promise<boolean> {
+    const isAdmin = await transactionManager.findOne(Channel, {
+      relations: {
+        adminGroup: true,
+      },
+      where: {
+        id: this.id,
+        adminGroup: {
+          id: In(userGroups.map(g => g.groupId)),
+        },
+      },
+    });
+
+    return !!isAdmin;
+  }
+
+  async hasAdminAccess(txm: EntityManager, authBag: AuthorizationBag, userGroups: any[]): Promise<boolean> {
+    // queries with optional relations and where by relation property, the join changes from "left" to "inner" incorrectly
+    // https://github.com/typeorm/typeorm/issues/9395
+    // const hasAdminAccess = await txm.findOne(Channel, {
+    //   where: [
+    //     {
+    //       id: this.id,
+    //       owner: {
+    //         id: authBag.userId,
+    //       }
+    //     },
+    //     {
+    //       id: this.id,
+    //       adminGroup: {
+    //         id: In(userGroups.map(g => g.groupId)),
+    //       },
+    //     },
+    //   ],
+    // });
+
+    return (await this.isOwner(txm, authBag)) || (await this.isAdminViaGroups(txm, authBag, userGroups));
+  }
+
+  async addGroup(transactionManager: EntityManager, group: Group): Promise<void> {
+    await transactionManager.createQueryBuilder().relation(Channel, 'groups').of(this).add(group);
+  }
+
+  async removeGroup(transactionManager: EntityManager, group: Group): Promise<void> {
+    await transactionManager.createQueryBuilder().relation(Channel, 'groups').of(this).remove(group);
+  }
+
+  async addUser(transactionManager: EntityManager, user: User): Promise<void> {
+    await transactionManager.createQueryBuilder().relation(Channel, 'members').of(this).add(user);
+  }
+
+  async removeUser(transactionManager: EntityManager, user: User): Promise<void> {
+    await transactionManager.createQueryBuilder().relation(Channel, 'members').of(this).remove(user);
+  }
 }
diff --git a/src/models/device.ts b/src/models/device.ts
index 09a14412186337c94454a0e97b106b00ab98d2ad..7dd430b5fef2759a2cc1091a52499009ff5b6d69 100644
--- a/src/models/device.ts
+++ b/src/models/device.ts
@@ -1,72 +1,71 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } from "typeorm";
-import { User } from "./user";
+import { Entity, ManyToOne, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } from 'typeorm';
+import { User } from './user';
 
 export enum DeviceType {
-    BROWSER = 'BROWSER',
-    APP  = 'APP',
-    MAIL  = 'MAIL',
+  BROWSER = 'BROWSER',
+  APP = 'APP',
+  MAIL = 'MAIL',
 }
 
 export enum DeviceSubType {
-    SAFARI = 'SAFARI',
-    OTHER = 'OTHER',
-    IOS = 'IOS',
-    ANDROID = 'ANDROID',
-    WINDOWS = 'WINDOWS',
-    LINUX = 'LINUX',
-    MAC = 'MAC',
-    MATTERMOST = 'MATTERMOST',    
-    PRIMARY = 'PRIMARY'
+  SAFARI = 'SAFARI',
+  OTHER = 'OTHER',
+  IOS = 'IOS',
+  ANDROID = 'ANDROID',
+  WINDOWS = 'WINDOWS',
+  LINUX = 'LINUX',
+  MAC = 'MAC',
+  MATTERMOST = 'MATTERMOST',
+  PRIMARY = 'PRIMARY',
 }
 
-@Entity("Devices")
+@Entity('Devices')
 export class Device {
-    @PrimaryGeneratedColumn("uuid")
-    id: string;
+  @PrimaryGeneratedColumn('uuid')
+  id: string;
 
-    @Column({ nullable: false })
-    name: string;
+  @Column({ nullable: false })
+  name: string;
 
-    @ManyToOne((type) => User, (user) => user.devices)
-    user: User;
+  @ManyToOne(type => User, user => user.devices)
+  user: User;
 
-    @Column({ nullable: true })
-    info: string;
+  @Column({ nullable: true })
+  info: string;
 
-    @Column('varchar')
-    type: DeviceType;
+  @Column('varchar')
+  type: DeviceType;
 
-    @Column('varchar', {
-        nullable: true,
-    })
-    subType: DeviceSubType;
+  @Column('varchar', {
+    nullable: true,
+  })
+  subType: DeviceSubType;
 
-    // Contains a changeable random anonymous id, mainly for Apple devices
-    @Column({ type:"uuid", nullable: true })
-    uuid: string;
+  // Contains a changeable random anonymous id, mainly for Apple devices
+  @Column({ type: 'uuid', nullable: true })
+  uuid: string;
 
-    @Column({ nullable: false })
-    token: string;
+  @Column({ nullable: false })
+  token: string;
 
-    // Trim token in case it's between quotes (mostly Safari business)
-    @BeforeUpdate()
-    @BeforeInsert()
-    async trimToken(): Promise<void> {
-        if (!this.token)
-            return;
-        if (this.token.charAt(0) === '"' && this.token.charAt(this.token.length -1) === '"')
-            this.token = this.token.substr(1, this.token.length -2);
-    }
+  // Trim token in case it's between quotes (mostly Safari business)
+  @BeforeUpdate()
+  @BeforeInsert()
+  async trimToken(): Promise<void> {
+    if (!this.token) return;
+    if (this.token.charAt(0) === '"' && this.token.charAt(this.token.length - 1) === '"')
+      this.token = this.token.substr(1, this.token.length - 2);
+  }
 
-    constructor(device) {
-        if (device) {
-            this.name = device.name;
-            this.user = device.user;
-            this.info = device.info;
-            this.type = device.type;
-            this.subType = device.subType;
-            this.uuid = device.uuid;
-            this.token = device.token;
-        }
+  constructor(device) {
+    if (device) {
+      this.name = device.name;
+      this.user = device.user;
+      this.info = device.info;
+      this.type = device.type;
+      this.subType = device.subType;
+      this.uuid = device.uuid;
+      this.token = device.token;
     }
+  }
 }
diff --git a/src/models/mute.ts b/src/models/mute.ts
index dac91a6c1abae9a45b058abe6b83ebd04c4fb5c7..6ad0ab9699f084449152ac69a778bcc1ee8be54d 100644
--- a/src/models/mute.ts
+++ b/src/models/mute.ts
@@ -1,28 +1,23 @@
-import {
-  Entity,
-  ManyToOne,
-  PrimaryGeneratedColumn,
-  Column,
-} from "typeorm";
-import { User } from "./user";
-import { Channel } from "./channel";
-import { BadRequestError } from "routing-controllers";
-import { EntityManager } from "typeorm";
+import { Entity, ManyToOne, PrimaryGeneratedColumn, Column } from 'typeorm';
+import { User } from './user';
+import { Channel } from './channel';
+import { BadRequestError } from 'routing-controllers';
+import { EntityManager } from 'typeorm';
 import { AESCypher } from '../middleware/aes-cipher';
 
-@Entity("Mutes")
+@Entity('Mutes')
 export class Mute {
-  @PrimaryGeneratedColumn("uuid")
+  @PrimaryGeneratedColumn('uuid')
   id: string;
 
-  @ManyToOne((type) => User, (user) => user.mutes)
+  @ManyToOne(type => User, user => user.mutes)
   user: User;
 
-  @ManyToOne((type) => Channel, { nullable: true })
+  @ManyToOne(type => Channel, { nullable: true })
   target: Channel;
 
   @Column({
-    enum: ["PERMANENT", "RANGE"],
+    enum: ['PERMANENT', 'RANGE'],
   })
   type: string;
 
@@ -45,49 +40,50 @@ export class Mute {
     }
   }
 
-  async validateMute(transactionManager: EntityManager)
-  {
+  async validateMute(transactionManager: EntityManager) {
     // If PERMANENT, clean start and end date if any
     if (this.type === 'PERMANENT') {
       this.start = null;
       this.end = null;
     }
 
-    if ((this.type === 'RANGE') && !this.end)
-      throw new BadRequestError("Invalid Mute End Date: if mute range selected, end date must be set");
+    if (this.type === 'RANGE' && !this.end)
+      throw new BadRequestError('Invalid Mute End Date: if mute range selected, end date must be set');
 
-    if ((this.start === this.end) && this.start && this.end)
-      throw new BadRequestError("Invalid Mute Date Range: start date and end date must be different");
+    if (this.start === this.end && this.start && this.end)
+      throw new BadRequestError('Invalid Mute Date Range: start date and end date must be different');
 
-    if (this.end && (this.end < new Date())) {
+    if (this.end && this.end < new Date()) {
       throw new BadRequestError("Invalid Mute End Date: end time can't be in the past");
     }
 
-    if (this.start && this.end && (this.start >= this.end)) {
-      throw new BadRequestError("Invalid Mute dates: start must be before end");
+    if (this.start && this.end && this.start >= this.end) {
+      throw new BadRequestError('Invalid Mute dates: start must be before end');
     }
 
     // Check is a PERMANENT Global mute already exists for this user
     if (this.type === 'PERMANENT' && this.target == null) {
-      if (await transactionManager.findOne(Mute, {
-        relations: ["target"],
-        where: {
-          user: this.user,
-          type: "PERMANENT",
-          target: null,
-        },
-      }))
-        throw new BadRequestError("A Global Permanent Mute already exists for this user.");
+      if (
+        await transactionManager.findOne(Mute, {
+          relations: ['target'],
+          where: {
+            user: {
+              id: this.user,
+            },
+            type: 'PERMANENT',
+            target: null,
+          },
+        })
+      )
+        throw new BadRequestError('A Global Permanent Mute already exists for this user.');
     }
-    
+
     return true;
   }
-
 }
 
 export function decodeEmail(encryptedEmail: string) {
-  if (!encryptedEmail)
-    throw new BadRequestError("Invalid encrypted data");
+  if (!encryptedEmail) throw new BadRequestError('Invalid encrypted data');
 
   return AESCypher.decrypt(encryptedEmail);
 }
diff --git a/src/models/user-channel-collection.ts b/src/models/user-channel-collection.ts
index 6fa0208d694b553b81189602de16ae35074ffd39..40c00279e1993ebdc1fef8c94b597c5e425fc4ba 100644
--- a/src/models/user-channel-collection.ts
+++ b/src/models/user-channel-collection.ts
@@ -1,11 +1,4 @@
-import {
-  Entity,
-  Index,
-  JoinColumn,
-  ManyToOne,
-  PrimaryColumn,
-  RelationId,
-} from 'typeorm';
+import { Entity, Index, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from 'typeorm';
 import { Channel } from './channel';
 
 import { User } from './user';
@@ -19,28 +12,24 @@ export class UserChannelCollection {
   @PrimaryColumn('varchar')
   type: UserChannelCollectionType;
 
-  @ManyToOne(type => User, { primary: true })
+  @PrimaryColumn()
+  @RelationId((userChannelCollection: UserChannelCollection) => userChannelCollection.user)
+  userId: string;
+
+  @PrimaryColumn()
+  @RelationId((userChannelCollection: UserChannelCollection) => userChannelCollection.channel)
+  channelId: string;
+
+  @ManyToOne(type => User)
   @JoinColumn()
   @Index()
   user: User;
 
-  @RelationId(
-    (userChannelCollection: UserChannelCollection) =>
-      userChannelCollection.user,
-  )
-  userId: string;
-
   @Index()
-  @ManyToOne(type => Channel, { primary: true })
+  @ManyToOne(type => Channel)
   @JoinColumn()
   channel: Channel;
 
-  @RelationId(
-    (userChannelCollection: UserChannelCollection) =>
-      userChannelCollection.channel,
-  )
-  channelId: string;
-
   constructor(collection: UserChannelCollection) {
     if (collection) {
       this.channel = collection.channel;
diff --git a/src/services/impl/api-key/verify-api-key.ts b/src/services/impl/api-key/verify-api-key.ts
index 80c456a8aa091de1ab939afaa3ea5c97135ce97c..6a1e5a5297eb06db149d9575ff9f09c4fd573444 100644
--- a/src/services/impl/api-key/verify-api-key.ts
+++ b/src/services/impl/api-key/verify-api-key.ts
@@ -7,11 +7,7 @@ export class VerifyApiKey implements Command {
   constructor(private id: string, private key: string, private entity: EntityTarget<ApiKeyObject>) {}
 
   async execute(transactionManager: EntityManager): Promise<boolean> {
-    const obj = await transactionManager.findOne(this.entity, {
-      where: {
-        id: this.id,
-      },
-    });
+    const obj = await transactionManager.findOneBy(this.entity, { id: this.id });
 
     if (!obj) return false;
 
diff --git a/src/services/impl/auth/create-update-user/create-mattermost-device.ts b/src/services/impl/auth/create-update-user/create-mattermost-device.ts
index 9c0885a4dc7d85907f5b7bb4cd365d14c646befd..240e13ff08cb85e80223cc8ab1c75f14be29fa3c 100644
--- a/src/services/impl/auth/create-update-user/create-mattermost-device.ts
+++ b/src/services/impl/auth/create-update-user/create-mattermost-device.ts
@@ -1,40 +1,43 @@
-import { Command } from "../../command";
-import { EntityManager } from "typeorm";
-import { User } from "../../../../models/user";
-import { Device } from "../../../../models/device";
+import { Command } from '../../command';
+import { EntityManager } from 'typeorm';
+import { User } from '../../../../models/user';
+import { Device } from '../../../../models/device';
 
 export class CreateMattermostDevice implements Command {
+  constructor(private user: User) {}
 
-    constructor(private user: User) { }
-
-    async execute(transactionManager: EntityManager): Promise<Device> {
-        try {
-            // Get Mattermost device if exists
-            let mmdevice = await transactionManager.findOne(Device, {
-                relations: ["user"],
-                where: {
-                    user: this.user.id,
-                    info: this.user.email,
-                    type: "APP",
-                    subType: "MATTERMOST",
-                    token: this.user.email,
-                },
-            });
-            if (!mmdevice) {
-                // Create Mattermost Device with user mail address
-                mmdevice = await transactionManager.save(new Device({
-                    name: "Mattermost",
-                    user: this.user,
-                    info: this.user.email,
-                    type: "APP",
-                    subType: "MATTERMOST",
-                    token: this.user.email,
-                }));
-            }
-            return mmdevice;
-        } catch (ex) {
-            console.error(ex);
-            throw new Error("Failed to create Mattermost device.");
-        }
+  async execute(transactionManager: EntityManager): Promise<Device> {
+    try {
+      // Get Mattermost device if exists
+      let mmdevice = await transactionManager.findOne(Device, {
+        relations: ['user'],
+        where: {
+          user: {
+            id: this.user.id,
+          },
+          info: this.user.email,
+          type: 'APP',
+          subType: 'MATTERMOST',
+          token: this.user.email,
+        },
+      });
+      if (!mmdevice) {
+        // Create Mattermost Device with user mail address
+        mmdevice = await transactionManager.save(
+          new Device({
+            name: 'Mattermost',
+            user: this.user,
+            info: this.user.email,
+            type: 'APP',
+            subType: 'MATTERMOST',
+            token: this.user.email,
+          }),
+        );
+      }
+      return mmdevice;
+    } catch (ex) {
+      console.error(ex);
+      throw new Error('Failed to create Mattermost device.');
     }
+  }
 }
diff --git a/src/services/impl/auth/create-update-user/update-user-email.ts b/src/services/impl/auth/create-update-user/update-user-email.ts
index c382dcee44793a2fe05ee6ba5ab80e009fe5574e..8162be651028b19c2b3675158e0f3f4a5cf8b608 100644
--- a/src/services/impl/auth/create-update-user/update-user-email.ts
+++ b/src/services/impl/auth/create-update-user/update-user-email.ts
@@ -1,55 +1,58 @@
-import { Command } from "../../command";
-import { EntityManager } from "typeorm";
-import { User } from "../../../../models/user";
-import { Device } from "../../../../models/device";
+import { Command } from '../../command';
+import { EntityManager } from 'typeorm';
+import { User } from '../../../../models/user';
+import { Device } from '../../../../models/device';
 
 export class UpdateUserEmail implements Command {
+  constructor(private user: User, private email: string) {}
 
-    constructor(private user: User, private email: string) { }
+  async execute(transactionManager: EntityManager): Promise<User> {
+    try {
+      // Update User
+      this.user.email = this.email;
+      transactionManager.save(this.user);
 
-    async execute(transactionManager: EntityManager): Promise<User> {
-        try {
-            // Update User
-            this.user.email = this.email;
-            transactionManager.save(this.user);
+      // Update default device with new mail
+      const device = await transactionManager.findOne(Device, {
+        relations: ['user'],
+        where: {
+          user: {
+            id: this.user.id,
+          },
+          name: this.user.email,
+          type: 'MAIL',
+          token: this.user.email,
+        },
+      });
+      if (device) {
+        device.name = this.email;
+        device.token = this.email;
+        transactionManager.save(device);
+      }
 
-            // Update default device with new mail
-            const device = await transactionManager.findOne(Device, {
-                relations: ["user"],
-                where: {
-                    user: this.user.id,
-                    name: this.user.email,
-                    type: "MAIL",
-                    token: this.user.email,
-                },
-            });
-            if (device) {
-                device.name = this.email;
-                device.token = this.email;
-                transactionManager.save(device);
-            }
+      // Update Mattermost device with new mail
+      const mmdevice = await transactionManager.findOne(Device, {
+        relations: ['user'],
+        where: {
+          user: {
+            id: this.user.id,
+          },
+          info: this.user.email,
+          type: 'APP',
+          subType: 'MATTERMOST',
+          token: this.user.email,
+        },
+      });
+      if (mmdevice) {
+        mmdevice.info = this.email;
+        mmdevice.token = this.email;
+        transactionManager.save(mmdevice);
+      }
 
-             // Update Mattermost device with new mail
-             const mmdevice = await transactionManager.findOne(Device, {
-                relations: ["user"],
-                where: {
-                    user: this.user.id,
-                    info: this.user.email,
-                    type: "APP",
-                    subType: "MATTERMOST",
-                    token: this.user.email,
-                },
-            });
-            if (mmdevice) {
-                mmdevice.info = this.email;
-                mmdevice.token = this.email;
-                transactionManager.save(mmdevice);
-            }
-            
-            return this.user;
-        } catch (ex) {
-            console.error(ex, this.email);
-            throw new Error("Failed to update user or device with new email.");
-        }
+      return this.user;
+    } catch (ex) {
+      console.error(ex, this.email);
+      throw new Error('Failed to update user or device with new email.');
     }
+  }
 }
diff --git a/src/services/impl/auth/get-authenticated-user/GetAuthenticatedUser.ts b/src/services/impl/auth/get-authenticated-user/GetAuthenticatedUser.ts
index 83e2642aa20c28506577d9ac02630a61ee4d2363..c5630afff24108aaa6628ade777aa5e53d7a60f8 100644
--- a/src/services/impl/auth/get-authenticated-user/GetAuthenticatedUser.ts
+++ b/src/services/impl/auth/get-authenticated-user/GetAuthenticatedUser.ts
@@ -1,15 +1,11 @@
-import { Command } from "../../command";
-import { EntityManager } from "typeorm";
-import { User } from "../../../../models/user";
+import { Command } from '../../command';
+import { EntityManager } from 'typeorm';
+import { User } from '../../../../models/user';
 
-export class GetAuthenticatedUser implements Command{
+export class GetAuthenticatedUser implements Command {
+  constructor(private username: string) {}
 
-    constructor(private username: string){
-
-    }
-
-    async execute(transactionManager: EntityManager): Promise<User> {
-        return await transactionManager.findOne(User, {username: this.username} )
-    }
-
-}
\ No newline at end of file
+  async execute(transactionManager: EntityManager): Promise<User> {
+    return await transactionManager.findOneBy(User, { username: this.username });
+  }
+}
diff --git a/src/services/impl/categories/create-category.ts b/src/services/impl/categories/create-category.ts
index c8f9be47ccf2ef1c8abf821e7733ea0e6a8e223c..49c4374540de531059a2e23be5796c724d3d873c 100644
--- a/src/services/impl/categories/create-category.ts
+++ b/src/services/impl/categories/create-category.ts
@@ -14,7 +14,7 @@ export class CreateCategory implements Command {
     }
     // Check if category already exists
     if (
-      await transactionManager.findOne(Category, {
+      await transactionManager.findOneBy(Category, {
         name: this.category.name,
       })
     ) {
@@ -24,7 +24,7 @@ export class CreateCategory implements Command {
     const newCategory = new Category({ ...this.category });
 
     if (this.category.parent) {
-      const parentCategory = await transactionManager.findOne(Category, {
+      const parentCategory = await transactionManager.findOneBy(Category, {
         name: this.category.parent,
       });
       if (!parentCategory) {
diff --git a/src/services/impl/channels/add-group-to-channel.ts b/src/services/impl/channels/add-group-to-channel.ts
index eae547a090712abc44bb182fbb8084100257df26..4621963d154b67c16a630d3c415c3590082e0a89 100644
--- a/src/services/impl/channels/add-group-to-channel.ts
+++ b/src/services/impl/channels/add-group-to-channel.ts
@@ -6,10 +6,7 @@ import { AuthorizationBag } from '../../../models/authorization-bag';
 import { ServiceFactory } from '../../services-factory';
 import { GroupsServiceInterface } from '../../groups-service';
 import { AuditChannels } from '../../../log/auditing';
-import { ChannelResponse } from '../../../controllers/channels/dto';
-import { Group } from '../../../models/group';
 import { GroupResponse } from '../../../controllers/channels/dto';
-import { CernAuthorizationService } from '../../../models/cern-authorization-service';
 
 export class AddGroupToChannel implements Command {
   constructor(private groupName: string, private channelId: string, private authorizationBag: AuthorizationBag) {}
@@ -29,15 +26,13 @@ export class AddGroupToChannel implements Command {
     if (!(await channel.isAdmin(this.authorizationBag)))
       throw new ForbiddenError("You don't have the rights to manage this channel.");
 
-    //if added group was already added
-    if (channel.groups.filter(g => g.groupIdentifier === this.groupName).length > 0) {
+    const groupToAdd = await this.groupsService.GetOrCreateGroup(this.groupName);
+    if (await channel.isGroupSubscribed(transactionManager, groupToAdd)) {
       throw new ForbiddenError(`Group ${this.groupName} is already a member.`);
     }
 
-    const groupToAdd = await this.groupsService.GetOrCreateGroup(this.groupName);
-    channel.addGroup(groupToAdd);
-    const updatedChannel = await transactionManager.save(channel);
-    await AuditChannels.setValue(updatedChannel.id, {
+    await channel.addGroup(transactionManager, groupToAdd);
+    await AuditChannels.setValue(channel.id, {
       event: 'AddGroup',
       user: this.authorizationBag.email,
       groupIdentifier: this.groupName,
diff --git a/src/services/impl/channels/add-member-to-channel.ts b/src/services/impl/channels/add-member-to-channel.ts
index f2f8f981c5c8c7a98228282b66e03f0a56501667..c3d2f5bdcd7d7819d4ccca584cbe73ef18038d89 100644
--- a/src/services/impl/channels/add-member-to-channel.ts
+++ b/src/services/impl/channels/add-member-to-channel.ts
@@ -1,57 +1,106 @@
 import { Command } from '../command';
 import { EntityManager } from 'typeorm';
 import { Channel } from '../../../models/channel';
-import { User } from '../../../models/user';
 import { ForbiddenError, NotFoundError } from 'routing-controllers';
 import { ServiceFactory } from '../../services-factory';
 import { AuthorizationBag } from '../../../models/authorization-bag';
 import { UsersServiceInterface } from '../../users-service';
 import { AuditChannels } from '../../../log/auditing';
-import { AuthService } from '../../../services/auth-service';
+import { MemberResponse } from '../../../controllers/channels/dto';
 import { CernAuthorizationService } from '../../../models/cern-authorization-service';
-import { MemberResponse, ChannelResponse } from '../../../controllers/channels/dto';
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { appsignal } = require('../../../../appsignal'); // Update to the location used in the previous step
 
 export class AddMemberToChannel implements Command {
   constructor(private membername: string, private channelId: string, private authorizationBag: AuthorizationBag) {}
 
   usersService: UsersServiceInterface = ServiceFactory.getUserService();
 
-  get newUser(): User {
-    if (!this.membername) return null;
-    if (this.membername.includes('@')) return new User({ email: this.membername });
-    return new User({ username: this.membername });
-  }
-
   async execute(transactionManager: EntityManager): Promise<MemberResponse> {
+    const tracer = appsignal.tracer();
+    const span = tracer.createSpan(undefined, tracer.currentSpan());
+    span.setCategory('services/impl');
+    span.setName('AddMemberToChannel');
+
+    let child = span.child();
+    child.setCategory('get_channel_via_findOne');
     const channel = await transactionManager.findOne(Channel, {
-      relations: ['owner', 'adminGroup', 'members', 'groups'],
-      where: {
-        id: this.channelId,
+      relations: {
+        owner: true,
+        adminGroup: true,
       },
+      where: { id: this.channelId },
     });
-    if (!channel) throw new NotFoundError('Channel does not exist');
+    child.close();
 
-    // you need to be channel admin
-    if (!(await channel.isAdmin(this.authorizationBag)))
+    if (!channel) {
+      span.close();
+      throw new NotFoundError('Channel does not exist');
+    }
+
+    child = span.child();
+    child.setCategory('getCurrentUserGroups');
+    // Get current user groups, to be used next in SQL to compare with channel groups
+    const userGroups = await CernAuthorizationService.getCurrentUserGroups(this.authorizationBag.userName);
+    child.close();
+
+    child = span.child();
+    child.setCategory('isAdmin');
+    const hasAdminAccess = await channel.hasAdminAccess(transactionManager, this.authorizationBag, userGroups);
+    console.debug('hasAdminAccess', hasAdminAccess);
+    if (!hasAdminAccess) {
+      child.close();
+      span.close();
       throw new ForbiddenError("You don't have the rights to manage this channel.");
-    //if added user is already a direct member or a group member
-    if (await channel.isMember(this.newUser)) {
-      throw new ForbiddenError(`User ${this.membername} is already a member or in a member group.`);
     }
+    child.close();
 
+    child = span.child();
+    child.setCategory('GetOrCreateUserFromUsernameOrEmail');
     const userToAdd = await this.usersService.GetOrCreateUserFromUsernameOrEmail(this.membername.toLowerCase().trim());
+    child.close();
+
+    child = span.child();
+    child.setCategory('isMemberViaUser');
+    const isMemberViaUser = await channel.isMemberViaUser(transactionManager, userToAdd.id);
+    console.debug('userToAdd.id', userToAdd.id);
+    console.debug('isMemberViaUser', isMemberViaUser);
+    if (isMemberViaUser) {
+      child.close();
+      span.close();
+      throw new ForbiddenError(
+        `User ${this.membername} / ${userToAdd.email} is already a member or in a member group.`,
+      );
+    }
+    child.close();
 
-    //if resolved new user is already a direct member or a group member
-    if (await channel.isMember(userToAdd)) {
+    child = span.child();
+    child.setCategory('getMemberUserGroups');
+    let memberGroups = [];
+    if (userToAdd.username) {
+      // Get new member groups, to be used next in SQL to compare with channel groups
+      memberGroups = await CernAuthorizationService.getCurrentUserGroups(userToAdd.username);
+    }
+    child.close();
+
+    child = span.child();
+    child.setCategory('isMemberViaGroup');
+    const isMemberViaGroup = await channel.isMemberViaGroup(transactionManager, memberGroups);
+    console.debug('isMemberViaGroup', isMemberViaGroup);
+    if (isMemberViaGroup) {
+      child.close();
+      span.close();
       throw new ForbiddenError(
         `User ${this.membername} / ${userToAdd.email} is already a member or in a member group.`,
       );
     }
+    child.close();
 
-    channel.addMember(userToAdd);
-    //const ret = await transactionManager.save(channel);
     // optimized save performance
-    await transactionManager.createQueryBuilder().relation(Channel, 'members').of(channel).add(userToAdd);
+    child = span.child();
+    child.setCategory('save_via_query_builder');
+    await channel.addUser(transactionManager, userToAdd);
+    child.close();
 
     await AuditChannels.setValue(channel.id, {
       event: 'AddMember',
@@ -59,6 +108,8 @@ export class AddMemberToChannel implements Command {
       membername: this.membername,
     });
 
+    span.close();
+
     return new MemberResponse(userToAdd);
   }
 }
diff --git a/src/services/impl/channels/create-channel.ts b/src/services/impl/channels/create-channel.ts
index 16d492b5da5f7ec78325fe195a34293eaa4fda1a..5d6d2a03336a0d5945ccaaedcca956b77597fe49 100644
--- a/src/services/impl/channels/create-channel.ts
+++ b/src/services/impl/channels/create-channel.ts
@@ -16,7 +16,7 @@ export class CreateChannel implements Command {
 
   async execute(transactionManager: EntityManager): Promise<ChannelResponse> {
     // Get owner (user who have created the channel) to set as user by default
-    const user = await transactionManager.findOne(User, {
+    const user = await transactionManager.findOneBy(User, {
       id: this.authorizationBag.userId,
     });
 
@@ -36,14 +36,14 @@ export class CreateChannel implements Command {
     }
 
     if (
-      await transactionManager.findOne(Channel, {
+      await transactionManager.findOneBy(Channel, {
         name: channel.name,
       })
     ) {
       throw new BadRequestError('Channel name already exists');
     }
     if (
-      await transactionManager.findOne(Channel, {
+      await transactionManager.findOneBy(Channel, {
         slug: channel.slug,
       })
     ) {
@@ -51,7 +51,7 @@ export class CreateChannel implements Command {
     }
 
     if (this.channel.adminGroup) {
-      const adminGroup = await transactionManager.findOne(Group, { groupIdentifier: this.channel.adminGroup });
+      const adminGroup = await transactionManager.findOneBy(Group, { groupIdentifier: this.channel.adminGroup });
       if (!adminGroup) throw new NotFoundError('Admin group does not exist');
       if (adminGroup.groupIdentifier != this.channel.adminGroup)
         throw new Error('Admin group name does not match provided id.');
diff --git a/src/services/impl/channels/find-all-channels.ts b/src/services/impl/channels/find-all-channels.ts
index 974912379a9481e781f423041ca0deff74f88289..c6b043db1338eda7a5ef49cd226e53e675068b29 100644
--- a/src/services/impl/channels/find-all-channels.ts
+++ b/src/services/impl/channels/find-all-channels.ts
@@ -1,12 +1,14 @@
 import { Command } from '../command';
-import { Brackets, EntityManager } from 'typeorm';
+import { EntityManager } from 'typeorm';
 import { Channel } from '../../../models/channel';
 import { AuthorizationBag } from '../../../models/authorization-bag';
 import { CernAuthorizationService } from '../../../models/cern-authorization-service';
-import { UserChannelCollection, UserChannelCollectionType } from '../../../models/user-channel-collection';
 import { UnauthorizedError } from 'routing-controllers';
 import { Category } from '../../../models/category';
 import { ChannelsListResponse, GetChannelResponse, ChannelsQuery } from '../../../controllers/channels/dto';
+import { SubmissionByForm } from '../../../models/channel-enums';
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { appsignal } = require('../../../../appsignal'); // Update to the location used in the previous step
 
 export class FindAllChannels implements Command {
   constructor(private query: ChannelsQuery, private authorizationBag: AuthorizationBag) {}
@@ -15,170 +17,61 @@ export class FindAllChannels implements Command {
     if (!this.authorizationBag)
       throw new UnauthorizedError('Access unauthorized. Please authenticate or use /public/channels instead.');
 
-    console.time('get-all-channels:total:' + this.authorizationBag.userName);
+    const tracer = appsignal.tracer();
+    const span = tracer.createSpan(undefined, tracer.currentSpan());
+    span.setCategory('services/impl');
+    span.setName('FindAllChannels');
 
-    console.time('get-all-channels:grappa:' + this.authorizationBag.userName);
+    let child = span.child();
+    child.setCategory('getCurrentUserGroups');
     // Get current user groups, to be used next in SQL to compare with channel groups
     const userGroups = await CernAuthorizationService.getCurrentUserGroups(this.authorizationBag.userName);
-    console.timeEnd('get-all-channels:grappa:' + this.authorizationBag.userName);
+    child.close();
 
-    console.time('get-all-channels:build-query:' + this.authorizationBag.userName);
     // Initial query with left joins but no select to avoid slow typeorm aggregation
     const qb = await transactionManager
       .getRepository(Channel)
       .createQueryBuilder('channel')
-      .leftJoin('channel.members', 'members')
-      .leftJoin('channel.groups', 'groups')
-      .leftJoin('channel.unsubscribed', 'unsubscribed')
       .leftJoin('channel.owner', 'owner')
-      .leftJoin('channel.adminGroup', 'adminGroup')
-      .leftJoin('channel.category', 'category')
-      .leftJoin('channel.tags', 'tags');
+      .leftJoin('channel.adminGroup', 'adminGroup');
 
-    if (this.query.searchText) {
-      qb.andWhere(
-        new Brackets(qb => {
-          qb.where(
-            'channel.description Like :searchText or channel.name Like :searchText or owner.username Like :searchText',
-            { searchText: `%${this.query.searchText}%` },
-          );
-        }),
-      );
-    }
+    if (this.query.searchText) qb.andWhere(Channel.qbSearchText(this.query.searchText));
 
     if (this.query.category) {
-      const searchParameterCategory = new Category({});
-      searchParameterCategory.id = this.query.category;
-      const descendants = await transactionManager.getTreeRepository(Category).findDescendants(searchParameterCategory);
-      const descendantIds = descendants.map(cat => cat.id);
-      qb.andWhere(sqb => {
-        const subQuery = sqb
-          .subQuery()
-          .select('channel.id')
-          .from(Channel, 'channel')
-          .leftJoin('channel.category', 'category')
-          .where('category.id IN (:...categories)', { categories: descendantIds })
-          .getQuery();
-        return 'channel.id IN ' + subQuery;
-      });
+      const categoryIds = await Category.getDescendantIds(transactionManager, this.query.category);
+      qb.leftJoin('channel.category', 'category');
+      qb.andWhere(Channel.qbMatchCategory(categoryIds));
     }
 
     if (this.query.tags) {
-      console.debug(this.query.tags);
-      // filter must include all tags
-      qb.andWhere(sqb => {
-        const subQuery = sqb
-          .subQuery()
-          .select('channel.id')
-          .from(Channel, 'channel')
-          .leftJoin('channel.tags', 'tags')
-          .where('tags.id IN (:...tags)', { tags: this.query.tags })
-          .groupBy('channel.id')
-          .having('COUNT(*) = :count', { count: this.query.tags.length })
-          .getQuery();
-        return 'channel.id IN ' + subQuery;
-      });
+      qb.leftJoin('channel.tags', 'tags');
+      qb.andWhere(Channel.qbMatchTags(this.query.tags));
     }
 
-    // Filter by member, group, admin, etc.
     qb.andWhere(
-      new Brackets(channelsQB => {
-        if (this.query.ownerFilter) {
-          channelsQB.orWhere('owner.id = :userId', {
-            userId: this.authorizationBag.userId,
-          });
-          if (userGroups && userGroups.length > 0)
-            channelsQB.orWhere('adminGroup.id IN (:...userGroups)', {
-              userGroups: userGroups.map(g => g.groupId),
-            });
-
-          return;
-        }
-        if (this.query.subscribedFilter) {
-          channelsQB.orWhere(':userId IN (members.id)', {
-            userId: this.authorizationBag.userId,
-          });
-          if (userGroups && userGroups.length > 0)
-            channelsQB.orWhere('groups.id IN (:...userGroups)', {
-              userGroups: userGroups.map(g => g.groupId),
-            });
-
-          return;
-        }
-
-        if (!this.authorizationBag.isSupporter) {
-          channelsQB
-            .orWhere("channel.visibility = 'PUBLIC'")
-            .orWhere("channel.visibility = 'INTERNAL'")
-            .orWhere('owner.id = :userId', {
-              userId: this.authorizationBag.userId,
-            });
-
-          if (userGroups && userGroups.length > 0) {
-            channelsQB.orWhere('adminGroup.id IN (:...userGroups)', {
-              userGroups: userGroups.map(g => g.groupId),
-            });
-          }
-          channelsQB.orWhere(':userId IN (members.id)', {
-            userId: this.authorizationBag.userId,
-          });
-          if (userGroups && userGroups.length > 0) {
-            channelsQB.orWhere('groups.id IN (:...userGroups)', {
-              userGroups: userGroups.map(g => g.groupId),
-            });
-          }
-        }
-      }),
+      Channel.filterChannels(
+        qb,
+        this.authorizationBag.userId,
+        userGroups,
+        this.authorizationBag.isSupporter,
+        this.query.ownerFilter,
+        this.query.subscribedFilter,
+        this.query.favoritesFilter,
+      ),
     );
 
-    if (this.query.subscribedFilter) {
-      // exclude all unsubscribed channels
-      qb.andWhere(sqb => {
-        const subQuery = sqb
-          .subQuery()
-          .select('channel.id')
-          .from(Channel, 'channel')
-          .leftJoin('channel.unsubscribed', 'unsubscribed')
-          .where(':userId IN (unsubscribed.id)', {
-            userId: this.authorizationBag.userId,
-          })
-          .getQuery();
-        return 'channel.id NOT IN ' + subQuery;
-      });
-    }
-
-    if (this.query.favoritesFilter) {
-      qb.andWhere(sqb => {
-        const subQuery = sqb
-          .subQuery()
-          .select('collection.channelId')
-          .from(UserChannelCollection, 'collection')
-          .where('collection.type = :type', {
-            type: UserChannelCollectionType.FAVORITE,
-          })
-          .andWhere('collection.userId = :userId', {
-            userId: this.authorizationBag.userId,
-          })
-          .getQuery();
-        return 'channel.id IN ' + subQuery;
-      });
-    }
-    console.timeEnd('get-all-channels:build-query:' + this.authorizationBag.userName);
-
-    console.time('get-all-channels:count:' + this.authorizationBag.userName);
-
     // query count
+    child = span.child();
+    child.setCategory('count');
     const count = await qb.clone().getCount();
-    console.timeEnd('get-all-channels:count:' + this.authorizationBag.userName);
+    child.close();
 
     // Nothing found, no match, we return an empty list
     if (count === 0) {
-      console.timeEnd('get-all-channels:total:' + this.authorizationBag.userName);
+      span.close();
       return new ChannelsListResponse();
     }
 
-    console.time('get-all-channels:query:' + this.authorizationBag.userName);
-
     // query channel_ids and filter, skip and take only what we need for this page
     qb.select(['channel.id', 'channel.name']) // select channel_id
       .orderBy('channel.name', 'ASC')
@@ -186,16 +79,17 @@ export class FindAllChannels implements Command {
       .limit(this.query.take || 10)
       .offset(this.query.skip || 0);
 
+    child = span.child();
+    child.setCategory('getRawMany');
     const channel_ids = await qb.getRawMany();
-    console.timeEnd('get-all-channels:query:' + this.authorizationBag.userName);
+    child.close();
 
-    console.time('get-all-channels:query-page:' + this.authorizationBag.userName);
     // select channels and relations for this list of channel_ids only
+    child = span.child();
+    child.setCategory('getSlice');
     const channels = await transactionManager
       .getRepository(Channel)
       .createQueryBuilder('channel')
-      .leftJoinAndSelect('channel.members', 'members')
-      .leftJoinAndSelect('channel.groups', 'groups')
       .leftJoinAndSelect('channel.unsubscribed', 'unsubscribed')
       .leftJoinAndSelect('channel.owner', 'owner')
       .leftJoinAndSelect('channel.adminGroup', 'adminGroup')
@@ -207,59 +101,23 @@ export class FindAllChannels implements Command {
       .loadRelationCountAndMap('channel.notificationCount', 'channel.notifications')
       .orderBy('channel.name', 'ASC')
       .getMany();
+    child.close();
 
-    console.timeEnd('get-all-channels:query-page:' + this.authorizationBag.userName);
-
-    console.time('get-all-channels:build-return:' + this.authorizationBag.userName);
+    child = span.child();
+    child.setCategory('GetChannelResponse');
     const returnChannels: GetChannelResponse[] = await Promise.all(
       channels.map(async channel => {
-        // Clearing privacy items, used in relation above for permissions queries
-        // But should not be returned to the frontend for display
-        // at least don't return channel.owner, channel.adminGroup, channel.members, channel.groups
-
-        return new GetChannelResponse(
-          channel,
-          await channel.isSubscribedWithCache(this.authorizationBag.user, userGroups),
-          await channel.isAdminWithCache(this.authorizationBag, userGroups),
-          await channel.canSendByFormWithCache(this.authorizationBag, userGroups),
-        );
-
-        /*
-        let filteredChannel = {
-          id: channel.id,
-          slug: channel.slug,
-          name: channel.name,
-          description: channel.description,
-          //unsubscribed: channel.unsubscribed, // should we return this in the main channel list?
-          visibility: channel.visibility,
-          subscriptionPolicy: channel.subscriptionPolicy,
-          archive: channel.archive,
-          // new version with memoizee, to fix
-          subscribed: await channel.isSubscribedWithCache(this.authorizationBag.user, userGroups),
-          manage: await channel.isAdminWithCache(this.authorizationBag, userGroups),
-          send: await channel.canSendByFormWithCache(this.authorizationBag, userGroups),
-          category: channel.category,
-          tags: channel.tags,
-          channelFlags: channel.channelFlags,
-          relationships: {
-            notifications: {
-              count: channel.notificationCount,
-            },
-            members: {
-              count: channel.members?.length || 0,
-            },
-            groups: {
-              count: channel.groups?.length || 0,
-            },
-          },
-          sendPrivate: channel.sendPrivate,
-        };
-        return ChannelResponse(filteredChannel);*/
+        const hasMemberAccess = await channel.hasMemberAccess(transactionManager, this.authorizationBag, userGroups);
+        const hasAdminAccess = await channel.hasAdminAccess(transactionManager, this.authorizationBag, userGroups);
+        const canSendByForm =
+          (channel.submissionByForm.includes(SubmissionByForm.members) && hasMemberAccess) ||
+          (channel.submissionByForm.includes(SubmissionByForm.administrators) && hasAdminAccess);
+        return new GetChannelResponse(channel, hasMemberAccess, hasAdminAccess, canSendByForm);
       }),
     );
-    console.timeEnd('get-all-channels:build-return:' + this.authorizationBag.userName);
+    child.close();
 
-    console.timeEnd('get-all-channels:total:' + this.authorizationBag.userName);
+    span.close();
     return new ChannelsListResponse(returnChannels, count);
   }
 }
diff --git a/src/services/impl/channels/find-channel-by-id.ts b/src/services/impl/channels/find-channel-by-id.ts
index 1af811d095bdfc9bdecfc8c361efba352586b60f..4a21c46328e4543575eef1b7d02806c3bf3d40d5 100644
--- a/src/services/impl/channels/find-channel-by-id.ts
+++ b/src/services/impl/channels/find-channel-by-id.ts
@@ -3,7 +3,7 @@ import { EntityManager } from 'typeorm';
 import { Channel } from '../../../models/channel';
 import { ForbiddenError, NotFoundError } from 'routing-controllers';
 import { AuthorizationBag } from '../../../models/authorization-bag';
-import { ChannelResponse, GetChannelResponse } from '../../../controllers/channels/dto';
+import { GetChannelResponse } from '../../../controllers/channels/dto';
 import { CernAuthorizationService } from '../../../models/cern-authorization-service';
 
 export class FindChannelById implements Command {
@@ -27,9 +27,9 @@ export class FindChannelById implements Command {
 
     return new GetChannelResponse(
       channel,
-      await channel.isSubscribedWithCache(this.authorizationBag.user, userGroups),
-      await channel.isAdminWithCache(this.authorizationBag, userGroups),
-      await channel.canSendByFormWithCache(this.authorizationBag, userGroups),
+      await channel.hasMemberAccess(transactionManager, this.authorizationBag, userGroups),
+      await channel.hasAdminAccess(transactionManager, this.authorizationBag, userGroups),
+      await channel.canSendByForm(transactionManager, this.authorizationBag, userGroups),
     );
   }
 }
diff --git a/src/services/impl/channels/get-channel-groups.ts b/src/services/impl/channels/get-channel-groups.ts
index 0b4a62b94094ac85a3312fc0eeeb14048e6d3160..e0a9675eb81a02af0b7d9655a5a6a5206f9c1503 100644
--- a/src/services/impl/channels/get-channel-groups.ts
+++ b/src/services/impl/channels/get-channel-groups.ts
@@ -22,7 +22,8 @@ export class GetChannelGroups implements Command {
     if (!(await channel.isAdmin(this.authorizationBag)))
       throw new ForbiddenError("You don't have the rights to manage this channel.");
 
-    const returnGroups = channel.groups.filter(g => g.groupIdentifier.includes(this.query.searchText || ''));
+    const channelGroups = await channel.groups;
+    const returnGroups = channelGroups.filter(g => g.groupIdentifier.includes(this.query.searchText || ''));
 
     return {
       totalNumberOfGroups: returnGroups.length,
diff --git a/src/services/impl/channels/get-channel-members.ts b/src/services/impl/channels/get-channel-members.ts
index 06bd14390853e936744a7dfde6acb090b20e8dcc..6c8d9fe95a49b65d1a08718b601a61194d7ee87e 100644
--- a/src/services/impl/channels/get-channel-members.ts
+++ b/src/services/impl/channels/get-channel-members.ts
@@ -22,7 +22,8 @@ export class GetChannelMembers implements Command {
     if (!(await channel.isAdmin(this.authorizationBag)))
       throw new ForbiddenError("You don't have the rights to manage this channel.");
 
-    const returnMembers = channel.members.filter(
+    const channelMembers = await channel.members;
+    const returnMembers = channelMembers.filter(
       m =>
         (m.username && m.username.includes(this.query.searchText || '')) ||
         m.email.includes(this.query.searchText || ''),
diff --git a/src/services/impl/channels/remove-group-from-channel.ts b/src/services/impl/channels/remove-group-from-channel.ts
index 4f884033548a113c2c2adf206b76f283afa86d0d..9a6143b7fc47e2196408a7abcddcfaca4e90d74a 100644
--- a/src/services/impl/channels/remove-group-from-channel.ts
+++ b/src/services/impl/channels/remove-group-from-channel.ts
@@ -35,7 +35,7 @@ export class RemoveGroupFromChannel implements Command {
 
     if (!group) throw new NotFoundError('Group not found');
 
-    channel.removeGroup(group);
+    await channel.removeGroup(transactionManager, group);
 
     const updatedChannel = await transactionManager.save(channel);
     if (!updatedChannel) throw new Error('Was not able to update the channel.');
diff --git a/src/services/impl/channels/remove-user-from-channel.ts b/src/services/impl/channels/remove-user-from-channel.ts
index c81f46887d268bf35702fc36d4234458a8903ed5..8bff66d53fd80a53f280ba8b4852e2e7ae9f3813 100644
--- a/src/services/impl/channels/remove-user-from-channel.ts
+++ b/src/services/impl/channels/remove-user-from-channel.ts
@@ -10,13 +10,10 @@ export class RemoveUserFromChannel implements Command {
   constructor(private memberId: string, private channelId: string, private authorizationBag: AuthorizationBag) {}
 
   async execute(transactionManager: EntityManager): Promise<void> {
-    const channel = await transactionManager.findOne(
-      Channel,
-      { id: this.channelId },
-      {
-        relations: ['unsubscribed', 'members', 'groups', 'owner', 'adminGroup'],
-      },
-    );
+    const channel = await transactionManager.findOne(Channel, {
+      where: { id: this.channelId },
+      relations: ['unsubscribed', 'members', 'groups', 'owner', 'adminGroup'],
+    });
 
     if (!channel) throw new NotFoundError('Channel does not exist');
 
@@ -25,7 +22,7 @@ export class RemoveUserFromChannel implements Command {
     if (this.memberId !== this.authorizationBag.userId && !(await channel.isAdmin(this.authorizationBag)))
       throw new ForbiddenError('Access to Channel not Authorized !');
 
-    const user = await transactionManager.findOne(User, { id: this.memberId });
+    const user = await transactionManager.findOneBy(User, { id: this.memberId });
     if (!user) throw new NotFoundError('User not found');
 
     await transactionManager.createQueryBuilder().relation(Channel, 'members').of(channel).remove(user);
diff --git a/src/services/impl/channels/subscribe-to-channel.ts b/src/services/impl/channels/subscribe-to-channel.ts
index 84da716007bd5dea44ae06dbab09782adcf3c365..980b1d70e49b48e8dd2cd50a87d32a81aec70902 100644
--- a/src/services/impl/channels/subscribe-to-channel.ts
+++ b/src/services/impl/channels/subscribe-to-channel.ts
@@ -10,13 +10,10 @@ export class SubscribeToChannel implements Command {
   constructor(private memberId: string, private channelId: string, private authorizationBag: AuthorizationBag) {}
 
   async execute(transactionManager: EntityManager): Promise<void> {
-    const channel = await transactionManager.findOne(
-      Channel,
-      { id: this.channelId },
-      {
-        relations: ['unsubscribed', 'members', 'groups', 'owner', 'adminGroup'],
-      },
-    );
+    const channel = await transactionManager.findOne(Channel, {
+      where: { id: this.channelId },
+      relations: ['unsubscribed', 'members', 'groups', 'owner', 'adminGroup'],
+    });
 
     if (!channel) throw new NotFoundError('Channel does not exist');
 
@@ -27,8 +24,8 @@ export class SubscribeToChannel implements Command {
     else if (!(await channel.hasAccess(this.authorizationBag)))
       throw new ForbiddenError('Access to Channel not Authorized !');
 
-    const user = await transactionManager.findOne(User, { id: this.memberId });
-    channel.addMember(user);
+    const user = await transactionManager.findOneBy(User, { id: this.memberId });
+    await channel.addUser(transactionManager, user);
     const updatedChannel = await transactionManager.save(channel);
 
     await AuditChannels.setValue(updatedChannel.id, {
diff --git a/src/services/impl/channels/unsubscribe-from-channel.ts b/src/services/impl/channels/unsubscribe-from-channel.ts
index 8e1f23468f2fa5e27a3ea6663fde7c78a0354a30..331172b2bbbdf6d62d1839dec38415b3139002ed 100644
--- a/src/services/impl/channels/unsubscribe-from-channel.ts
+++ b/src/services/impl/channels/unsubscribe-from-channel.ts
@@ -11,13 +11,10 @@ export class UnsubscribeFromChannel implements Command {
   constructor(private memberId: string, private channelId: string, private authorizationBag: AuthorizationBag) {}
 
   async execute(transactionManager: EntityManager): Promise<void> {
-    const channel = await transactionManager.findOne(
-      Channel,
-      { id: this.channelId },
-      {
-        relations: ['unsubscribed', 'members', 'groups', 'owner', 'adminGroup'],
-      },
-    );
+    const channel = await transactionManager.findOne(Channel, {
+      where: { id: this.channelId },
+      relations: ['unsubscribed', 'members', 'groups', 'owner', 'adminGroup'],
+    });
 
     if (!channel) throw new NotFoundError('Channel does not exist');
 
@@ -31,7 +28,7 @@ export class UnsubscribeFromChannel implements Command {
         'This channel is mandatory, unsubscribe is not possible. You can however mute it if needed.',
       );
 
-    const user = await transactionManager.findOne(User, { id: this.memberId });
+    const user = await transactionManager.findOneBy(User, { id: this.memberId });
     if (!user) throw new NotFoundError('User not found');
 
     channel.unsubscribeMember(user);
diff --git a/src/services/impl/channels/update-channel-admin-group.ts b/src/services/impl/channels/update-channel-admin-group.ts
index fe2ed4f0184a05d0727a704a98ddb73f13415dab..98659ecaecdc9fad2d19d3817596ae293a034819 100644
--- a/src/services/impl/channels/update-channel-admin-group.ts
+++ b/src/services/impl/channels/update-channel-admin-group.ts
@@ -30,7 +30,7 @@ export class UpdateChannelAdminGroup implements Command {
 
     const newGroup = new Group({ groupIdentifier: this.newAdminGroup.newAdminGroup });
 
-    let groupToAdd = await transactionManager.findOne(Group, {
+    let groupToAdd = await transactionManager.findOneBy(Group, {
       groupIdentifier: this.newAdminGroup.newAdminGroup,
     });
     if (!groupToAdd && (await newGroup.exists())) {
diff --git a/src/services/impl/channels/update-channel.ts b/src/services/impl/channels/update-channel.ts
index a2b65c6347f5a40984f85d3c322a8715862540c2..fc4dc3d156b4d9bc432a3800d2b1bc1e38b613d6 100644
--- a/src/services/impl/channels/update-channel.ts
+++ b/src/services/impl/channels/update-channel.ts
@@ -35,16 +35,13 @@ export class UpdateChannel implements Command {
     }
 
     if (
-      await transactionManager.findOne(
-        Channel,
-        {
+      await transactionManager.findOne(Channel, {
+        where: {
           id: Not(channel.id),
           name: this.channel.name,
         },
-        {
-          withDeleted: true,
-        },
-      )
+        withDeleted: true,
+      })
     ) {
       throw new BadRequestError('Channel name already exists');
     }
diff --git a/src/services/impl/devices/update-user-device.ts b/src/services/impl/devices/update-user-device.ts
index 631716b23ca18d7d1de53ed7713c53a3a27403d2..9e4c494cf699123116a00eba6a01b9649bd2ce97 100644
--- a/src/services/impl/devices/update-user-device.ts
+++ b/src/services/impl/devices/update-user-device.ts
@@ -27,7 +27,7 @@ export class UpdateUserDevice implements Command {
 
     if (result.affected !== 1) throw new ForbiddenError('The device does not exist or you are not the owner');
 
-    const response = await transactionManager.findOne(Device, { id: this.deviceId });
+    const response = await transactionManager.findOneBy(Device, { id: this.deviceId });
 
     return new DeviceResponse(response);
   }
diff --git a/src/services/impl/groups/get-or-create-group.ts b/src/services/impl/groups/get-or-create-group.ts
index c2edd6f1c80da53e91cd5b3e0db1b4f1dfc7efec..68848ab298580e042257bae8392f1eaf1d5f3fd8 100644
--- a/src/services/impl/groups/get-or-create-group.ts
+++ b/src/services/impl/groups/get-or-create-group.ts
@@ -7,7 +7,7 @@ export class GetOrCreateGroup implements Command {
   constructor(private groupIdentifier: string) {}
 
   async execute(transactionManager: EntityManager): Promise<Group> {
-    let group = await transactionManager.findOne(Group, {
+    let group = await transactionManager.findOneBy(Group, {
       groupIdentifier: this.groupIdentifier,
     });
     if (group) return group;
diff --git a/src/services/impl/mutes/create-unsubscribe-mute.ts b/src/services/impl/mutes/create-unsubscribe-mute.ts
index 1fc217c2858e9a37ae61fe3f93a4df15e7d6720a..58abb133ee9d3ee6687c79c01016a818ca54264a 100644
--- a/src/services/impl/mutes/create-unsubscribe-mute.ts
+++ b/src/services/impl/mutes/create-unsubscribe-mute.ts
@@ -1,5 +1,5 @@
 import { Command } from '../command';
-import { EntityManager } from 'typeorm';
+import { EntityManager, IsNull } from 'typeorm';
 import { decodeEmail, Mute } from '../../../models/mute';
 import { User } from '../../../models/user';
 import { BadRequestError } from 'routing-controllers';
@@ -7,20 +7,14 @@ import { AuthService } from '../../../services/auth-service';
 import { ServiceFactory } from '../../../services/services-factory';
 
 export class CreateUnSubscribeMute implements Command {
-  constructor(
-    private mute: Mute,
-    private blob: string,
-    private email: string,
-  ) {}
+  constructor(private mute: Mute, private blob: string, private email: string) {}
 
   authService: AuthService = ServiceFactory.getAuthenticationService();
 
   async execute(transactionManager: EntityManager) {
     try {
-      if (!this.email || !this.blob)
-        throw new Error('Error: missing email or blob in request');
-      if (decodeEmail(this.blob) !== this.email)
-        throw new Error('Error: tempered unsubscribe request');
+      if (!this.email || !this.blob) throw new Error('Error: missing email or blob in request');
+      if (decodeEmail(this.blob) !== this.email) throw new Error('Error: tempered unsubscribe request');
     } catch (e) {
       console.error(e);
       // Return a generic error, no need to explain why it fails
@@ -28,7 +22,7 @@ export class CreateUnSubscribeMute implements Command {
     }
 
     // Retrieve existing user if any for this email
-    let user = await transactionManager.findOne(User, { email: this.email });
+    let user = await transactionManager.findOneBy(User, { email: this.email });
 
     // Check if a global permanent unsubscribe record already exists for this user
     if (
@@ -37,7 +31,7 @@ export class CreateUnSubscribeMute implements Command {
         relations: ['user', 'target'],
         where: {
           user: user,
-          target: null,
+          target: IsNull(),
           type: 'PERMANENT',
         },
       }))
@@ -45,7 +39,7 @@ export class CreateUnSubscribeMute implements Command {
       throw new BadRequestError('This email is already unsubscribed');
     }
 
-    let mute = new Mute({
+    const mute = new Mute({
       type: this.mute.type,
       target: null,
       user: user,
diff --git a/src/services/impl/mutes/create-user-mute.ts b/src/services/impl/mutes/create-user-mute.ts
index 2f91a90edd0e49c0afad8602c07265c662226595..318e062a6aaf95d32715d99f97fa301b61fb12c3 100644
--- a/src/services/impl/mutes/create-user-mute.ts
+++ b/src/services/impl/mutes/create-user-mute.ts
@@ -32,7 +32,7 @@ export class CreateUserMute implements Command {
     );
     if (savedMute) {
       return await transactionManager.findOne(Mute, {
-        relations: ["target"],
+        relations: ['target'],
         where: {
           id: savedMute.id,
         },
diff --git a/src/services/impl/mutes/get-user-mutes.ts b/src/services/impl/mutes/get-user-mutes.ts
index 5147a21b0464cce07bd7b11ed470e4d9d4103b66..3b3b064499d598f8c29dccb11de4a56bd6dbb4c5 100644
--- a/src/services/impl/mutes/get-user-mutes.ts
+++ b/src/services/impl/mutes/get-user-mutes.ts
@@ -1,20 +1,19 @@
-import { Command } from "../command";
-import { EntityManager } from "typeorm";
-import { Mute } from "../../../models/mute";
-import { AuthorizationBag } from "../../../models/authorization-bag";
+import { Command } from '../command';
+import { EntityManager, IsNull } from 'typeorm';
+import { Mute } from '../../../models/mute';
+import { AuthorizationBag } from '../../../models/authorization-bag';
 
 export class GetUserMutes implements Command {
-  constructor(
-    private channelId: string,
-    private authorizationBag: AuthorizationBag
-    ) {}
+  constructor(private channelId: string, private authorizationBag: AuthorizationBag) {}
 
   private async fetchGlobalMutes(transactionManager: EntityManager) {
     return await transactionManager.find(Mute, {
-      relations: ["target"],
+      relations: ['target'],
       where: {
-        user: this.authorizationBag.userId,
-        target: null
+        user: {
+          id: this.authorizationBag.userId,
+        },
+        target: IsNull(),
       },
     });
   }
@@ -22,10 +21,10 @@ export class GetUserMutes implements Command {
   private async fetchChannelMutes(transactionManager: EntityManager) {
     return await transactionManager
       .getRepository(Mute)
-      .createQueryBuilder("mute")
-      .leftJoin("mute.user", "user")
-      .leftJoinAndSelect("mute.target", "target")
-      .where("user.id = :userId AND target.id = :channelId", {
+      .createQueryBuilder('mute')
+      .leftJoin('mute.user', 'user')
+      .leftJoinAndSelect('mute.target', 'target')
+      .where('user.id = :userId AND target.id = :channelId', {
         userId: this.authorizationBag.userId,
         channelId: this.channelId,
       })
@@ -35,10 +34,10 @@ export class GetUserMutes implements Command {
   private async fetchAllChannelsMutes(transactionManager: EntityManager) {
     return await transactionManager
       .getRepository(Mute)
-      .createQueryBuilder("mute")
-      .leftJoin("mute.user", "user")
-      .leftJoinAndSelect("mute.target", "target")
-      .where("user.id = :userId AND target.id IS NOT NULL", {
+      .createQueryBuilder('mute')
+      .leftJoin('mute.user', 'user')
+      .leftJoinAndSelect('mute.target', 'target')
+      .where('user.id = :userId AND target.id IS NOT NULL', {
         userId: this.authorizationBag.userId,
       })
       .getMany();
@@ -46,7 +45,7 @@ export class GetUserMutes implements Command {
 
   async execute(transactionManager: EntityManager) {
     const result = {
-      globalMutes: await this.fetchGlobalMutes(transactionManager)
+      globalMutes: await this.fetchGlobalMutes(transactionManager),
     };
 
     if (this.channelId) {
diff --git a/src/services/impl/notifications/find-all-notifications.ts b/src/services/impl/notifications/find-all-notifications.ts
index 336a3d51004ea0fcf5e125c084bd7f6b12c257dc..1054fc8832b42fb1f1e3d28449d3c275aa0201f0 100644
--- a/src/services/impl/notifications/find-all-notifications.ts
+++ b/src/services/impl/notifications/find-all-notifications.ts
@@ -33,7 +33,7 @@ export class FindAllNotifications implements Command {
       throw new ForbiddenError('Access to Channel not Authorized!');
 
     const isAdmin = await channel.isAdmin(this.authorizationBag);
-    const userCanSend = await channel.canSendByForm(this.authorizationBag);
+    const userCanSend = await channel.canSendByForm(transactionManager, this.authorizationBag, userGroups);
 
     if (this.authorizationBag) console.timeEnd('get-all-notifications:channel:' + this.authorizationBag.userName);
 
diff --git a/src/services/impl/notifications/send-notification.ts b/src/services/impl/notifications/send-notification.ts
index b6a74b5ca12849d18b86f6d47972e9a3bc2e660b..a0d6d2548d8b36fe99a4e90dee8921a82220c0b8 100644
--- a/src/services/impl/notifications/send-notification.ts
+++ b/src/services/impl/notifications/send-notification.ts
@@ -17,8 +17,11 @@ import { GroupsServiceInterface } from '../../groups-service';
 import * as memoize from 'memoizee';
 import { AuditNotifications } from '../../../log/auditing';
 import { GetNotificationResponse, SendNotificationRequest } from '../../../controllers/notifications/dto';
-import { auth } from 'firebase-admin';
-import { SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG } from 'constants';
+
+import { CernAuthorizationService } from '../../../models/cern-authorization-service';
+import { SubmissionByForm } from '../../../models/channel-enums';
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { appsignal } = require('../../../../appsignal'); // Update to the location used in the previous step
 
 export class SendNotification implements Command {
   private usersService: UsersServiceInterface = ServiceFactory.getUserService();
@@ -26,32 +29,82 @@ export class SendNotification implements Command {
 
   constructor(private notification: SendNotificationRequest, private authorizationBag: AuthorizationBag) {}
 
+  async hasAccess(transactionManager: EntityManager, channel: Channel, userGroups: any[]): Promise<boolean> {
+    if (!this.authorizationBag) {
+      // Call from the /unauthenticated
+      // restricted by direct access in openshift
+      return true;
+    }
+
+    const hasApiKeyAccess = channel.hasApiKeyAccess(this.authorizationBag);
+    console.debug('hasApiKeyAccess', hasApiKeyAccess);
+    if (hasApiKeyAccess) return true;
+
+    if (channel.submissionByForm.includes(SubmissionByForm.administrators)) {
+      const hasAdminAccess = await channel.hasAdminAccess(transactionManager, this.authorizationBag, userGroups);
+      console.debug('hasAdminAccess', hasAdminAccess);
+      if (hasAdminAccess) return true;
+    }
+
+    const isTargetedNotification =
+      this.notification.targetUsers?.length > 0 ||
+      this.notification.targetGroups?.length > 0 ||
+      this.notification.targetData?.length > 0;
+    if (isTargetedNotification) {
+      // Targeted notifications are not allowed for members
+      return;
+    }
+
+    if (channel.submissionByForm.includes(SubmissionByForm.members)) {
+      const hasMemberAccess = await channel.hasMemberAccess(transactionManager, this.authorizationBag, userGroups);
+      console.debug('hasMemberAccess', hasMemberAccess);
+      if (hasMemberAccess) return true;
+    }
+
+    return false;
+  }
+
   async execute(transactionManager: EntityManager): Promise<GetNotificationResponse> {
+    const tracer = appsignal.tracer();
+    const span = tracer.createSpan(undefined, tracer.currentSpan());
+    span.setCategory('services/impl');
+    span.setName('SendNotification');
+
+    let child = span.child();
+    child.setCategory('get_channel_via_findOne');
     const targetChannel = await transactionManager.findOne(Channel, {
-      relations: ['members', 'groups', 'owner', 'adminGroup', 'category'],
+      relations: {
+        owner: true,
+        adminGroup: true,
+        category: true,
+      },
       where: { id: this.notification.target },
     });
+    child.close();
 
-    if (!targetChannel) throw new NotFoundError('Channel does not exist');
-
-    if (this.authorizationBag) {
-      if (
-        !(await targetChannel.canSendByForm(
-          this.authorizationBag,
-          this.notification.targetUsers?.length > 0 ||
-            this.notification.targetGroups?.length > 0 ||
-            this.notification.targetData?.length > 0,
-        ))
-      )
-        throw new ForbiddenError('Sending to Channel not Authorized !');
-
-      if (this.notification.priority === PriorityLevel.CRITICAL) {
-        if (!(await targetChannel.canSendCritical(this.authorizationBag)))
-          throw new ForbiddenError('Sending Critical Notifications to Channel not Authorized !');
-      }
-    } else {
-      // Call from the /unauthenticated
-      // restricted by direct access in openshift
+    if (!targetChannel) {
+      span.close();
+      throw new NotFoundError('Channel does not exist');
+    }
+
+    child = span.child();
+    child.setCategory('getCurrentUserGroups');
+    // Get current user groups, to be used next in SQL to compare with channel groups
+    const userGroups = await CernAuthorizationService.getCurrentUserGroups(this.authorizationBag.userName);
+    child.close();
+
+    child = span.child();
+    child.setCategory('has-access');
+    if (!(await this.hasAccess(transactionManager, targetChannel, userGroups))) {
+      child.close();
+      span.close();
+      throw new ForbiddenError('Sending to Channel not Authorized !');
+    }
+    child.close();
+
+    if (this.notification.priority === PriorityLevel.CRITICAL) {
+      if (!(await targetChannel.canSendCritical(this.authorizationBag)))
+        throw new ForbiddenError('Sending Critical Notifications to Channel not Authorized !');
     }
 
     this.validateFields();
@@ -129,10 +182,10 @@ export class SendNotification implements Command {
       from: newNotification.source,
     });
 
+    span.close();
     // Get back object from DB, without full info about target Channel
     return new GetNotificationResponse(
       await transactionManager.findOne(Notification, {
-        relations: ['targetUsers', 'targetGroups'],
         where: { id: newNotification.id },
       }),
     );
@@ -245,7 +298,7 @@ export class SendNotification implements Command {
         targetUsers.push(user);
 
         if (intersection) return;
-        if (await targetChannel.isMember(user)) return;
+        if (await targetChannel.isUserSubscribed(transactionManager, user)) return;
 
         usersToSubscribe.push(user);
       }),
@@ -255,10 +308,10 @@ export class SendNotification implements Command {
       return targetUsers;
     }
 
-    for (const user of usersToSubscribe) {
-      targetChannel.addMember(user);
-      await transactionManager.createQueryBuilder().relation(Channel, 'members').of(targetChannel).add(user);
-    }
+    console.debug('subscribing target users', targetUsers);
+    // optimized save performance
+    await transactionManager.createQueryBuilder().relation(Channel, 'members').of(targetChannel).add(usersToSubscribe);
+
     return targetUsers;
   }
 
@@ -287,7 +340,7 @@ export class SendNotification implements Command {
         targetGroups.push(group);
 
         if (intersection) return;
-        if (group && channel.groups.some(g => g.groupIdentifier === group.groupIdentifier)) return;
+        if (await channel.isGroupSubscribed(transactionManager, group)) return;
 
         groupsToSubscribe.push(group);
       }),
@@ -297,9 +350,10 @@ export class SendNotification implements Command {
       return targetGroups;
     }
 
-    // subscribe groups after creation to avoid inconsistent state
-    groupsToSubscribe.forEach(group => channel.addGroup(group));
-    await transactionManager.save(channel);
+    console.debug('subscribing target groups', targetGroups);
+
+    // optimized save performance
+    await transactionManager.createQueryBuilder().relation(Channel, 'groups').of(channel).add(groupsToSubscribe);
 
     return targetGroups;
   }
diff --git a/src/services/impl/notifications/update-user-notification.ts b/src/services/impl/notifications/update-user-notification.ts
index 6e037de711e6ad8d9484c3106ce8fb9d31bade78..fd2352d6f20269e21e4427bdff3944824c79896d 100644
--- a/src/services/impl/notifications/update-user-notification.ts
+++ b/src/services/impl/notifications/update-user-notification.ts
@@ -12,7 +12,7 @@ export class UpdateUserNotification implements Command {
 
   async execute(transactionManager: EntityManager): Promise<any> {
     if (await this.isNotificationOfUser(transactionManager)) {
-      let userNotification = await transactionManager.findOne(UserNotification, {
+      let userNotification = await transactionManager.findOneBy(UserNotification, {
         user: { id: this.authorizationBag.userId },
         notification: this.notification.notification,
       });
@@ -23,18 +23,17 @@ export class UpdateUserNotification implements Command {
       delete userNotification.user;
 
       return {
-        ...(await transactionManager.findOne(Notification, { id: this.notification.notification })),
+        ...(await transactionManager.findOneBy(Notification, { id: this.notification.notification })),
         ...userNotification,
       };
     }
   }
 
   async isNotificationOfUser(transactionManager: EntityManager): Promise<boolean> {
-    const notification = await transactionManager.findOne(
-      Notification,
-      { id: this.notification.notification },
-      { relations: ['users', 'users.user'] },
-    );
+    const notification = await transactionManager.findOne(Notification, {
+      where: { id: this.notification.notification },
+      relations: ['users', 'users.user'],
+    });
     return notification.users.filter(u => u.user.id === this.authorizationBag.userId).length === 1;
   }
 }
diff --git a/src/services/impl/preferences/get-user-preferences.ts b/src/services/impl/preferences/get-user-preferences.ts
index 780be22142af6e09eafc7fbf1a62b0b5c86bf928..e8987e2abde6f5b57e706831663c1b5d0f90002a 100644
--- a/src/services/impl/preferences/get-user-preferences.ts
+++ b/src/services/impl/preferences/get-user-preferences.ts
@@ -1,20 +1,21 @@
-import { Command } from "../command";
-import { EntityManager } from "typeorm";
-import { Preference } from "../../../models/preference";
-import { AuthorizationBag } from "../../../models/authorization-bag";
+import { Command } from '../command';
+import { EntityManager, IsNull } from 'typeorm';
+import { Preference } from '../../../models/preference';
+import { AuthorizationBag } from '../../../models/authorization-bag';
 
 export class GetUserPreferences implements Command {
-  constructor(
-    private channelId: string,
-    private authorizationBag: AuthorizationBag
-    ) {}
+  constructor(private channelId: string, private authorizationBag: AuthorizationBag) {}
 
   private async fetchGlobalPreferences(transactionManager: EntityManager) {
     return await transactionManager.find(Preference, {
-      relations: ["target", "disabledChannels", "devices"],
+      relations: {
+        target: true,
+        disabledChannels: true,
+        devices: true,
+      },
       where: {
-        user: this.authorizationBag.userId,
-        target: null
+        user: { id: this.authorizationBag.userId },
+        target: IsNull(),
       },
     });
   }
@@ -22,11 +23,11 @@ export class GetUserPreferences implements Command {
   private async fetchChannelPreferences(transactionManager: EntityManager) {
     return await transactionManager
       .getRepository(Preference)
-      .createQueryBuilder("preference")
-      .leftJoin("preference.user", "user")
-      .leftJoinAndSelect("preference.target", "target")
-      .leftJoinAndSelect("preference.devices", "device")
-      .where("user.id = :userId AND target.id = :channelId", {
+      .createQueryBuilder('preference')
+      .leftJoin('preference.user', 'user')
+      .leftJoinAndSelect('preference.target', 'target')
+      .leftJoinAndSelect('preference.devices', 'device')
+      .where('user.id = :userId AND target.id = :channelId', {
         userId: this.authorizationBag.userId,
         channelId: this.channelId,
       })
@@ -35,7 +36,7 @@ export class GetUserPreferences implements Command {
 
   async execute(transactionManager: EntityManager) {
     const result = {
-      globalPreferences: await this.fetchGlobalPreferences(transactionManager)
+      globalPreferences: await this.fetchGlobalPreferences(transactionManager),
     };
 
     if (this.channelId) {
diff --git a/src/services/impl/preferences/set-enabled-global-preferences.ts b/src/services/impl/preferences/set-enabled-global-preferences.ts
index 26d88fdd481ea4d4c2ec87915fc864c0fd7c032e..21e3c1dd5a71c1e7ef37da3f4ce5ec8d6606b5c1 100644
--- a/src/services/impl/preferences/set-enabled-global-preferences.ts
+++ b/src/services/impl/preferences/set-enabled-global-preferences.ts
@@ -15,29 +15,25 @@ export class SetEnabledGlobalPreference implements Command {
   ) {}
 
   async execute(transactionManager: EntityManager) {
-    let preference = await transactionManager.findOne(Preference, {
-      where: { id: this.preferenceId, user: this.authorizationBag.userId },
+    const preference = await transactionManager.findOne(Preference, {
+      where: {
+        id: this.preferenceId,
+        user: { id: this.authorizationBag.userId },
+      },
       relations: ['target', 'disabledChannels', 'devices'],
     });
 
-    let channel = await transactionManager.findOne(Channel, {
+    const channel = await transactionManager.findOne(Channel, {
       where: { id: this.channelId },
     });
 
     if (preference.target)
-      throw new ForbiddenError(
-        'Enable/disable preference is only allowed for global preferences.',
-      );
+      throw new ForbiddenError('Enable/disable preference is only allowed for global preferences.');
 
-    if (!preference)
-      throw new ForbiddenError(
-        'The preference does not exist or you are not the owner.',
-      );
+    if (!preference) throw new ForbiddenError('The preference does not exist or you are not the owner.');
 
     if (this.isEnabled) {
-      preference.disabledChannels = preference.disabledChannels.filter(
-        channel => channel.id !== this.channelId,
-      );
+      preference.disabledChannels = preference.disabledChannels.filter(channel => channel.id !== this.channelId);
     } else if (!preference.disabledChannels.some(c => c.id === channel.id)) {
       preference.disabledChannels.push(channel);
     }
diff --git a/src/services/impl/tags/create-tag.ts b/src/services/impl/tags/create-tag.ts
index e52cb74e065b433cd49babdd8c22651e6aeb6cfe..9cb21b9dca898cc99db24a1a7962fda042b91627 100644
--- a/src/services/impl/tags/create-tag.ts
+++ b/src/services/impl/tags/create-tag.ts
@@ -1,30 +1,32 @@
-import { Command } from "../command";
-import { EntityManager } from "typeorm";
-import { Tag } from "../../../models/tag";
-import { AuthorizationBag } from "../../../models/authorization-bag";
-import { BadRequestError } from "routing-controllers";
+import { Command } from '../command';
+import { EntityManager } from 'typeorm';
+import { Tag } from '../../../models/tag';
+import { AuthorizationBag } from '../../../models/authorization-bag';
+import { BadRequestError } from 'routing-controllers';
 
 export class CreateTag implements Command {
-  constructor(private tag: Tag, private authorizationBag: AuthorizationBag) { }
+  constructor(private tag: Tag, private authorizationBag: AuthorizationBag) {}
 
   async execute(transactionManager: EntityManager) {
     // Check tag length
     if (this.tag.name.length < 2 || this.tag.name.length > 32) {
-      throw new BadRequestError("Tag should be between 2 and 32 characters.")
+      throw new BadRequestError('Tag should be between 2 and 32 characters.');
     }
     // Check if tag already exists
-    if (await transactionManager.findOne(Tag, {
-      name: this.tag.name,
-    }) )  {
-      throw new BadRequestError("Tag already exists");
+    if (
+      await transactionManager.findOneBy(Tag, {
+        name: this.tag.name,
+      })
+    ) {
+      throw new BadRequestError('Tag already exists');
     }
 
-    const tag = new Tag({ ...this.tag })
+    const tag = new Tag({ ...this.tag });
 
     const savedTag = await transactionManager.save(tag);
     if (savedTag) {
-      return await transactionManager.findOne(Tag, {
-          id: savedTag.id,
+      return await transactionManager.findOneBy(Tag, {
+        id: savedTag.id,
       });
     }
   }
diff --git a/src/services/impl/usersettings/delete-draft.ts b/src/services/impl/usersettings/delete-draft.ts
index 2bbb0de5ec2bd3f0fa58c73d4c77a80fad9f085f..37f0c858d4ad650cf81224a26be7b67e3b10600e 100644
--- a/src/services/impl/usersettings/delete-draft.ts
+++ b/src/services/impl/usersettings/delete-draft.ts
@@ -1,25 +1,24 @@
-import {Command} from "../command";
-import {EntityManager} from "typeorm";
-import {UserSettings} from "../../../models/user-settings";
-import {AuthorizationBag} from "../../../models/authorization-bag";
-import {NotFoundError} from "routing-controllers";
+import { Command } from '../command';
+import { EntityManager } from 'typeorm';
+import { UserSettings } from '../../../models/user-settings';
+import { AuthorizationBag } from '../../../models/authorization-bag';
+import { NotFoundError } from 'routing-controllers';
 
 export class DeleteDraft implements Command {
-    constructor(private authorizationBag: AuthorizationBag) {
-    }
-
-    async execute(transactionManager: EntityManager) {
-        let userSetting = await transactionManager.findOne(UserSettings, {
-            where: {
-                user: this.authorizationBag.userId,
-            },
-        });
+  constructor(private authorizationBag: AuthorizationBag) {}
 
-        if (!userSetting) {
-            throw new NotFoundError("Draft not found")
-        }
+  async execute(transactionManager: EntityManager) {
+    const userSetting = await transactionManager.findOne(UserSettings, {
+      where: {
+        user: { id: this.authorizationBag.userId },
+      },
+    });
 
-        userSetting.draft = null;
-        await transactionManager.save(userSetting);
+    if (!userSetting) {
+      throw new NotFoundError('Draft not found');
     }
+
+    userSetting.draft = null;
+    await transactionManager.save(userSetting);
+  }
 }
diff --git a/src/services/impl/usersettings/get-draft.ts b/src/services/impl/usersettings/get-draft.ts
index 6e2d191ed67e1a9fd2990865cd5f985288118fe8..98dd1299d3172b02266a41c18b0cf5f72e82909c 100644
--- a/src/services/impl/usersettings/get-draft.ts
+++ b/src/services/impl/usersettings/get-draft.ts
@@ -1,21 +1,20 @@
-import {Command} from "../command";
-import {EntityManager} from "typeorm";
-import {UserSettings} from "../../../models/user-settings";
-import {AuthorizationBag} from "../../../models/authorization-bag";
+import { Command } from '../command';
+import { EntityManager } from 'typeorm';
+import { UserSettings } from '../../../models/user-settings';
+import { AuthorizationBag } from '../../../models/authorization-bag';
 
 export class GetDraft implements Command {
-    constructor(private authorizationBag: AuthorizationBag) {
-    }
+  constructor(private authorizationBag: AuthorizationBag) {}
 
-    async execute(transactionManager: EntityManager) {
-        const userSetting = await transactionManager.findOne(UserSettings, {
-            where: {
-                user: this.authorizationBag.userId,
-            },
-        });
+  async execute(transactionManager: EntityManager) {
+    const userSetting = await transactionManager.findOne(UserSettings, {
+      where: {
+        user: { id: this.authorizationBag.userId },
+      },
+    });
 
-        if (userSetting) {
-            return userSetting.draft;
-        }
+    if (userSetting) {
+      return userSetting.draft;
     }
+  }
 }
diff --git a/src/services/impl/usersettings/get-user-settings.ts b/src/services/impl/usersettings/get-user-settings.ts
index b8427d1fcb5da99430f544688d12548cf43f4b00..c09bc38c4510c5742322b5025e5180321d7f2435 100644
--- a/src/services/impl/usersettings/get-user-settings.ts
+++ b/src/services/impl/usersettings/get-user-settings.ts
@@ -1,35 +1,32 @@
-import { Command } from "../command";
-import { EntityManager } from "typeorm";
-import { UserSettings } from "../../../models/user-settings";
-import { User } from "../../../models/user";
-import { AuthorizationBag } from "../../../models/authorization-bag";
-import { NotFoundError } from "routing-controllers";
+import { Command } from '../command';
+import { EntityManager } from 'typeorm';
+import { UserSettings } from '../../../models/user-settings';
+import { User } from '../../../models/user';
+import { AuthorizationBag } from '../../../models/authorization-bag';
+import { NotFoundError } from 'routing-controllers';
 
 export class GetUserSettings implements Command {
-    constructor(private authorizationBag: AuthorizationBag) {
-    }
-
-    async execute(transactionManager: EntityManager) {
+  constructor(private authorizationBag: AuthorizationBag) {}
 
-        let userSettings = await transactionManager.findOne(UserSettings, {
-            where: {
-                user: this.authorizationBag.userId,
-            },
-        });
+  async execute(transactionManager: EntityManager) {
+    let userSettings = await transactionManager.findOne(UserSettings, {
+      where: {
+        user: {
+          id: this.authorizationBag.userId
+        },
+      },
+    });
 
-        if (!userSettings) {
-            const user = await transactionManager.findOne(
-                User,
-                { id: this.authorizationBag.userId}
-            );
+    if (!userSettings) {
+      const user = await transactionManager.findOneBy(User, { id: this.authorizationBag.userId });
 
-            userSettings = new UserSettings({
-                user: user,
-                draft: "",
-                favoriteList: [],
-            });
-            return await transactionManager.save(userSettings);
-        }
-        return userSettings;
+      userSettings = new UserSettings({
+        user: user,
+        draft: '',
+        favoriteList: [],
+      });
+      return await transactionManager.save(userSettings);
     }
+    return userSettings;
+  }
 }
diff --git a/src/services/impl/usersettings/save-draft.ts b/src/services/impl/usersettings/save-draft.ts
index 0446f94497562a092a562d7ddf21e2299a65d022..b34d5f6b42c7f0deabb2284d9dae7ddeb854e119 100644
--- a/src/services/impl/usersettings/save-draft.ts
+++ b/src/services/impl/usersettings/save-draft.ts
@@ -1,26 +1,25 @@
-import {Command} from "../command";
-import {EntityManager} from "typeorm";
-import {UserSettings} from "../../../models/user-settings";
-import {AuthorizationBag} from "../../../models/authorization-bag";
+import { Command } from '../command';
+import { EntityManager } from 'typeorm';
+import { UserSettings } from '../../../models/user-settings';
+import { AuthorizationBag } from '../../../models/authorization-bag';
 
 export class SaveDraft implements Command {
-    constructor(private draft: string, private authorizationBag: AuthorizationBag) {
-    }
-
-    async execute(transactionManager: EntityManager) {
-        let userSetting = await transactionManager.findOne(UserSettings, {
-            where: {
-                user: this.authorizationBag.userId,
-            },
-        });
+  constructor(private draft: string, private authorizationBag: AuthorizationBag) {}
 
-        if (!userSetting) {
-            userSetting = new UserSettings({
-                user: this.authorizationBag.userId,
-            })
-        }
+  async execute(transactionManager: EntityManager) {
+    let userSetting = await transactionManager.findOne(UserSettings, {
+      where: {
+        user: { id: this.authorizationBag.userId },
+      },
+    });
 
-        userSetting.draft = this.draft;
-        await transactionManager.save(userSetting);
+    if (!userSetting) {
+      userSetting = new UserSettings({
+        user: this.authorizationBag.userId,
+      });
     }
+
+    userSetting.draft = this.draft;
+    await transactionManager.save(userSetting);
+  }
 }