diff --git a/.vscode/settings.json b/.vscode/settings.json index d74b1e5bf..499d0f3a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ "dist": true, ".vscode": true, "package-lock.json": true - } + }, + "tslint.autoFixOnSave": true } diff --git a/DESIGN.md b/DESIGN.md index 4bd09913c..1481e7f4b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -21,3 +21,12 @@ 1. No tenants 2. Create a tenant <-- consuming the TMA + +## Database connections + +### Redis Clients + +1. Tenant RedisPubSub Subscriber +2. Tenant RedisPubSub Publisher +3. Management RedisPubSub Subscriber +4. Management RedisPubSub Publisher diff --git a/package-lock.json b/package-lock.json index 207de292a..e6bfa8dfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -148,6 +148,15 @@ "integrity": "sha512-IsX9aDHDzJohkm3VCDB8tkzl5RQ34E/PFA29TQk6uDGb7Oc869ZBtmdKVDBzY3+h9GnXB8ssrRXEPVZrlIOPOw==", "dev": true }, + "@types/passport": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-0.4.5.tgz", + "integrity": "sha512-Ow5akVXwEZlOPCWGbEGy0GX4ocdwKz7JJH1K+BMd/BSOxmJTo2obH2AKbsgcncQvw5z7AGopdIu1Ap/j9sMRnQ==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/range-parser": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", @@ -207,6 +216,18 @@ "string-width": "^2.0.0" } }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -348,6 +369,32 @@ "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", "dev": true }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -541,6 +588,12 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "bunyan": { "version": "1.8.12", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", @@ -585,6 +638,37 @@ "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", "dev": true }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "chokidar": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", @@ -670,6 +754,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", @@ -939,11 +1029,27 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "eslint-plugin-prettier": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz", + "integrity": "sha512-floiaI4F7hRkTrFe8V2ItOK97QYrX75DjmdzmVITZoAP6Cn06oEDPQRsO6MlHEP/u2SxI3xQ52Kpjw6j5WGfeQ==", + "dev": true, + "requires": { + "fast-diff": "^1.1.1", + "jest-docblock": "^21.0.0" + } + }, "esprima": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1150,6 +1256,12 @@ } } }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -1228,6 +1340,12 @@ "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", "dev": true }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, "fsevents": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", @@ -1945,6 +2063,21 @@ "uuid": "^3.1.0" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -2020,7 +2153,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "optional": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -2314,6 +2446,12 @@ "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" }, + "jest-docblock": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", + "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", + "dev": true + }, "joi": { "version": "13.4.0", "resolved": "https://registry.npmjs.org/joi/-/joi-13.4.0.tgz", @@ -2324,6 +2462,12 @@ "topo": "3.x.x" } }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, "js-yaml": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", @@ -2856,6 +3000,20 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "passport": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", + "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", @@ -2879,11 +3037,22 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -3115,6 +3284,15 @@ "semver": "^5.1.0" } }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, "resolve-from": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", @@ -3503,6 +3681,15 @@ "safe-buffer": "~5.1.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -3536,6 +3723,12 @@ "ws": "^5.2.0" } }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -3648,6 +3841,73 @@ "strip-json-comments": "^2.0.1" } }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tslint": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", + "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.12.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "tslint-config-prettier": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.13.0.tgz", + "integrity": "sha512-assE77K7K8Q9j8CVIHiU3d1uoKc8N5v7UPpkQ9IE8BEPWkvSYR37lDuYekDlAMFqR1IpD6CrS+uOJLl6pw7Wdw==", + "dev": true + }, + "tslint-plugin-prettier": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-1.3.0.tgz", + "integrity": "sha512-6UqeeV6EABp0RdQkW6eC1vwnAXcKMGJgPeJ5soXiKdSm2vv7c3dp+835CM8pjgx9l4uSa7tICm1Kli+SMsADDg==", + "dev": true, + "requires": { + "eslint-plugin-prettier": "^2.2.0", + "tslib": "^1.7.1" + } + }, + "tsutils": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", diff --git a/package.json b/package.json index be8a2c554..47a0dc4ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coralproject/talk", - "version": "1.0.0", + "version": "5.0.0", "description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.", "scripts": { "start": "node dist/index.js", @@ -28,6 +28,7 @@ "lodash": "^4.17.10", "luxon": "^1.2.1", "mongodb": "^3.0.10", + "passport": "^0.4.0", "performance-now": "^2.1.0", "subscriptions-transport-ws": "^0.9.11", "uuid": "^3.2.1" @@ -43,6 +44,7 @@ "@types/lodash": "^4.14.109", "@types/luxon": "^0.5.3", "@types/mongodb": "^3.0.19", + "@types/passport": "^0.4.5", "@types/uuid": "^3.4.3", "@types/ws": "^5.1.2", "graphql-playground-middleware-express": "^1.7.0", @@ -50,6 +52,9 @@ "prettier": "^1.13.4", "ts-node": "^6.1.1", "tsconfig-paths": "^3.4.0", + "tslint": "^5.10.0", + "tslint-config-prettier": "^1.13.0", + "tslint-plugin-prettier": "^1.3.0", "typescript": "^2.9.1" } } \ No newline at end of file diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 7d7d34821..3711234e6 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -1,18 +1,18 @@ import { Express } from "express"; -import { Db } from "mongodb"; import http from "http"; import { Redis } from "ioredis"; +import { Db } from "mongodb"; import { Config } from "talk-server/config"; -import { Schemas } from "talk-server/graph/schemas"; import { handleSubscriptions } from "talk-server/graph/common/subscriptions/middleware"; +import { Schemas } from "talk-server/graph/schemas"; -import { createRouter } from "./router"; -import serveStatic from "./middleware/serveStatic"; import { access as accessLogger, error as errorLogger, } from "./middleware/logging"; +import serveStatic from "./middleware/serveStatic"; +import { createRouter } from "./router"; export interface AppOptions { parent: Express; diff --git a/src/core/server/app/middleware/logging.ts b/src/core/server/app/middleware/logging.ts index c4e317e27..9db48fa31 100644 --- a/src/core/server/app/middleware/logging.ts +++ b/src/core/server/app/middleware/logging.ts @@ -1,11 +1,11 @@ -import { RequestHandler, ErrorRequestHandler } from "express"; -import logger from "../../logger"; +import { ErrorRequestHandler, RequestHandler } from "express"; import now from "performance-now"; +import logger from "../../logger"; export const access: RequestHandler = (req, res, next) => { const startTime = now(); const end = res.end; - res.end = (chunk: any, encodingOrCb?: string | Function, cb?: Function) => { + res.end = (chunk: any, encodingOrCb?: any, cb?: any) => { // Compute the end time. const responseTime = Math.round(now() - startTime); diff --git a/src/core/server/app/middleware/passport/index.ts b/src/core/server/app/middleware/passport/index.ts new file mode 100644 index 000000000..b1cf18fc2 --- /dev/null +++ b/src/core/server/app/middleware/passport/index.ts @@ -0,0 +1,15 @@ +import { Db } from "mongodb"; +import passport, { Authenticator } from "passport"; + +export interface PassportOptions { + db: Db; +} + +export function createPassport({ + db, +}: PassportOptions): passport.Authenticator { + // Create the authenticator. + const auth = new Authenticator(); + + return auth; +} diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index 753fa06ac..e50e42b15 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -1,7 +1,8 @@ -import express, { Router } from "express"; +import express from "express"; +import passport from "passport"; -import tenantGraphMiddleware from "talk-server/graph/tenant/middleware"; import managementGraphMiddleware from "talk-server/graph/management/middleware"; +import tenantGraphMiddleware from "talk-server/graph/tenant/middleware"; import { AppOptions } from "./index"; import playground from "./middleware/playground"; @@ -9,17 +10,6 @@ import playground from "./middleware/playground"; async function createManagementRouter(opts: AppOptions) { const router = express.Router(); - if (opts.config.get("env") === "development") { - // GraphiQL - router.get( - "/graphiql", - playground({ - endpoint: "/api/management/graphql", - subscriptionEndpoint: "/api/management/live", - }) - ); - } - // Management API router.use( "/graphql", @@ -37,17 +27,6 @@ async function createManagementRouter(opts: AppOptions) { async function createTenantRouter(opts: AppOptions) { const router = express.Router(); - if (opts.config.get("env") === "development") { - // GraphiQL - router.get( - "/graphiql", - playground({ - endpoint: "/api/tenant/graphql", - subscriptionEndpoint: "/api/tenant/live", - }) - ); - } - // Tenant API router.use( "/graphql", @@ -62,6 +41,9 @@ async function createAPIRouter(opts: AppOptions) { // Create a router. const router = express.Router(); + // Setup Passport. + router.use(passport.initialize()); + // Configure the tenant routes. router.use("/tenant", await createTenantRouter(opts)); @@ -77,5 +59,25 @@ export async function createRouter(opts: AppOptions) { router.use("/api", await createAPIRouter(opts)); + if (opts.config.get("env") === "development") { + // Tenant GraphiQL + router.get( + "/tenant/graphiql", + playground({ + endpoint: "/api/tenant/graphql", + subscriptionEndpoint: "/api/tenant/live", + }) + ); + + // Management GraphiQL + router.get( + "/management/graphiql", + playground({ + endpoint: "/api/management/graphql", + subscriptionEndpoint: "/api/management/live", + }) + ); + } + return router; } diff --git a/src/core/server/config.ts b/src/core/server/config.ts index a6dcedc2b..fd200db91 100644 --- a/src/core/server/config.ts +++ b/src/core/server/config.ts @@ -1,6 +1,6 @@ -import Joi from "joi"; import convict from "convict"; import dotenv from "dotenv"; +import Joi from "joi"; // Apply all the configuration provided in the .env file if it isn't already in // the environment. diff --git a/src/core/server/graph/common/middleware/index.ts b/src/core/server/graph/common/middleware/index.ts index 0d9af1264..361bc7073 100644 --- a/src/core/server/graph/common/middleware/index.ts +++ b/src/core/server/graph/common/middleware/index.ts @@ -1,10 +1,10 @@ +import { resolveGraphqlOptions } from "apollo-server-core"; import { - graphqlExpress, ExpressGraphQLOptionsFunction, + graphqlExpress, GraphQLOptions, } from "apollo-server-express"; -import { GraphQLError, FieldDefinitionNode, ValidationContext } from "graphql"; -import { resolveGraphqlOptions } from "apollo-server-core"; +import { FieldDefinitionNode, GraphQLError, ValidationContext } from "graphql"; import { Config } from "talk-server/config"; // Sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L46-L57 @@ -28,7 +28,7 @@ export const graphqlMiddleware = ( // Generate the validation rules. const validationRules: Array<(context: ValidationContext) => any> = []; - if (config.get("env") !== "production") { + if (config.get("env") === "production") { // Disable introspection in production. validationRules.push(NoIntrospection); } diff --git a/src/core/server/graph/common/resolvers/mutation.ts b/src/core/server/graph/common/resolvers/mutation.ts new file mode 100644 index 000000000..78de8a76a --- /dev/null +++ b/src/core/server/graph/common/resolvers/mutation.ts @@ -0,0 +1,3 @@ +export interface ClientMutationProps { + clientMutationId: string; +} diff --git a/src/core/server/graph/common/scalars/cursor.ts b/src/core/server/graph/common/scalars/cursor.ts index 92b584a20..c48cd0b65 100644 --- a/src/core/server/graph/common/scalars/cursor.ts +++ b/src/core/server/graph/common/scalars/cursor.ts @@ -1,11 +1,11 @@ -import { DateTime } from "luxon"; import { GraphQLScalarType } from "graphql"; import { Kind } from "graphql/language"; +import { DateTime } from "luxon"; import { Cursor } from "talk-server/models/connection"; function parseIntegerCursor(value: string): number { try { - const cursor = parseInt(value); + const cursor = parseInt(value, 10); return cursor; } catch (err) { diff --git a/src/core/server/graph/common/schema/index.ts b/src/core/server/graph/common/schema/index.ts index e2c859d03..28d5899e5 100644 --- a/src/core/server/graph/common/schema/index.ts +++ b/src/core/server/graph/common/schema/index.ts @@ -1,5 +1,5 @@ -import { addResolveFunctionsToSchema, IResolvers } from "graphql-tools"; import { getGraphQLProjectConfig } from "graphql-config"; +import { addResolveFunctionsToSchema, IResolvers } from "graphql-tools"; export default function loadSchema(projectName: string, resolvers: IResolvers) { // Load the configuration from the provided `.graphqlconfig` file. diff --git a/src/core/server/graph/common/subscriptions/middleware.ts b/src/core/server/graph/common/subscriptions/middleware.ts index 726a8c2b2..b7e064c9d 100644 --- a/src/core/server/graph/common/subscriptions/middleware.ts +++ b/src/core/server/graph/common/subscriptions/middleware.ts @@ -1,6 +1,6 @@ +import { execute, GraphQLSchema, subscribe } from "graphql"; import http from "http"; import { SubscriptionServer } from "subscriptions-transport-ws"; -import { GraphQLSchema, execute, subscribe } from "graphql"; export interface SubscriptionMiddlewareOptions { schema: GraphQLSchema; diff --git a/src/core/server/graph/common/subscriptions/pubsub.ts b/src/core/server/graph/common/subscriptions/pubsub.ts index 62c9b44e5..89ede4771 100644 --- a/src/core/server/graph/common/subscriptions/pubsub.ts +++ b/src/core/server/graph/common/subscriptions/pubsub.ts @@ -1,6 +1,6 @@ import { RedisPubSub } from "graphql-redis-subscriptions"; -import { createRedisClient } from "talk-server/services/redis"; import { Config } from "talk-server/config"; +import { createRedisClient } from "talk-server/services/redis"; export async function createPubSub(config: Config): Promise { // Create the Redis clients for the PubSub server. diff --git a/src/core/server/graph/management/middleware.ts b/src/core/server/graph/management/middleware.ts index b547b8380..def73c2e9 100644 --- a/src/core/server/graph/management/middleware.ts +++ b/src/core/server/graph/management/middleware.ts @@ -1,8 +1,8 @@ -import { Db } from "mongodb"; import { GraphQLSchema } from "graphql"; +import { Db } from "mongodb"; -import { graphqlMiddleware } from "talk-server/graph/common/middleware"; import { Config } from "talk-server/config"; +import { graphqlMiddleware } from "talk-server/graph/common/middleware"; import Context from "./context"; diff --git a/src/core/server/graph/tenant/context.ts b/src/core/server/graph/tenant/context.ts index aabe93615..18c6cee88 100644 --- a/src/core/server/graph/tenant/context.ts +++ b/src/core/server/graph/tenant/context.ts @@ -1,20 +1,27 @@ -import loaders from "./loaders"; import { Db } from "mongodb"; import { Tenant } from "talk-server/models/tenant"; +import { User } from "talk-server/models/user"; +import loaders from "./loaders"; +import mutators from "./mutators"; export interface TenantContextOptions { - tenant?: Tenant; db: Db; + tenant?: Tenant; + user?: User; } export default class TenantContext { public loaders: ReturnType; + public mutators: ReturnType; public db: Db; public tenant?: Tenant; + public user?: User; - constructor({ tenant, db }: TenantContextOptions) { + constructor({ user, tenant, db }: TenantContextOptions) { this.tenant = tenant; + this.user = user; this.loaders = loaders(this); + this.mutators = mutators(this); this.db = db; } } diff --git a/src/core/server/graph/tenant/loaders/assets.ts b/src/core/server/graph/tenant/loaders/assets.ts index b8747deb6..a468db9e9 100644 --- a/src/core/server/graph/tenant/loaders/assets.ts +++ b/src/core/server/graph/tenant/loaders/assets.ts @@ -1,11 +1,8 @@ import DataLoader from "dataloader"; -import { - Asset, - retrieveMany as retrieveManyAssets, -} from "talk-server/models/asset"; -import Context from "talk-server/graph/tenant/context"; +import TenantContext from "talk-server/graph/tenant/context"; +import { Asset, retrieveManyAssets } from "talk-server/models/asset"; -export default (ctx: Context) => ({ +export default (ctx: TenantContext) => ({ asset: new DataLoader(ids => retrieveManyAssets(ctx.db, ctx.tenant.id, ids) ), diff --git a/src/core/server/graph/tenant/loaders/comments.ts b/src/core/server/graph/tenant/loaders/comments.ts index c6c1c22ce..d90e0ab71 100644 --- a/src/core/server/graph/tenant/loaders/comments.ts +++ b/src/core/server/graph/tenant/loaders/comments.ts @@ -1,12 +1,12 @@ import DataLoader from "dataloader"; +import Context from "talk-server/graph/tenant/context"; import { Comment, - retrieveMany, - retrieveAssetConnection, ConnectionInput, + retrieveAssetConnection, + retrieveMany, retrieveRepliesConnection, } from "talk-server/models/comment"; -import Context from "talk-server/graph/tenant/context"; export default (ctx: Context) => ({ comment: new DataLoader((ids: string[]) => diff --git a/src/core/server/graph/tenant/loaders/index.ts b/src/core/server/graph/tenant/loaders/index.ts index d9f2ca191..d4a00cc28 100644 --- a/src/core/server/graph/tenant/loaders/index.ts +++ b/src/core/server/graph/tenant/loaders/index.ts @@ -1,7 +1,7 @@ +import Context from "talk-server/graph/tenant/context"; import Assets from "./assets"; import Comments from "./comments"; import Users from "./users"; -import Context from "talk-server/graph/tenant/context"; export default (ctx: Context) => ({ Assets: Assets(ctx), diff --git a/src/core/server/graph/tenant/loaders/users.ts b/src/core/server/graph/tenant/loaders/users.ts index 3620b9bfc..a7b9f5aa7 100644 --- a/src/core/server/graph/tenant/loaders/users.ts +++ b/src/core/server/graph/tenant/loaders/users.ts @@ -1,6 +1,6 @@ import DataLoader from "dataloader"; -import { User, retrieveMany } from "talk-server/models/user"; import Context from "talk-server/graph/tenant/context"; +import { retrieveMany, User } from "talk-server/models/user"; export default (ctx: Context) => ({ user: new DataLoader(ids => diff --git a/src/core/server/graph/tenant/middleware.ts b/src/core/server/graph/tenant/middleware.ts index e6c2f36d6..a98dfa563 100644 --- a/src/core/server/graph/tenant/middleware.ts +++ b/src/core/server/graph/tenant/middleware.ts @@ -1,10 +1,10 @@ -import { Db } from "mongodb"; import { GraphQLSchema } from "graphql"; +import { Db } from "mongodb"; -import { retrieveByDomain } from "talk-server/models/tenant"; -import { createPubSub } from "talk-server/graph/common/subscriptions/pubsub"; import { Config } from "talk-server/config"; import { graphqlMiddleware } from "talk-server/graph/common/middleware"; +import { createPubSub } from "talk-server/graph/common/subscriptions/pubsub"; +import { retrieveTenantByDomain } from "talk-server/models/tenant"; import TenantContext from "./context"; @@ -14,11 +14,15 @@ export default async (schema: GraphQLSchema, config: Config, db: Db) => { return graphqlMiddleware(config, async req => { // TODO: replace with shared synced cache instead of direct db access. - const tenant = await retrieveByDomain(db, req.hostname); + const tenant = await retrieveTenantByDomain(db, req.hostname); + // Load the user from the request. + const user = req.user; + + // Return the graph options. return { schema, - context: new TenantContext({ db, tenant }), + context: new TenantContext({ db, tenant, user }), }; }); }; diff --git a/src/core/server/graph/tenant/mutators/comment.ts b/src/core/server/graph/tenant/mutators/comment.ts new file mode 100644 index 000000000..f51deecb0 --- /dev/null +++ b/src/core/server/graph/tenant/mutators/comment.ts @@ -0,0 +1,15 @@ +import TenantContext from "talk-server/graph/tenant/context"; +import { CreateCommentInput } from "talk-server/graph/tenant/resolvers/mutation"; +import { Comment } from "talk-server/models/comment"; +import { create } from "talk-server/services/comments"; + +export default (ctx: TenantContext) => ({ + create: (input: CreateCommentInput): Promise => { + return create(ctx.db, ctx.tenant.id, { + author_id: ctx.user.id, + asset_id: input.assetID, + body: input.body, + parent_id: input.parentID, + }); + }, +}); diff --git a/src/core/server/graph/tenant/mutators/index.ts b/src/core/server/graph/tenant/mutators/index.ts new file mode 100644 index 000000000..25433bfbe --- /dev/null +++ b/src/core/server/graph/tenant/mutators/index.ts @@ -0,0 +1,6 @@ +import TenantContext from "talk-server/graph/tenant/context"; +import Comment from "./comment"; + +export default (ctx: TenantContext) => ({ + Comment: Comment(ctx), +}); diff --git a/src/core/server/graph/tenant/resolvers/asset.ts b/src/core/server/graph/tenant/resolvers/asset.ts index 606dd13da..cab72fbc8 100644 --- a/src/core/server/graph/tenant/resolvers/asset.ts +++ b/src/core/server/graph/tenant/resolvers/asset.ts @@ -1,5 +1,5 @@ -import { Asset } from "talk-server/models/asset"; import Context from "talk-server/graph/tenant/context"; +import { Asset } from "talk-server/models/asset"; import { ConnectionInput } from "talk-server/models/comment"; export default { diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index 4d4d71688..22f72b5e5 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -1,5 +1,5 @@ -import { Comment, ConnectionInput } from "talk-server/models/comment"; import Context from "talk-server/graph/tenant/context"; +import { Comment, ConnectionInput } from "talk-server/models/comment"; export default { author: async (comment: Comment, _: any, ctx: Context) => diff --git a/src/core/server/graph/tenant/resolvers/index.ts b/src/core/server/graph/tenant/resolvers/index.ts index 52402d21e..d1143f1ff 100644 --- a/src/core/server/graph/tenant/resolvers/index.ts +++ b/src/core/server/graph/tenant/resolvers/index.ts @@ -1,6 +1,7 @@ +import Cursor from "../../common/scalars/cursor"; import Asset from "./asset"; import Comment from "./comment"; -import Cursor from "../../common/scalars/cursor"; +import Mutation from "./mutation"; import Query from "./query"; export default { @@ -8,4 +9,5 @@ export default { Comment, Cursor, Query, + Mutation, }; diff --git a/src/core/server/graph/tenant/resolvers/mutation.ts b/src/core/server/graph/tenant/resolvers/mutation.ts new file mode 100644 index 000000000..4d3b82a76 --- /dev/null +++ b/src/core/server/graph/tenant/resolvers/mutation.ts @@ -0,0 +1,26 @@ +import { ClientMutationProps } from "talk-server/graph/common/resolvers/mutation"; +import TenantContext from "talk-server/graph/tenant/context"; +import { Comment } from "talk-server/models/comment"; + +export interface CreateCommentInput extends ClientMutationProps { + assetID: string; + parentID?: string; + body: string; +} + +export interface CreateCommentPayload extends ClientMutationProps { + comment: Comment; +} + +const Mutation = { + createComment: async ( + source: void, + input: CreateCommentInput, + ctx: TenantContext + ): Promise => ({ + comment: await ctx.mutators.Comment.create(input), + clientMutationId: input.clientMutationId, + }), +}; + +export default Mutation; diff --git a/src/core/server/graph/tenant/resolvers/query.ts b/src/core/server/graph/tenant/resolvers/query.ts index faf7e8dc5..f4ea10b4b 100644 --- a/src/core/server/graph/tenant/resolvers/query.ts +++ b/src/core/server/graph/tenant/resolvers/query.ts @@ -3,7 +3,7 @@ import { Asset } from "talk-server/models/asset"; export default { asset: async ( - _: any, + source: void, { id, url }: { id?: string; url: string }, ctx: TenantContext ): Promise => { diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index ef3005475..1a3d1b4f7 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -18,146 +18,146 @@ scalar Cursor # The moderation mode of the site. enum MODERATION_MODE { - """ - Comments posted while in `PRE` mode will be labeled with a `PREMOD` - status and will require a moderator decision before being visible. - """ - PRE + """ + Comments posted while in `PRE` mode will be labeled with a `PREMOD` + status and will require a moderator decision before being visible. + """ + PRE - """ - Comments posted while in `POST` will be visible immediately. - """ - POST + """ + Comments posted while in `POST` will be visible immediately. + """ + POST } """ Wordlist describes all the available wordlists. """ type Wordlist { - """ - banned words will by default reject the comment if it is found. - """ - banned: [String!]! + """ + banned words will by default reject the comment if it is found. + """ + banned: [String!]! - """ - suspect words will simply flag the comment. - """ - suspect: [String!]! + """ + suspect words will simply flag the comment. + """ + suspect: [String!]! } # Settings stores the global settings for a given installation. type Settings { - """ - moderation is the moderation mode for all Asset's on the site. - """ - moderation: MODERATION_MODE! + """ + moderation is the moderation mode for all Asset's on the site. + """ + moderation: MODERATION_MODE! - """ - Enables a requirement for email confirmation before a user can login. - """ - requireEmailConfirmation: Boolean! + """ + Enables a requirement for email confirmation before a user can login. + """ + requireEmailConfirmation: Boolean! - """ - infoBoxEnable will enable the Info Box content visible above the question - box. - """ - infoBoxEnable: Boolean! + """ + infoBoxEnable will enable the Info Box content visible above the question + box. + """ + infoBoxEnable: Boolean! - """ - infoBoxContent is the content of the Info Box. - """ - infoBoxContent: String + """ + infoBoxContent is the content of the Info Box. + """ + infoBoxContent: String - """ - questionBoxEnable will enable the Question Box's content to be visible above - the comment box. - """ - questionBoxEnable: Boolean! + """ + questionBoxEnable will enable the Question Box's content to be visible above + the comment box. + """ + questionBoxEnable: Boolean! - """ - questionBoxContent is the content of the Question Box. - """ - questionBoxContent: String + """ + questionBoxContent is the content of the Question Box. + """ + questionBoxContent: String - """ - questionBoxIcon is the icon for the Question Box. - """ - questionBoxIcon: String + """ + questionBoxIcon is the icon for the Question Box. + """ + questionBoxIcon: String - """ - premodLinksEnable will put all comments that contain links into premod. - """ - premodLinksEnable: Boolean! + """ + premodLinksEnable will put all comments that contain links into premod. + """ + premodLinksEnable: Boolean! - """ - autoCloseStream when true will auto close the stream when the `closeTimeout` - amount of seconds have been reached. - """ - autoCloseStream: Boolean! + """ + autoCloseStream when true will auto close the stream when the `closeTimeout` + amount of seconds have been reached. + """ + autoCloseStream: Boolean! - """ - customCssUrl is the URL of the custom CSS used to display on the frontend. - """ - customCssUrl: String + """ + customCssUrl is the URL of the custom CSS used to display on the frontend. + """ + customCssUrl: String - """ - closedTimeout is the amount of seconds from the created_at timestamp that a - given asset will be considered closed. - """ - closedTimeout: Int! + """ + closedTimeout is the amount of seconds from the created_at timestamp that a + given asset will be considered closed. + """ + closedTimeout: Int! - """ - closedMessage is the message shown to the user when the given Asset is - closed. - """ - closedMessage: String + """ + closedMessage is the message shown to the user when the given Asset is + closed. + """ + closedMessage: String - """ - disableCommenting will disable commenting site-wide. - """ - disableCommenting: Boolean! + """ + disableCommenting will disable commenting site-wide. + """ + disableCommenting: Boolean! - """ - disableCommentingMessage will be shown above the comment stream while - commenting is disabled site-wide. - """ - disableCommentingMessage: String + """ + disableCommentingMessage will be shown above the comment stream while + commenting is disabled site-wide. + """ + disableCommentingMessage: String - """ - editCommentWindowLength is the length of time (in milliseconds) after a - comment is posted that it can still be edited by the author. - """ - editCommentWindowLength: Int! + """ + editCommentWindowLength is the length of time (in milliseconds) after a + comment is posted that it can still be edited by the author. + """ + editCommentWindowLength: Int! - """ - charCountEnable is true when the character count restriction is enabled. - """ - charCountEnable: Boolean! + """ + charCountEnable is true when the character count restriction is enabled. + """ + charCountEnable: Boolean! - """ - charCount is the maximum number of characters a comment may be. - """ - charCount: Int + """ + charCount is the maximum number of characters a comment may be. + """ + charCount: Int - """ - organizationName is the name of the organization. - """ - organizationName: String + """ + organizationName is the name of the organization. + """ + organizationName: String - """ - organizationContactEmail is the email of the organization. - """ - organizationContactEmail: String + """ + organizationContactEmail is the email of the organization. + """ + organizationContactEmail: String - """ - wordlist will return a given list of words. - """ - wordlist: Wordlist! + """ + wordlist will return a given list of words. + """ + wordlist: Wordlist! - """ - domains will return a given list of whitelisted domains. - """ - domains: [String!]! + """ + domains will return a given list of whitelisted domains. + """ + domains: [String!]! } ################################################################################ @@ -168,15 +168,15 @@ type Settings { User is someone that leaves Comments, and logs in. """ type User { - """ - id is the identifier of the User. - """ - id: ID! + """ + id is the identifier of the User. + """ + id: ID! - """ - username is the name of the User visible to other Users. - """ - username: String! + """ + username is the name of the User visible to other Users. + """ + username: String! } ################################################################################ @@ -184,95 +184,85 @@ type User { ################################################################################ enum COMMENT_STATUS { - NONE - ACCEPTED + NONE + ACCEPTED } """ Comment is a comment left by a User on an Asset or another Comment as a reply. """ type Comment { - """ - id is the identifier of the Comment. - """ - id: ID! + """ + id is the identifier of the Comment. + """ + id: ID! - """ - body is the content of the Comment. - """ - body: String + """ + body is the content of the Comment. + """ + body: String - """ - author is the User that authored the Comment. - """ - author: User + """ + author is the User that authored the Comment. + """ + author: User - """ - status represents the Comment's current Status. - """ - status: COMMENT_STATUS! + """ + status represents the Comment's current Status. + """ + status: COMMENT_STATUS! - """ - replyCount is the number of replies. Only direct replies to this Comment - are counted. Deleted comments are included in this count. - """ - replyCount: Int + """ + replyCount is the number of replies. Only direct replies to this Comment + are counted. Deleted comments are included in this count. + """ + replyCount: Int - """ - replies will return the replies to this comment. - """ - replies( - first: Int = 10 - orderBy: COMMENT_SORT = CREATED_AT_DESC - after: Cursor - ): CommentsConnection + """ + replies will return the replies to this comment. + """ + replies( + first: Int = 10 + orderBy: COMMENT_SORT = CREATED_AT_DESC + after: Cursor + ): CommentsConnection } type PageInfo { - """ - Cursor of first node in subset. - """ - startCursor: Cursor - - """ - Cursor of last node in subset. - """ - endCursor: Cursor - - """ - Indicates that there are more nodes after this subset. - """ - hasNextPage: Boolean! + """ + Indicates that there are more nodes after this subset. + """ + hasNextPage: Boolean! } """ CommentEdge represents a unique Comment in a CommentConnection. """ type CommentEdge { - """ - node is the Comment for this edge. - """ - node: Comment + """ + node is the Comment for this edge. + """ + node: Comment - """ + """ - """ - cursor: Cursor + """ + cursor: Cursor } """ CommentsConnection represents a subset of a comment list. """ type CommentsConnection { - """ - edges are a subset of CommentEdge's. - """ - edges: [CommentEdge!]! + """ + edges are a subset of CommentEdge's. + """ + edges: [CommentEdge!]! - """ - pageInfo is - """ - pageInfo: PageInfo! + """ + pageInfo is + """ + pageInfo: PageInfo! } ################################################################################ @@ -280,84 +270,89 @@ type CommentsConnection { ################################################################################ enum COMMENT_SORT { - CREATED_AT_DESC - CREATED_AT_ASC - REPLIES_DESC - RESPECT_DESC + CREATED_AT_DESC + CREATED_AT_ASC + REPLIES_DESC + RESPECT_DESC } """ Asset is an Article or Page where Comments are written on by Users. """ type Asset { - """ - id is the identifier of the Asset. - """ - id: ID! + """ + id is the identifier of the Asset. + """ + id: ID! - """ - url is the url that the Asset is located on. - """ - url: String! + """ + url is the url that the Asset is located on. + """ + url: String! - """ - title is the title of the scraped Asset. - """ - title: String + """ + title is the title of the scraped Asset. + """ + title: String - """ - comments are the comments on the Asset. - """ - comments( - first: Int = 10 - orderBy: COMMENT_SORT = CREATED_AT_DESC - after: Cursor - ): CommentsConnection + """ + comments are the comments on the Asset. + """ + comments( + first: Int = 10 + orderBy: COMMENT_SORT = CREATED_AT_DESC + after: Cursor + ): CommentsConnection - """ - author is the authors listed in the meta tags for the Asset. - """ - author: String + """ + author is the authors listed in the meta tags for the Asset. + """ + author: String - """ - closedAt is the Time that the Asset is closed for commenting. - """ - closedAt: Time + """ + closedAt is the Time that the Asset is closed for commenting. + """ + closedAt: Time - """ - isClosed returns true when the Asset is currently closed for commenting. - """ - isClosed: Boolean! + """ + isClosed returns true when the Asset is currently closed for commenting. + """ + isClosed: Boolean! - """ - createdAt is the date that the Asset was created at. - """ - createdAt: Time! + """ + createdAt is the date that the Asset was created at. + """ + createdAt: Time! +} + +""" +AssetEdge represents a unique Asset in a AssetConnection. +""" +type AssetEdge { + """ + node is the Asset for this edge. + """ + node: Asset + + """ + + """ + cursor: Cursor } """ AssetsConnection represents a subset of a Asset list. """ type AssetsConnection { - """ - Indicates that there are more Assets after this subset. - """ - hasNextPage: Boolean! + """ + edges are a subset of AssetEdge's. + """ + edges: [AssetEdge!]! - """ - Cursor of first Asset in subset. - """ - startCursor: Cursor - - """ - Cursor of last Asset in subset. - """ - endCursor: Cursor - - """ - Subset of Assets. - """ - nodes: [Asset!]! + """ + pageInfo is + """ + pageInfo: PageInfo! } ################################################################################ @@ -365,30 +360,30 @@ type AssetsConnection { ################################################################################ type Query { - """ - comment returns a specific comment. - """ - comment(id: ID!): Comment + """ + comment returns a specific comment. + """ + comment(id: ID!): Comment - """ - assets returns a AssetsConnection. - """ - assets(cursor: Cursor, limit: Int = 10): AssetsConnection + """ + assets returns a AssetsConnection. + """ + assets(cursor: Cursor, limit: Int = 10): AssetsConnection - """ - asset is the Asset specified by its ID. - """ - asset(id: ID!): Asset + """ + asset is the Asset specified by its ID. + """ + asset(id: ID!): Asset - """ - me is the current logged in User. - """ - me: User + """ + me is the current logged in User. + """ + me: User - """ - settings is the Settings for a given Tenant. - """ - settings: Settings! + """ + settings is the Settings for a given Tenant. + """ + settings: Settings! } ################################################################################ @@ -403,25 +398,25 @@ type Query { CreateCommentInput provides the input for the createComment Mutation. """ input CreateCommentInput { - """ - assetID is the ID of the Asset where we are creating a comment on. - """ - assetID: ID! + """ + assetID is the ID of the Asset where we are creating a comment on. + """ + assetID: ID! - """ - parentID is the optional ID of the Comment that we are replying to. - """ - parentID: ID + """ + parentID is the optional ID of the Comment that we are replying to. + """ + parentID: ID - """ - body is the Comment body, the content of the Comment. - """ - body: String! + """ + body is the Comment body, the content of the Comment. + """ + body: String! - """ - clientMutationId is required for Relay support. - """ - clientMutationId: String! + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! } """ @@ -429,15 +424,15 @@ CreateCommentPayload contains the created Comment after the createComment mutation. """ type CreateCommentPayload { - """ - comment is the possibly created comment. - """ - comment: Comment + """ + comment is the possibly created comment. + """ + comment: Comment - """ - clientMutationId is required for Relay support. - """ - clientMutationId: String! + """ + clientMutationId is required for Relay support. + """ + clientMutationId: String! } ################## @@ -445,10 +440,10 @@ type CreateCommentPayload { ################## type Mutation { - """ - createComment will create a Comment as the current logged in User. - """ - createComment(input: CreateCommentInput!): CreateCommentPayload + """ + createComment will create a Comment as the current logged in User. + """ + createComment(input: CreateCommentInput!): CreateCommentPayload } ################################################################################ @@ -456,5 +451,5 @@ type Mutation { ################################################################################ type Subscription { - commentCreated(assetID: ID!): Comment + commentCreated(assetID: ID!): Comment } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 5c44fbdca..fdf4f3aea 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -1,14 +1,14 @@ import express, { Express } from "express"; import http from "http"; +import getManagementSchema from "talk-server/graph/management/schema"; +import { Schemas } from "talk-server/graph/schemas"; +import getTenantSchema from "talk-server/graph/tenant/schema"; +import { attachSubscriptionHandlers, createApp, listenAndServe } from "./app"; import config, { Config } from "./config"; -import { createApp, listenAndServe, attachSubscriptionHandlers } from "./app"; import logger from "./logger"; import { createMongoDB } from "./services/mongodb"; import { createRedisClient } from "./services/redis"; -import getManagementSchema from "talk-server/graph/management/schema"; -import getTenantSchema from "talk-server/graph/tenant/schema"; -import { Schemas } from "talk-server/graph/schemas"; export interface ServerOptions { config?: Config; diff --git a/src/core/server/models/asset.ts b/src/core/server/models/asset.ts index 326c448a8..34e1df8ca 100644 --- a/src/core/server/models/asset.ts +++ b/src/core/server/models/asset.ts @@ -1,10 +1,10 @@ -import { Db, Collection } from "mongodb"; -import Query, { FilterQuery } from "./query"; -import { defaults } from "lodash"; -import uuid from "uuid"; -import { Omit } from "talk-common/types"; import dotize from "dotize"; +import { defaults } from "lodash"; +import { Collection, Db } from "mongodb"; +import { Omit } from "talk-common/types"; import { TenantResource } from "talk-server/models/tenant"; +import uuid from "uuid"; +import Query from "./query"; function collection(db: Db): Collection { return db.collection("assets"); @@ -29,7 +29,7 @@ export interface Asset extends TenantResource { export type CreateAssetInput = Pick; -export async function create( +export async function createAsset( db: Db, tenantID: string, input: CreateAssetInput @@ -69,7 +69,7 @@ export async function create( return result.value; } -export async function retrieve( +export async function retrieveAsset( db: Db, tenantID: string, id: string @@ -79,11 +79,11 @@ export async function retrieve( .findOne({ id, tenant_id: tenantID }); } -export async function retrieveMany( +export async function retrieveManyAssets( db: Db, tenantID: string, ids: string[] -): Promise> { +): Promise { const cursor = await db .collection("assets") .find({ id: { $in: ids }, tenant_id: tenantID }); @@ -98,7 +98,7 @@ export type UpdateAssetInput = Omit< "id" | "tenant_id" | "url" | "created_at" >; -export async function update( +export async function updateAsset( db: Db, tenantID: string, id: string, diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index cb5bb09b9..951fcee97 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -1,11 +1,11 @@ -import { Db, Collection } from "mongodb"; -import { Omit, Sub } from "talk-common/types"; import { merge } from "lodash"; -import uuid from "uuid"; -import { Connection, Edge, Cursor } from "talk-server/models/connection"; -import Query from "talk-server/models/query"; +import { Collection, Db } from "mongodb"; +import { Omit, Sub } from "talk-common/types"; import { ActionCounts } from "talk-server/models/actions"; +import { Connection, Cursor, Edge } from "talk-server/models/connection"; +import Query from "talk-server/models/query"; import { TenantResource } from "talk-server/models/tenant"; +import uuid from "uuid"; function collection(db: Db): Collection { return db.collection("comments"); @@ -116,7 +116,7 @@ export async function retrieveMany( db: Db, tenantID: string, ids: string[] -): Promise[]> { +): Promise>> { const cursor = await collection(db).find({ id: { $in: ids, @@ -152,7 +152,7 @@ export interface ConnectionInput { function nodesToEdge( input: ConnectionInput, nodes: Comment[] -): Edge[] { +): Array> { let getCursor: (comment: Comment, index: number) => Cursor; switch (input.orderBy) { case CommentSort.CREATED_AT_DESC: diff --git a/src/core/server/models/connection.ts b/src/core/server/models/connection.ts index 9e0b87602..eb687c255 100644 --- a/src/core/server/models/connection.ts +++ b/src/core/server/models/connection.ts @@ -10,6 +10,6 @@ export interface PageInfo { } export interface Connection { - edges: Edge[]; + edges: Array>; pageInfo: PageInfo; } diff --git a/src/core/server/models/query.ts b/src/core/server/models/query.ts index 3d61e78e1..6e3200d70 100644 --- a/src/core/server/models/query.ts +++ b/src/core/server/models/query.ts @@ -20,7 +20,7 @@ export default class Query { private collection: Collection; private skip?: number; private limit?: number; - private sort?: Object; + private sort?: object; constructor(collection: Collection) { this.collection = collection; @@ -61,7 +61,7 @@ export default class Query { * * @param sort the sorting option for the documents */ - public orderBy(sort: Object): Query { + public orderBy(sort: object): Query { this.sort = merge({}, this.sort || {}, sort); return this; } @@ -69,7 +69,7 @@ export default class Query { /** * exec will return a cursor to the query. */ - async exec(): Promise> { + public async exec(): Promise> { let cursor = await this.collection.find(this.filter); if (this.limit) { diff --git a/src/core/server/models/tenant.ts b/src/core/server/models/tenant.ts index 39e6b1e87..11a2fac04 100644 --- a/src/core/server/models/tenant.ts +++ b/src/core/server/models/tenant.ts @@ -1,8 +1,8 @@ -import { Db, Collection } from "mongodb"; -import { merge } from "lodash"; import dotize from "dotize"; +import { merge } from "lodash"; +import { Collection, Db } from "mongodb"; +import { Sub } from "talk-common/types"; import uuid from "uuid"; -import { Omit, Sub } from "talk-common/types"; function collection(db: Db): Collection { return db.collection("tenants"); @@ -75,7 +75,7 @@ export type CreateTenantInput = Pick< * @param db the MongoDB connection used to create the tenant. * @param input the customizable parts of the Tenant available during creation */ -export async function create( +export async function createTenant( db: Db, input: CreateTenantInput ): Promise> { @@ -113,7 +113,7 @@ export async function create( return tenant; } -export async function retrieveByDomain( +export async function retrieveTenantByDomain( db: Db, domain: string ): Promise> { @@ -124,10 +124,10 @@ export async function retrieve(db: Db, id: string): Promise> { return collection(db).findOne({ id }); } -export async function retrieveMany( +export async function retrieveManyTenants( db: Db, ids: string[] -): Promise[]> { +): Promise>> { const cursor = await collection(db).find({ id: { $in: ids, @@ -139,10 +139,10 @@ export async function retrieveMany( return ids.map(id => tenants.find(tenant => tenant.id === id)); } -export async function retrieveManyByDomain( +export async function retrieveManyTenantsByDomain( db: Db, domains: string[] -): Promise[]> { +): Promise>> { const cursor = await collection(db).find({ domain: { $in: domains, @@ -156,13 +156,15 @@ export async function retrieveManyByDomain( ); } -export async function retrieveAll(db: Db): Promise[]> { +export async function retrieveAllTenants( + db: Db +): Promise>> { return collection(db) .find({}) .toArray(); } -export async function update( +export async function updateTenant( db: Db, id: string, update: Partial diff --git a/src/core/server/models/user.ts b/src/core/server/models/user.ts index 6831c905e..fba955e24 100644 --- a/src/core/server/models/user.ts +++ b/src/core/server/models/user.ts @@ -1,9 +1,9 @@ -import { ActionCounts } from "talk-server/models/actions"; -import { Db, Collection } from "mongodb"; -import uuid from "uuid"; -import { Omit, Sub } from "talk-common/types"; import { merge } from "lodash"; +import { Collection, Db } from "mongodb"; +import { Omit, Sub } from "talk-common/types"; +import { ActionCounts } from "talk-server/models/actions"; import { TenantResource } from "talk-server/models/tenant"; +import uuid from "uuid"; function collection(db: Db): Collection { return db.collection("users"); @@ -61,7 +61,7 @@ export interface UserStatusHistory { export interface UserStatusItem { status: T; // TODO: migrate field - history: UserStatusHistory[]; + history: Array>; } export interface UserStatus { @@ -152,7 +152,7 @@ export async function retrieveMany( db: Db, tenantID: string, ids: string[] -): Promise[]> { +): Promise>> { const cursor = await collection(db).find({ id: { $in: ids, diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 511fb9a84..95a9001cb 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -1,6 +1,29 @@ import { Db } from "mongodb"; -import { Comment } from "talk-server/models/comment"; -export async function create(db: Db): Promise { - return null; +import { Omit } from "talk-common/types"; +import { + Comment, + CommentStatus, + create as createComment, + CreateCommentInput, +} from "talk-server/models/comment"; + +export type CreateComment = Omit< + CreateCommentInput, + "status" | "action_counts" +>; + +export async function create( + db: Db, + tenantID: string, + input: CreateComment +): Promise { + // TODO: run the comment through the moderation phases. + const comment = await createComment(db, tenantID, { + status: CommentStatus.ACCEPTED, + action_counts: {}, + ...input, + }); + + return comment; } diff --git a/src/core/server/services/mongodb/index.ts b/src/core/server/services/mongodb/index.ts index f9e9e3d6b..efaf8bcba 100644 --- a/src/core/server/services/mongodb/index.ts +++ b/src/core/server/services/mongodb/index.ts @@ -1,4 +1,4 @@ -import { MongoClient, Db } from "mongodb"; +import { Db, MongoClient } from "mongodb"; import { Config } from "talk-server/config"; /** diff --git a/src/core/server/services/tenant/cache.ts b/src/core/server/services/tenant/cache.ts index 990c36a2d..eb86d384a 100644 --- a/src/core/server/services/tenant/cache.ts +++ b/src/core/server/services/tenant/cache.ts @@ -1,8 +1,12 @@ -import { Db } from "mongodb"; -import { Redis } from "ioredis"; import DataLoader from "dataloader"; +import { Redis } from "ioredis"; +import { Db } from "mongodb"; -import { Tenant, retrieveAll, retrieveMany } from "talk-server/models/tenant"; +import { + retrieveAllTenants, + retrieveManyTenants, + Tenant, +} from "talk-server/models/tenant"; const CacheUpdateChannel = "tenant"; @@ -18,7 +22,7 @@ export default class Cache { this.db = db; // Prepare the list of all tenant's maintained by this instance. - this.tenants = new DataLoader(ids => retrieveMany(db, ids)); + this.tenants = new DataLoader(ids => retrieveManyTenants(db, ids)); // Subscribe to tenant notifications. subscriber.subscribe(CacheUpdateChannel); @@ -33,7 +37,7 @@ export default class Cache { */ public async primeAll() { // Grab all the tenants for this node. - const tenants = await retrieveAll(this.db); + const tenants = await retrieveAllTenants(this.db); // Clear out all the items in the cache. this.tenants.clearAll(); diff --git a/src/index.ts b/src/index.ts index 421c57eba..ec3479156 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,9 @@ -import createTalk from "./core"; import express from "express"; +import logger from "talk-server/logger"; + +import createTalk from "./core"; + // Create the app that will serve as the mounting point for the Talk Server. const app = express(); @@ -11,7 +14,9 @@ async function bootstrap() { // Start the server. await server.start(app); - } catch (err) {} + } catch (err) { + logger.error({ err }, "can not bootstrap server"); + } } bootstrap(); diff --git a/src/types/passport.d.ts b/src/types/passport.d.ts new file mode 100644 index 000000000..673c8f61f --- /dev/null +++ b/src/types/passport.d.ts @@ -0,0 +1,7 @@ +import { User } from "talk-server/models/user"; + +declare module "express" { + interface Request { + user?: User; + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 000000000..d17f3c4fd --- /dev/null +++ b/tslint.json @@ -0,0 +1,14 @@ +{ + "extends": [ + "tslint:recommended", + "tslint-config-prettier", + "tslint-plugin-prettier" + ], + "rules": { + "prettier": true, + "object-literal-sort-keys": false, + "interface-name": [true, "never-prefix"], + "no-switch-case-fall-through": true, + "member-ordering": false + } +}