diff --git a/.editorconfig b/.editorconfig index 3b0653aca..674907205 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,8 +2,14 @@ root = true [*] indent_style = space -indent_size = 4 +indent_size = 2 end_of_line = lf charset = utf-8 -trim_trailing_whitespace = false -insert_final_newline = false \ No newline at end of file +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +insert_final_newline = false + +[*.ts] +max_line_length = 80 diff --git a/.prettierrc.json b/.prettierrc.json index 627d6c57c..1b016bffe 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,3 @@ { - "singleQuote": true, "trailingComma": "es5" } \ No newline at end of file diff --git a/package.json b/package.json index ad3248aa7..be8a2c554 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,55 @@ { - "name": "@coralproject/talk", - "version": "1.0.0", - "description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.", - "scripts": { - "start": "node dist/index.js", - "build": "tsc", - "watch": "nodemon --config .nodemonrc.json src/index.ts", - "lint": "prettier --write src/**/*.ts" - }, - "author": "", - "license": "Apache-2.0", - "dependencies": { - "apollo-server-express": "^1.3.6", - "bunyan": "^1.8.12", - "convict": "^4.3.0", - "dataloader": "^1.4.0", - "dotenv": "^6.0.0", - "dotize": "^0.2.0", - "express": "^4.16.3", - "express-static-gzip": "^0.3.2", - "graphql": "^0.13.2", - "graphql-config": "^2.0.1", - "graphql-redis-subscriptions": "^1.5.0", - "graphql-tools": "^3.0.2", - "ioredis": "^3.2.2", - "joi": "^13.4.0", - "lodash": "^4.17.10", - "luxon": "^1.2.1", - "mongodb": "^3.0.10", - "performance-now": "^2.1.0", - "subscriptions-transport-ws": "^0.9.11", - "uuid": "^3.2.1" - }, - "devDependencies": { - "@types/bunyan": "^1.8.4", - "@types/convict": "^4.2.0", - "@types/dotenv": "^4.0.3", - "@types/express": "^4.16.0", - "@types/graphql": "^0.13.1", - "@types/ioredis": "^3.2.8", - "@types/joi": "^13.0.8", - "@types/lodash": "^4.14.109", - "@types/luxon": "^0.5.3", - "@types/mongodb": "^3.0.19", - "@types/uuid": "^3.4.3", - "@types/ws": "^5.1.2", - "graphql-playground-middleware-express": "^1.7.0", - "nodemon": "^1.17.5", - "prettier": "^1.13.4", - "ts-node": "^6.1.1", - "tsconfig-paths": "^3.4.0", - "typescript": "^2.9.1" - } -} + "name": "@coralproject/talk", + "version": "1.0.0", + "description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.", + "scripts": { + "start": "node dist/index.js", + "build": "tsc", + "watch": "nodemon --config .nodemonrc.json src/index.ts", + "lint": "prettier --write \"src/**/*.ts\"" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "apollo-server-express": "^1.3.6", + "bunyan": "^1.8.12", + "convict": "^4.3.0", + "dataloader": "^1.4.0", + "dotenv": "^6.0.0", + "dotize": "^0.2.0", + "express": "^4.16.3", + "express-static-gzip": "^0.3.2", + "graphql": "^0.13.2", + "graphql-config": "^2.0.1", + "graphql-redis-subscriptions": "^1.5.0", + "graphql-tools": "^3.0.2", + "ioredis": "^3.2.2", + "joi": "^13.4.0", + "lodash": "^4.17.10", + "luxon": "^1.2.1", + "mongodb": "^3.0.10", + "performance-now": "^2.1.0", + "subscriptions-transport-ws": "^0.9.11", + "uuid": "^3.2.1" + }, + "devDependencies": { + "@types/bunyan": "^1.8.4", + "@types/convict": "^4.2.0", + "@types/dotenv": "^4.0.3", + "@types/express": "^4.16.0", + "@types/graphql": "^0.13.1", + "@types/ioredis": "^3.2.8", + "@types/joi": "^13.0.8", + "@types/lodash": "^4.14.109", + "@types/luxon": "^0.5.3", + "@types/mongodb": "^3.0.19", + "@types/uuid": "^3.4.3", + "@types/ws": "^5.1.2", + "graphql-playground-middleware-express": "^1.7.0", + "nodemon": "^1.17.5", + "prettier": "^1.13.4", + "ts-node": "^6.1.1", + "tsconfig-paths": "^3.4.0", + "typescript": "^2.9.1" + } +} \ No newline at end of file diff --git a/src/core/common/types.ts b/src/core/common/types.ts index 8f38d00d2..2d322e1d3 100644 --- a/src/core/common/types.ts +++ b/src/core/common/types.ts @@ -1,5 +1,5 @@ export type Diff = ({ [P in T]: P } & - { [P in U]: never } & { [x: string]: never })[T]; + { [P in U]: never } & { [x: string]: never })[T]; export type Omit = Pick>; diff --git a/src/core/index.ts b/src/core/index.ts index 879e5fd66..097e43ba1 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,4 @@ -import Server, { ServerOptions } from './server'; +import Server, { ServerOptions } from "./server"; /** * Create a Talk Server. @@ -6,8 +6,8 @@ import Server, { ServerOptions } from './server'; * @param options ServerOptions that will be used to configure Talk. */ export default async function createTalk( - options: ServerOptions = {} + options: ServerOptions = {} ): Promise { - // Create the server with the provided options. - return new Server(options); + // Create the server with the provided options. + return new Server(options); } diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 3d9ff8036..7d7d34821 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -1,47 +1,47 @@ -import { Express } from 'express'; -import { Db } from 'mongodb'; -import http from 'http'; -import { Redis } from 'ioredis'; +import { Express } from "express"; +import { Db } from "mongodb"; +import http from "http"; +import { Redis } from "ioredis"; -import { Config } from 'talk-server/config'; -import { Schemas } from 'talk-server/graph/schemas'; -import { handleSubscriptions } from 'talk-server/graph/common/subscriptions/middleware'; +import { Config } from "talk-server/config"; +import { Schemas } from "talk-server/graph/schemas"; +import { handleSubscriptions } from "talk-server/graph/common/subscriptions/middleware"; -import { createRouter } from './router'; -import serveStatic from './middleware/serveStatic'; +import { createRouter } from "./router"; +import serveStatic from "./middleware/serveStatic"; import { - access as accessLogger, - error as errorLogger, -} from './middleware/logging'; + access as accessLogger, + error as errorLogger, +} from "./middleware/logging"; export interface AppOptions { - parent: Express; - config: Config; - mongo: Db; - redis: Redis; - schemas: Schemas; + parent: Express; + config: Config; + mongo: Db; + redis: Redis; + schemas: Schemas; } /** * createApp will create a Talk Express app that can be used to handle requests. */ export async function createApp(options: AppOptions): Promise { - // Pull the parent out of the options. - const { parent } = options; + // Pull the parent out of the options. + const { parent } = options; - // Logging - parent.use(accessLogger); + // Logging + parent.use(accessLogger); - // Static Files - parent.use(serveStatic); + // Static Files + parent.use(serveStatic); - // Mount the router. - parent.use(await createRouter(options)); + // Mount the router. + parent.use(await createRouter(options)); - // Error Handling - parent.use(errorLogger); + // Error Handling + parent.use(errorLogger); - return parent; + return parent; } /** @@ -51,13 +51,13 @@ export async function createApp(options: AppOptions): Promise { * @param port the port to listen on */ export const listenAndServe = ( - app: Express, - port: number + app: Express, + port: number ): Promise => - new Promise(async resolve => { - // Listen on the designated port. - const httpServer = app.listen(port, () => resolve(httpServer)); - }); + new Promise(async resolve => { + // Listen on the designated port. + const httpServer = app.listen(port, () => resolve(httpServer)); + }); /** * attachSubscriptionHandlers attaches all the handlers to the http.Server to @@ -67,18 +67,18 @@ export const listenAndServe = ( * @param server the http.Server to attach the websocket upgraders to */ export async function attachSubscriptionHandlers( - schemas: Schemas, - server: http.Server + schemas: Schemas, + server: http.Server ) { - // Setup the Management Subscription endpoint. - handleSubscriptions(server, { - schema: schemas.management, - path: '/api/management/live', - }); + // Setup the Management Subscription endpoint. + handleSubscriptions(server, { + schema: schemas.management, + path: "/api/management/live", + }); - // Setup the Tenant Subscription endpoint. - handleSubscriptions(server, { - schema: schemas.tenant, - path: '/api/tenant/live', - }); + // Setup the Tenant Subscription endpoint. + handleSubscriptions(server, { + schema: schemas.tenant, + path: "/api/tenant/live", + }); } diff --git a/src/core/server/app/middleware/logging.ts b/src/core/server/app/middleware/logging.ts index 98916f7d0..c4e317e27 100644 --- a/src/core/server/app/middleware/logging.ts +++ b/src/core/server/app/middleware/logging.ts @@ -1,6 +1,6 @@ -import { RequestHandler, ErrorRequestHandler } from 'express'; -import logger from '../../logger'; -import now from 'performance-now'; +import { RequestHandler, ErrorRequestHandler } from "express"; +import logger from "../../logger"; +import now from "performance-now"; export const access: RequestHandler = (req, res, next) => { const startTime = now(); @@ -10,11 +10,11 @@ export const access: RequestHandler = (req, res, next) => { const responseTime = Math.round(now() - startTime); // Get some extra goodies from the request. - const userAgent = req.get('User-Agent'); + const userAgent = req.get("User-Agent"); // Reattach the old end, and finish. res.end = end; - if (typeof encodingOrCb === 'function') { + if (typeof encodingOrCb === "function") { res.end(chunk, encodingOrCb); } else { res.end(chunk, encodingOrCb, cb); @@ -30,7 +30,7 @@ export const access: RequestHandler = (req, res, next) => { userAgent, responseTime, }, - 'http request' + "http request" ); }; @@ -38,6 +38,6 @@ export const access: RequestHandler = (req, res, next) => { }; export const error: ErrorRequestHandler = (err, req, res, next) => { - logger.error({ err }, 'http error'); + logger.error({ err }, "http error"); next(err); }; diff --git a/src/core/server/app/middleware/playground.ts b/src/core/server/app/middleware/playground.ts index a504b23e2..1873a0537 100644 --- a/src/core/server/app/middleware/playground.ts +++ b/src/core/server/app/middleware/playground.ts @@ -1,4 +1,4 @@ -import { MiddlewareOptions } from 'graphql-playground-html'; -import playground from 'graphql-playground-middleware-express'; +import { MiddlewareOptions } from "graphql-playground-html"; +import playground from "graphql-playground-middleware-express"; export default (options: MiddlewareOptions) => playground(options); diff --git a/src/core/server/app/middleware/serveStatic.ts b/src/core/server/app/middleware/serveStatic.ts index 0e156c061..c69e599bb 100644 --- a/src/core/server/app/middleware/serveStatic.ts +++ b/src/core/server/app/middleware/serveStatic.ts @@ -1,4 +1,4 @@ -import serveStatic from 'express-static-gzip'; -import path from 'path'; +import serveStatic from "express-static-gzip"; +import path from "path"; -export default serveStatic(path.join(__dirname, '..', '..', 'dist'), {}); +export default serveStatic(path.join(__dirname, "..", "..", "dist"), {}); diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index 6c9db8a08..753fa06ac 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -1,85 +1,81 @@ -import express, { Router } from 'express'; +import express, { Router } from "express"; -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 managementGraphMiddleware from "talk-server/graph/management/middleware"; -import { AppOptions } from './index'; -import playground from './middleware/playground'; +import { AppOptions } from "./index"; +import playground from "./middleware/playground"; async function createManagementRouter(opts: AppOptions) { - const router = express.Router(); + 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', - express.json(), - await managementGraphMiddleware( - opts.schemas.management, - opts.config, - opts.mongo - ) + if (opts.config.get("env") === "development") { + // GraphiQL + router.get( + "/graphiql", + playground({ + endpoint: "/api/management/graphql", + subscriptionEndpoint: "/api/management/live", + }) ); + } - return router; + // Management API + router.use( + "/graphql", + express.json(), + await managementGraphMiddleware( + opts.schemas.management, + opts.config, + opts.mongo + ) + ); + + return router; } async function createTenantRouter(opts: AppOptions) { - const router = express.Router(); + 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', - express.json(), - await tenantGraphMiddleware( - opts.schemas.tenant, - opts.config, - opts.mongo - ) + if (opts.config.get("env") === "development") { + // GraphiQL + router.get( + "/graphiql", + playground({ + endpoint: "/api/tenant/graphql", + subscriptionEndpoint: "/api/tenant/live", + }) ); + } - return router; + // Tenant API + router.use( + "/graphql", + express.json(), + await tenantGraphMiddleware(opts.schemas.tenant, opts.config, opts.mongo) + ); + + return router; } async function createAPIRouter(opts: AppOptions) { - // Create a router. - const router = express.Router(); + // Create a router. + const router = express.Router(); - // Configure the tenant routes. - router.use('/tenant', await createTenantRouter(opts)); + // Configure the tenant routes. + router.use("/tenant", await createTenantRouter(opts)); - // Configure the management routes. - router.use('/management', await createManagementRouter(opts)); + // Configure the management routes. + router.use("/management", await createManagementRouter(opts)); - return router; + return router; } export async function createRouter(opts: AppOptions) { - // Create a router. - const router = express.Router(); + // Create a router. + const router = express.Router(); - router.use('/api', await createAPIRouter(opts)); + router.use("/api", await createAPIRouter(opts)); - return router; + return router; } diff --git a/src/core/server/config.ts b/src/core/server/config.ts index fd40913fb..a6dcedc2b 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"; +import convict from "convict"; +import dotenv from "dotenv"; // Apply all the configuration provided in the .env file if it isn't already in // the environment. @@ -8,72 +8,72 @@ dotenv.config(); // Add custom format for the mongo uri scheme. convict.addFormat({ - name: 'mongo-uri', - validate: (url: string) => { - Joi.assert( - url, - Joi.string().uri({ - scheme: ['mongodb'], - }) - ); - }, + name: "mongo-uri", + validate: (url: string) => { + Joi.assert( + url, + Joi.string().uri({ + scheme: ["mongodb"], + }) + ); + }, }); // Add custom format for the redis uri scheme. convict.addFormat({ - name: 'redis-uri', - validate: (url: string) => { - Joi.assert( - url, - Joi.string().uri({ - scheme: ['redis'], - }) - ); - }, + name: "redis-uri", + validate: (url: string) => { + Joi.assert( + url, + Joi.string().uri({ + scheme: ["redis"], + }) + ); + }, }); const config = convict({ - env: { - doc: 'The application environment.', - format: ['production', 'development', 'test'], - default: 'development', - env: 'NODE_ENV', - }, - port: { - doc: 'The port to bind.', - format: 'port', - default: 3000, - env: 'PORT', - arg: 'port', - }, - mongodb: { - doc: 'The MongoDB database to connect to.', - format: 'mongo-uri', - default: 'mongodb://localhost/talk', - env: 'MONGODB', - arg: 'mongodb', - }, - redis: { - doc: 'The Redis database to connect to.', - format: 'redis-uri', - default: 'redis://localhost:6379', - env: 'REDIS', - arg: 'redis', - }, - secret: { - doc: 'The secret used to sign and verify JWTs', - format: '*', - default: null, - env: 'SECRET', - arg: 'secret', - }, - logging_level: { - doc: 'The logging level to print to the console', - format: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'], - default: 'info', - env: 'LOGGING_LEVEL', - arg: 'logging', - }, + env: { + doc: "The application environment.", + format: ["production", "development", "test"], + default: "development", + env: "NODE_ENV", + }, + port: { + doc: "The port to bind.", + format: "port", + default: 3000, + env: "PORT", + arg: "port", + }, + mongodb: { + doc: "The MongoDB database to connect to.", + format: "mongo-uri", + default: "mongodb://localhost/talk", + env: "MONGODB", + arg: "mongodb", + }, + redis: { + doc: "The Redis database to connect to.", + format: "redis-uri", + default: "redis://localhost:6379", + env: "REDIS", + arg: "redis", + }, + secret: { + doc: "The secret used to sign and verify JWTs", + format: "*", + default: null, + env: "SECRET", + arg: "secret", + }, + logging_level: { + doc: "The logging level to print to the console", + format: ["fatal", "error", "warn", "info", "debug", "trace"], + default: "info", + env: "LOGGING_LEVEL", + arg: "logging", + }, }); export type Config = typeof config; diff --git a/src/core/server/graph/common/middleware/index.ts b/src/core/server/graph/common/middleware/index.ts index c3d0a6a24..0d9af1264 100644 --- a/src/core/server/graph/common/middleware/index.ts +++ b/src/core/server/graph/common/middleware/index.ts @@ -1,52 +1,48 @@ import { - graphqlExpress, - ExpressGraphQLOptionsFunction, - GraphQLOptions, -} from 'apollo-server-express'; -import { GraphQLError, FieldDefinitionNode, ValidationContext } from 'graphql'; -import { resolveGraphqlOptions } from 'apollo-server-core'; -import { Config } from 'talk-server/config'; + graphqlExpress, + ExpressGraphQLOptionsFunction, + GraphQLOptions, +} from "apollo-server-express"; +import { GraphQLError, FieldDefinitionNode, ValidationContext } from "graphql"; +import { resolveGraphqlOptions } from "apollo-server-core"; +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 const NoIntrospection = (context: ValidationContext) => ({ - Field(node: FieldDefinitionNode) { - if (node.name.value === '__schema' || node.name.value === '__type') { - context.reportError( - new GraphQLError( - 'GraphQL introspection is not allowed in production, but the query contained __schema or __type.', - [node] - ) - ); - } - }, + Field(node: FieldDefinitionNode) { + if (node.name.value === "__schema" || node.name.value === "__type") { + context.reportError( + new GraphQLError( + "GraphQL introspection is not allowed in production, but the query contained __schema or __type.", + [node] + ) + ); + } + }, }); export const graphqlMiddleware = ( - config: Config, - baseOptions: GraphQLOptions | ExpressGraphQLOptionsFunction + config: Config, + baseOptions: GraphQLOptions | ExpressGraphQLOptionsFunction ) => { - // Generate the validation rules. - const validationRules: Array<(context: ValidationContext) => any> = []; + // Generate the validation rules. + const validationRules: Array<(context: ValidationContext) => any> = []; - if (config.get('env') !== 'production') { - // Disable introspection in production. - validationRules.push(NoIntrospection); - } + if (config.get("env") !== "production") { + // Disable introspection in production. + validationRules.push(NoIntrospection); + } - // Generate the actual middleware. - return graphqlExpress(async (req, res) => { - // Resolve the base options. - const requestOptions = await resolveGraphqlOptions( - baseOptions, - req, - res - ); + // Generate the actual middleware. + return graphqlExpress(async (req, res) => { + // Resolve the base options. + const requestOptions = await resolveGraphqlOptions(baseOptions, req, res); - // Apply the validators, sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L104-L107 - requestOptions.validationRules = requestOptions.validationRules - ? requestOptions.validationRules.concat(validationRules) - : validationRules; + // Apply the validators, sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L104-L107 + requestOptions.validationRules = requestOptions.validationRules + ? requestOptions.validationRules.concat(validationRules) + : validationRules; - return requestOptions; - }); + return requestOptions; + }); }; diff --git a/src/core/server/graph/common/scalars/cursor.ts b/src/core/server/graph/common/scalars/cursor.ts index 9847635dc..92b584a20 100644 --- a/src/core/server/graph/common/scalars/cursor.ts +++ b/src/core/server/graph/common/scalars/cursor.ts @@ -1,72 +1,72 @@ -import { DateTime } from 'luxon'; -import { GraphQLScalarType } from 'graphql'; -import { Kind } from 'graphql/language'; -import { Cursor } from 'talk-server/models/connection'; +import { DateTime } from "luxon"; +import { GraphQLScalarType } from "graphql"; +import { Kind } from "graphql/language"; +import { Cursor } from "talk-server/models/connection"; function parseIntegerCursor(value: string): number { - try { - const cursor = parseInt(value); + try { + const cursor = parseInt(value); - return cursor; - } catch (err) { - return null; - } + return cursor; + } catch (err) { + return null; + } } function parseCursor(value: string): Cursor { - if (value.endsWith('Z')) { - const date = DateTime.fromISO(value, {}); - if (!date.isValid) { - return parseIntegerCursor(value); - } - - return date.toJSDate(); + if (value.endsWith("Z")) { + const date = DateTime.fromISO(value, {}); + if (!date.isValid) { + return parseIntegerCursor(value); } - return parseIntegerCursor(value); + return date.toJSDate(); + } + + return parseIntegerCursor(value); } export default new GraphQLScalarType({ - name: 'Cursor', - description: 'Cursor represents a paginating cursor.', - serialize(value) { - switch (typeof value) { - case 'object': - if (value instanceof Date) { - return value.toISOString(); - } else if (value instanceof DateTime) { - return value.toISO(); - } - - return null; - case 'number': - return value.toString(); - case 'string': - return value; - default: - return null; - } - }, - parseValue(value) { - if (typeof value === 'string') { - return parseCursor(value); + name: "Cursor", + description: "Cursor represents a paginating cursor.", + serialize(value) { + switch (typeof value) { + case "object": + if (value instanceof Date) { + return value.toISOString(); + } else if (value instanceof DateTime) { + return value.toISO(); } return null; - }, - parseLiteral(ast) { - switch (ast.kind) { - case Kind.STRING: - // This handles an empty string. - if (!ast.value || ast.value.length === 0) { - return null; - } + case "number": + return value.toString(); + case "string": + return value; + default: + return null; + } + }, + parseValue(value) { + if (typeof value === "string") { + return parseCursor(value); + } - return parseCursor(ast.value); - case Kind.INT: - return parseIntegerCursor(ast.value); - default: - return null; + return null; + }, + parseLiteral(ast) { + switch (ast.kind) { + case Kind.STRING: + // This handles an empty string. + if (!ast.value || ast.value.length === 0) { + return null; } - }, + + return parseCursor(ast.value); + case Kind.INT: + return parseIntegerCursor(ast.value); + default: + return null; + } + }, }); diff --git a/src/core/server/graph/common/schema/index.ts b/src/core/server/graph/common/schema/index.ts index 222599952..e2c859d03 100644 --- a/src/core/server/graph/common/schema/index.ts +++ b/src/core/server/graph/common/schema/index.ts @@ -1,15 +1,15 @@ -import { addResolveFunctionsToSchema, IResolvers } from 'graphql-tools'; -import { getGraphQLProjectConfig } from 'graphql-config'; +import { addResolveFunctionsToSchema, IResolvers } from "graphql-tools"; +import { getGraphQLProjectConfig } from "graphql-config"; export default function loadSchema(projectName: string, resolvers: IResolvers) { - // Load the configuration from the provided `.graphqlconfig` file. - const config = getGraphQLProjectConfig(__dirname, projectName); + // Load the configuration from the provided `.graphqlconfig` file. + const config = getGraphQLProjectConfig(__dirname, projectName); - // Get the GraphQLSchema from the configuration. - const schema = config.getSchema(); + // Get the GraphQLSchema from the configuration. + const schema = config.getSchema(); - // Attach the resolvers to the schema. - addResolveFunctionsToSchema({ schema, resolvers }); + // Attach the resolvers to the schema. + addResolveFunctionsToSchema({ schema, resolvers }); - return schema; + return schema; } diff --git a/src/core/server/graph/common/subscriptions/middleware.ts b/src/core/server/graph/common/subscriptions/middleware.ts index a1bc2d921..726a8c2b2 100644 --- a/src/core/server/graph/common/subscriptions/middleware.ts +++ b/src/core/server/graph/common/subscriptions/middleware.ts @@ -1,29 +1,29 @@ -import http from 'http'; -import { SubscriptionServer } from 'subscriptions-transport-ws'; -import { GraphQLSchema, execute, subscribe } from 'graphql'; +import http from "http"; +import { SubscriptionServer } from "subscriptions-transport-ws"; +import { GraphQLSchema, execute, subscribe } from "graphql"; export interface SubscriptionMiddlewareOptions { - schema: GraphQLSchema; - path: string; + schema: GraphQLSchema; + path: string; } export function handleSubscriptions( - server: http.Server, - { schema, path }: SubscriptionMiddlewareOptions + server: http.Server, + { schema, path }: SubscriptionMiddlewareOptions ): SubscriptionServer { - // Configure some options for the subscription system. - const options = { - schema, - execute, - subscribe, - }; + // Configure some options for the subscription system. + const options = { + schema, + execute, + subscribe, + }; - // Configure the socket options for the websocket server. It needs to handle - // upgrade requests on that route. - const socketOption = { - server, - path, - }; + // Configure the socket options for the websocket server. It needs to handle + // upgrade requests on that route. + const socketOption = { + server, + path, + }; - return new SubscriptionServer(options, socketOption); + return new SubscriptionServer(options, socketOption); } diff --git a/src/core/server/graph/common/subscriptions/pubsub.ts b/src/core/server/graph/common/subscriptions/pubsub.ts index f858f510f..62c9b44e5 100644 --- a/src/core/server/graph/common/subscriptions/pubsub.ts +++ b/src/core/server/graph/common/subscriptions/pubsub.ts @@ -1,15 +1,15 @@ -import { RedisPubSub } from 'graphql-redis-subscriptions'; -import { createRedisClient } from 'talk-server/services/redis'; -import { Config } from 'talk-server/config'; +import { RedisPubSub } from "graphql-redis-subscriptions"; +import { createRedisClient } from "talk-server/services/redis"; +import { Config } from "talk-server/config"; export async function createPubSub(config: Config): Promise { - // Create the Redis clients for the PubSub server. - const publisher = await createRedisClient(config); - const subscriber = await createRedisClient(config); + // Create the Redis clients for the PubSub server. + const publisher = await createRedisClient(config); + const subscriber = await createRedisClient(config); - // Create the new PubSub manager. - return new RedisPubSub({ - publisher, - subscriber, - }); + // Create the new PubSub manager. + return new RedisPubSub({ + publisher, + subscriber, + }); } diff --git a/src/core/server/graph/management/context.ts b/src/core/server/graph/management/context.ts index 061626ae8..dda52cb5b 100644 --- a/src/core/server/graph/management/context.ts +++ b/src/core/server/graph/management/context.ts @@ -1,13 +1,13 @@ -import { Db } from 'mongodb'; +import { Db } from "mongodb"; export interface ManagementContextOptions { - db: Db; + db: Db; } export default class ManagementContext { - public db: Db; + public db: Db; - constructor({ db }: ManagementContextOptions) { - this.db = db; - } + constructor({ db }: ManagementContextOptions) { + this.db = db; + } } diff --git a/src/core/server/graph/management/middleware.ts b/src/core/server/graph/management/middleware.ts index d33a6f4cd..b547b8380 100644 --- a/src/core/server/graph/management/middleware.ts +++ b/src/core/server/graph/management/middleware.ts @@ -1,13 +1,13 @@ -import { Db } from 'mongodb'; -import { GraphQLSchema } from 'graphql'; +import { Db } from "mongodb"; +import { GraphQLSchema } from "graphql"; -import { graphqlMiddleware } from 'talk-server/graph/common/middleware'; -import { Config } from 'talk-server/config'; +import { graphqlMiddleware } from "talk-server/graph/common/middleware"; +import { Config } from "talk-server/config"; -import Context from './context'; +import Context from "./context"; export default (schema: GraphQLSchema, config: Config, db: Db) => - graphqlMiddleware(config, async () => ({ - schema, - context: new Context({ db }), - })); + graphqlMiddleware(config, async () => ({ + schema, + context: new Context({ db }), + })); diff --git a/src/core/server/graph/management/resolvers/index.ts b/src/core/server/graph/management/resolvers/index.ts index b95bd7347..42b27b820 100644 --- a/src/core/server/graph/management/resolvers/index.ts +++ b/src/core/server/graph/management/resolvers/index.ts @@ -1,5 +1,5 @@ -import Cursor from '../../common/scalars/cursor'; +import Cursor from "../../common/scalars/cursor"; export default { - Cursor, + Cursor, }; diff --git a/src/core/server/graph/management/schema/index.ts b/src/core/server/graph/management/schema/index.ts index 8a616743a..ab7cd2856 100644 --- a/src/core/server/graph/management/schema/index.ts +++ b/src/core/server/graph/management/schema/index.ts @@ -1,6 +1,6 @@ -import loadSchema from 'talk-server/graph/common/schema'; -import resolvers from 'talk-server/graph/management/resolvers'; +import loadSchema from "talk-server/graph/common/schema"; +import resolvers from "talk-server/graph/management/resolvers"; export default function getManagementSchema() { - return loadSchema('management', resolvers); + return loadSchema("management", resolvers); } diff --git a/src/core/server/graph/schemas.ts b/src/core/server/graph/schemas.ts index 729d4d9c6..db115ac38 100644 --- a/src/core/server/graph/schemas.ts +++ b/src/core/server/graph/schemas.ts @@ -1,6 +1,6 @@ -import { GraphQLSchema } from 'graphql'; +import { GraphQLSchema } from "graphql"; export interface Schemas { - management: GraphQLSchema; - tenant: GraphQLSchema; + management: GraphQLSchema; + tenant: GraphQLSchema; } diff --git a/src/core/server/graph/tenant/context.ts b/src/core/server/graph/tenant/context.ts index 9b16d8189..aabe93615 100644 --- a/src/core/server/graph/tenant/context.ts +++ b/src/core/server/graph/tenant/context.ts @@ -1,20 +1,20 @@ -import loaders from './loaders'; -import { Db } from 'mongodb'; -import { Tenant } from 'talk-server/models/tenant'; +import loaders from "./loaders"; +import { Db } from "mongodb"; +import { Tenant } from "talk-server/models/tenant"; export interface TenantContextOptions { - tenant?: Tenant; - db: Db; + tenant?: Tenant; + db: Db; } export default class TenantContext { - public loaders: ReturnType; - public db: Db; - public tenant?: Tenant; + public loaders: ReturnType; + public db: Db; + public tenant?: Tenant; - constructor({ tenant, db }: TenantContextOptions) { - this.tenant = tenant; - this.loaders = loaders(this); - this.db = db; - } + constructor({ tenant, db }: TenantContextOptions) { + this.tenant = tenant; + this.loaders = loaders(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 31160ff90..b8747deb6 100644 --- a/src/core/server/graph/tenant/loaders/assets.ts +++ b/src/core/server/graph/tenant/loaders/assets.ts @@ -1,12 +1,12 @@ -import DataLoader from 'dataloader'; +import DataLoader from "dataloader"; import { - Asset, - retrieveMany as retrieveManyAssets, -} from 'talk-server/models/asset'; -import Context from 'talk-server/graph/tenant/context'; + Asset, + retrieveMany as retrieveManyAssets, +} from "talk-server/models/asset"; +import Context from "talk-server/graph/tenant/context"; export default (ctx: Context) => ({ - asset: new DataLoader(ids => - retrieveManyAssets(ctx.db, ctx.tenant.id, ids) - ), + 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 3314e9349..c6c1c22ce 100644 --- a/src/core/server/graph/tenant/loaders/comments.ts +++ b/src/core/server/graph/tenant/loaders/comments.ts @@ -1,25 +1,19 @@ -import DataLoader from 'dataloader'; +import DataLoader from "dataloader"; import { - Comment, - retrieveMany, - retrieveAssetConnection, - ConnectionInput, - retrieveRepliesConnection, -} from 'talk-server/models/comment'; -import Context from 'talk-server/graph/tenant/context'; + Comment, + retrieveMany, + retrieveAssetConnection, + ConnectionInput, + retrieveRepliesConnection, +} from "talk-server/models/comment"; +import Context from "talk-server/graph/tenant/context"; export default (ctx: Context) => ({ - comment: new DataLoader((ids: string[]) => - retrieveMany(ctx.db, ctx.tenant.id, ids) - ), - forAsset: (assetID: string, input: ConnectionInput) => - retrieveAssetConnection(ctx.db, ctx.tenant.id, assetID, input), - forParent: (assetID: string, parentID: string, input: ConnectionInput) => - retrieveRepliesConnection( - ctx.db, - ctx.tenant.id, - assetID, - parentID, - input - ), + comment: new DataLoader((ids: string[]) => + retrieveMany(ctx.db, ctx.tenant.id, ids) + ), + forAsset: (assetID: string, input: ConnectionInput) => + retrieveAssetConnection(ctx.db, ctx.tenant.id, assetID, input), + forParent: (assetID: string, parentID: string, input: ConnectionInput) => + retrieveRepliesConnection(ctx.db, ctx.tenant.id, assetID, parentID, input), }); diff --git a/src/core/server/graph/tenant/loaders/index.ts b/src/core/server/graph/tenant/loaders/index.ts index 028d05469..d9f2ca191 100644 --- a/src/core/server/graph/tenant/loaders/index.ts +++ b/src/core/server/graph/tenant/loaders/index.ts @@ -1,10 +1,10 @@ -import Assets from './assets'; -import Comments from './comments'; -import Users from './users'; -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), - Comments: Comments(ctx), - Users: Users(ctx), + Assets: Assets(ctx), + Comments: Comments(ctx), + Users: Users(ctx), }); diff --git a/src/core/server/graph/tenant/loaders/users.ts b/src/core/server/graph/tenant/loaders/users.ts index 2946f4ab8..3620b9bfc 100644 --- a/src/core/server/graph/tenant/loaders/users.ts +++ b/src/core/server/graph/tenant/loaders/users.ts @@ -1,9 +1,9 @@ -import DataLoader from 'dataloader'; -import { User, retrieveMany } from 'talk-server/models/user'; -import Context from 'talk-server/graph/tenant/context'; +import DataLoader from "dataloader"; +import { User, retrieveMany } from "talk-server/models/user"; +import Context from "talk-server/graph/tenant/context"; export default (ctx: Context) => ({ - user: new DataLoader(ids => - retrieveMany(ctx.db, ctx.tenant.id, ids) - ), + user: new DataLoader(ids => + retrieveMany(ctx.db, ctx.tenant.id, ids) + ), }); diff --git a/src/core/server/graph/tenant/middleware.ts b/src/core/server/graph/tenant/middleware.ts index 5835195d7..e6c2f36d6 100644 --- a/src/core/server/graph/tenant/middleware.ts +++ b/src/core/server/graph/tenant/middleware.ts @@ -1,24 +1,24 @@ -import { Db } from 'mongodb'; -import { GraphQLSchema } from 'graphql'; +import { Db } from "mongodb"; +import { GraphQLSchema } from "graphql"; -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 { 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 TenantContext from './context'; +import TenantContext from "./context"; export default async (schema: GraphQLSchema, config: Config, db: Db) => { - // Configure the PubSub broker. - const pubsub = await createPubSub(config); + // Configure the PubSub broker. + const pubsub = await createPubSub(config); - return graphqlMiddleware(config, async req => { - // TODO: replace with shared synced cache instead of direct db access. - const tenant = await retrieveByDomain(db, req.hostname); + return graphqlMiddleware(config, async req => { + // TODO: replace with shared synced cache instead of direct db access. + const tenant = await retrieveByDomain(db, req.hostname); - return { - schema, - context: new TenantContext({ db, tenant }), - }; - }); + return { + schema, + context: new TenantContext({ db, tenant }), + }; + }); }; diff --git a/src/core/server/graph/tenant/resolvers/asset.ts b/src/core/server/graph/tenant/resolvers/asset.ts index 0d2ef65ca..606dd13da 100644 --- a/src/core/server/graph/tenant/resolvers/asset.ts +++ b/src/core/server/graph/tenant/resolvers/asset.ts @@ -1,8 +1,8 @@ -import { Asset } from 'talk-server/models/asset'; -import Context from 'talk-server/graph/tenant/context'; -import { ConnectionInput } from 'talk-server/models/comment'; +import { Asset } from "talk-server/models/asset"; +import Context from "talk-server/graph/tenant/context"; +import { ConnectionInput } from "talk-server/models/comment"; export default { - comments: async (asset: Asset, input: ConnectionInput, ctx: Context) => - ctx.loaders.Comments.forAsset(asset.id, input), + comments: async (asset: Asset, input: ConnectionInput, ctx: Context) => + ctx.loaders.Comments.forAsset(asset.id, input), }; diff --git a/src/core/server/graph/tenant/resolvers/comment.ts b/src/core/server/graph/tenant/resolvers/comment.ts index d70ea2408..4d4d71688 100644 --- a/src/core/server/graph/tenant/resolvers/comment.ts +++ b/src/core/server/graph/tenant/resolvers/comment.ts @@ -1,9 +1,9 @@ -import { Comment, ConnectionInput } from 'talk-server/models/comment'; -import Context from 'talk-server/graph/tenant/context'; +import { Comment, ConnectionInput } from "talk-server/models/comment"; +import Context from "talk-server/graph/tenant/context"; export default { - author: async (comment: Comment, _: any, ctx: Context) => - ctx.loaders.Users.user.load(comment.author_id), - replies: async (comment: Comment, input: ConnectionInput, ctx: Context) => - ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input), + author: async (comment: Comment, _: any, ctx: Context) => + ctx.loaders.Users.user.load(comment.author_id), + replies: async (comment: Comment, input: ConnectionInput, ctx: Context) => + ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input), }; diff --git a/src/core/server/graph/tenant/resolvers/index.ts b/src/core/server/graph/tenant/resolvers/index.ts index 5b75890a8..52402d21e 100644 --- a/src/core/server/graph/tenant/resolvers/index.ts +++ b/src/core/server/graph/tenant/resolvers/index.ts @@ -1,11 +1,11 @@ -import Asset from './asset'; -import Comment from './comment'; -import Cursor from '../../common/scalars/cursor'; -import Query from './query'; +import Asset from "./asset"; +import Comment from "./comment"; +import Cursor from "../../common/scalars/cursor"; +import Query from "./query"; export default { - Asset, - Comment, - Cursor, - Query, + Asset, + Comment, + Cursor, + Query, }; diff --git a/src/core/server/graph/tenant/resolvers/query.ts b/src/core/server/graph/tenant/resolvers/query.ts index 9c3a7167f..faf7e8dc5 100644 --- a/src/core/server/graph/tenant/resolvers/query.ts +++ b/src/core/server/graph/tenant/resolvers/query.ts @@ -1,13 +1,13 @@ -import TenantContext from 'talk-server/graph/tenant/context'; -import { Asset } from 'talk-server/models/asset'; +import TenantContext from "talk-server/graph/tenant/context"; +import { Asset } from "talk-server/models/asset"; export default { - asset: async ( - _: any, - { id, url }: { id?: string; url: string }, - ctx: TenantContext - ): Promise => { - return ctx.loaders.Assets.asset.load(id); - }, - settings: async (parent: any, args: any, ctx: TenantContext) => ctx.tenant, + asset: async ( + _: any, + { id, url }: { id?: string; url: string }, + ctx: TenantContext + ): Promise => { + return ctx.loaders.Assets.asset.load(id); + }, + settings: async (parent: any, args: any, ctx: TenantContext) => ctx.tenant, }; diff --git a/src/core/server/graph/tenant/schema/index.ts b/src/core/server/graph/tenant/schema/index.ts index 3b4e1482f..93641ac66 100644 --- a/src/core/server/graph/tenant/schema/index.ts +++ b/src/core/server/graph/tenant/schema/index.ts @@ -1,6 +1,6 @@ -import loadSchema from 'talk-server/graph/common/schema'; -import resolvers from 'talk-server/graph/tenant/resolvers'; +import loadSchema from "talk-server/graph/common/schema"; +import resolvers from "talk-server/graph/tenant/resolvers"; export default function getTenantSchema() { - return loadSchema('tenant', resolvers); + return loadSchema("tenant", resolvers); } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 42e2df93e..5c44fbdca 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -1,85 +1,85 @@ -import express, { Express } from 'express'; -import http from 'http'; +import express, { Express } from "express"; +import http from "http"; -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'; +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; + config?: Config; } /** * Server provides an interface to create, start, and manage a Talk Server. */ class Server { - // parentApp is the root application that the server will bind to. - private parentApp: Express; + // parentApp is the root application that the server will bind to. + private parentApp: Express; - // schemas are the set of GraphQLSchema objects for each schema used by the - // server. - private schemas: Schemas; + // schemas are the set of GraphQLSchema objects for each schema used by the + // server. + private schemas: Schemas; - // config exposes application specific configuration. - public config: Config; + // config exposes application specific configuration. + public config: Config; - // httpServer is the running instance of the HTTP server that will bind to - // the requested port. - public httpServer: http.Server; + // httpServer is the running instance of the HTTP server that will bind to + // the requested port. + public httpServer: http.Server; - constructor(options: ServerOptions) { - this.parentApp = express(); - this.config = config - .load(options.config || {}) - .validate({ allowed: 'strict' }); + constructor(options: ServerOptions) { + this.parentApp = express(); + this.config = config + .load(options.config || {}) + .validate({ allowed: "strict" }); - // Load the graph schemas. - this.schemas = { - management: getManagementSchema(), - tenant: getTenantSchema(), - }; - } + // Load the graph schemas. + this.schemas = { + management: getManagementSchema(), + tenant: getTenantSchema(), + }; + } - /** - * start orchestrates the application by starting it and returning a promise - * when the server has started. - * - * @param parent the optional express application to bind the server to. - */ - public async start(parent?: Express) { - const port = this.config.get('port'); + /** + * start orchestrates the application by starting it and returning a promise + * when the server has started. + * + * @param parent the optional express application to bind the server to. + */ + public async start(parent?: Express) { + const port = this.config.get("port"); - // Ensure we have an app to bind to. - parent = parent ? parent : this.parentApp; + // Ensure we have an app to bind to. + parent = parent ? parent : this.parentApp; - // Setup MongoDB. - const mongo = await createMongoDB(config); + // Setup MongoDB. + const mongo = await createMongoDB(config); - // Setup Redis. - const redis = await createRedisClient(config); + // Setup Redis. + const redis = await createRedisClient(config); - // Create the Talk App, branching off from the parent app. - const app: Express = await createApp({ - parent, - mongo, - redis, - config: this.config, - schemas: this.schemas, - }); + // Create the Talk App, branching off from the parent app. + const app: Express = await createApp({ + parent, + mongo, + redis, + config: this.config, + schemas: this.schemas, + }); - // Start the application and store the resulting http.Server. - this.httpServer = await listenAndServe(app, port); + // Start the application and store the resulting http.Server. + this.httpServer = await listenAndServe(app, port); - // Setup the websocket servers on the new http.Server. - attachSubscriptionHandlers(this.schemas, this.httpServer); + // Setup the websocket servers on the new http.Server. + attachSubscriptionHandlers(this.schemas, this.httpServer); - logger.info({ port }, 'now listening'); - } + logger.info({ port }, "now listening"); + } } export default Server; diff --git a/src/core/server/logger.ts b/src/core/server/logger.ts index ade908cbc..c3b50c355 100644 --- a/src/core/server/logger.ts +++ b/src/core/server/logger.ts @@ -1,5 +1,5 @@ -import bunyan from 'bunyan'; +import bunyan from "bunyan"; -const logger = bunyan.createLogger({ name: 'talk' }); +const logger = bunyan.createLogger({ name: "talk" }); export default logger; diff --git a/src/core/server/models/actions.ts b/src/core/server/models/actions.ts index 514437aad..ead2033eb 100644 --- a/src/core/server/models/actions.ts +++ b/src/core/server/models/actions.ts @@ -1,3 +1,3 @@ export interface ActionCounts { - [_: string]: number; + [_: string]: number; } diff --git a/src/core/server/models/asset.ts b/src/core/server/models/asset.ts index f158ba9a2..326c448a8 100644 --- a/src/core/server/models/asset.ts +++ b/src/core/server/models/asset.ts @@ -1,117 +1,117 @@ -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 { TenantResource } from 'talk-server/models/tenant'; +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 { TenantResource } from "talk-server/models/tenant"; function collection(db: Db): Collection { - return db.collection('assets'); + return db.collection("assets"); } export interface Asset extends TenantResource { - readonly id: string; - url: string; - scraped?: Date; - closedAt?: Date; - closedMessage?: string; - title?: string; - description?: string; - image?: string; - section?: string; - subsection?: string; - author?: string; - publication_date?: Date; - modified_date?: Date; - created_at: Date; + readonly id: string; + url: string; + scraped?: Date; + closedAt?: Date; + closedMessage?: string; + title?: string; + description?: string; + image?: string; + section?: string; + subsection?: string; + author?: string; + publication_date?: Date; + modified_date?: Date; + created_at: Date; } -export type CreateAssetInput = Pick; +export type CreateAssetInput = Pick; export async function create( - db: Db, - tenantID: string, - input: CreateAssetInput + db: Db, + tenantID: string, + input: CreateAssetInput ): Promise { - const now = new Date(); + const now = new Date(); - // Construct the filter. - const query = new Query(collection(db)).where({ - tenant_id: tenantID, + // Construct the filter. + const query = new Query(collection(db)).where({ + tenant_id: tenantID, + }); + if (input.id) { + query.where({ id: input.id }); + } else { + query.where({ url: input.url }); + } + + // Craft the update object. + const update: { $setOnInsert: Asset } = { + $setOnInsert: defaults(input, { + id: uuid.v4(), + tenant_id: tenantID, + created_at: now, + }), + }; + + // Perform the upsert operation. + const result = await db + .collection("assets") + .findOneAndUpdate(query.filter, update, { + // Create the object if it doesn't already exist. + upsert: true, + // False to return the updated document instead of the original + // document. + returnOriginal: false, }); - if (input.id) { - query.where({ id: input.id }); - } else { - query.where({ url: input.url }); - } - // Craft the update object. - const update: { $setOnInsert: Asset } = { - $setOnInsert: defaults(input, { - id: uuid.v4(), - tenant_id: tenantID, - created_at: now, - }), - }; - - // Perform the upsert operation. - const result = await db - .collection('assets') - .findOneAndUpdate(query.filter, update, { - // Create the object if it doesn't already exist. - upsert: true, - // False to return the updated document instead of the original - // document. - returnOriginal: false, - }); - - return result.value; + return result.value; } export async function retrieve( - db: Db, - tenantID: string, - id: string + db: Db, + tenantID: string, + id: string ): Promise { - return await db - .collection('assets') - .findOne({ id, tenant_id: tenantID }); + return await db + .collection("assets") + .findOne({ id, tenant_id: tenantID }); } export async function retrieveMany( - db: Db, - tenantID: string, - ids: string[] + db: Db, + tenantID: string, + ids: string[] ): Promise> { - const cursor = await db - .collection('assets') - .find({ id: { $in: ids }, tenant_id: tenantID }); + const cursor = await db + .collection("assets") + .find({ id: { $in: ids }, tenant_id: tenantID }); - const assets = await cursor.toArray(); + const assets = await cursor.toArray(); - return ids.map(id => assets.find(asset => asset.id === id)); + return ids.map(id => assets.find(asset => asset.id === id)); } export type UpdateAssetInput = Omit< - Partial, - 'id' | 'tenant_id' | 'url' | 'created_at' + Partial, + "id" | "tenant_id" | "url" | "created_at" >; export async function update( - db: Db, - tenantID: string, - id: string, - update: UpdateAssetInput + db: Db, + tenantID: string, + id: string, + update: UpdateAssetInput ): Promise> { - const result = await db.collection('assets').findOneAndUpdate( - { id, tenant_id: tenantID }, - // Only update fields that have been updated. - { $set: dotize(update) }, - // False to return the updated document instead of the original - // document. - { returnOriginal: false } - ); + const result = await db.collection("assets").findOneAndUpdate( + { id, tenant_id: tenantID }, + // Only update fields that have been updated. + { $set: dotize(update) }, + // False to return the updated document instead of the original + // document. + { returnOriginal: false } + ); - return result.value; + return result.value; } diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index f49ac6c17..cb5bb09b9 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -1,145 +1,145 @@ -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 { ActionCounts } from 'talk-server/models/actions'; -import { TenantResource } from 'talk-server/models/tenant'; +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 { ActionCounts } from "talk-server/models/actions"; +import { TenantResource } from "talk-server/models/tenant"; function collection(db: Db): Collection { - return db.collection('comments'); + return db.collection("comments"); } export interface BodyHistoryItem { - body: string; - created_at: Date; + body: string; + created_at: Date; } export interface StatusHistoryItem { - status: CommentStatus; // TODO: migrate field - assigned_by?: string; - created_at: Date; + status: CommentStatus; // TODO: migrate field + assigned_by?: string; + created_at: Date; } export enum CommentStatus { - ACCEPTED = 'ACCEPTED', - REJECTED = 'REJECTED', - PREMOD = 'PREMOD', - SYSTEM_WITHHELD = 'SYSTEM_WITHHELD', - NONE = 'NONE', + ACCEPTED = "ACCEPTED", + REJECTED = "REJECTED", + PREMOD = "PREMOD", + SYSTEM_WITHHELD = "SYSTEM_WITHHELD", + NONE = "NONE", } export interface Comment extends TenantResource { - readonly id: string; - parent_id?: string; - author_id: string; - asset_id: string; - body: string; - body_history: BodyHistoryItem[]; - status: CommentStatus; - status_history: StatusHistoryItem[]; - action_counts: ActionCounts; - reply_count: number; - created_at: Date; - deleted_at?: Date; - metadata?: { - [_: string]: any; - }; + readonly id: string; + parent_id?: string; + author_id: string; + asset_id: string; + body: string; + body_history: BodyHistoryItem[]; + status: CommentStatus; + status_history: StatusHistoryItem[]; + action_counts: ActionCounts; + reply_count: number; + created_at: Date; + deleted_at?: Date; + metadata?: { + [_: string]: any; + }; } export type CreateCommentInput = Omit< - Comment, - | 'id' - | 'tenant_id' - | 'created_at' - | 'reply_count' - | 'body_history' - | 'status_history' + Comment, + | "id" + | "tenant_id" + | "created_at" + | "reply_count" + | "body_history" + | "status_history" >; export async function create( - db: Db, - tenantID: string, - input: CreateCommentInput + db: Db, + tenantID: string, + input: CreateCommentInput ): Promise> { - const now = new Date(); + const now = new Date(); - // Pull out some useful properties from the input. - const { body, status } = input; + // Pull out some useful properties from the input. + const { body, status } = input; - // default are the properties set by the application when a new comment is - // created. - const defaults: Sub = { - id: uuid.v4(), - tenant_id: tenantID, + // default are the properties set by the application when a new comment is + // created. + const defaults: Sub = { + id: uuid.v4(), + tenant_id: tenantID, + created_at: now, + reply_count: 0, + body_history: [ + { + body, created_at: now, - reply_count: 0, - body_history: [ - { - body, - created_at: now, - }, - ], - status_history: [ - { - status, - created_at: now, - }, - ], - }; + }, + ], + status_history: [ + { + status, + created_at: now, + }, + ], + }; - // Merge the defaults and the input together. - const comment: Comment = merge({}, defaults, input); + // Merge the defaults and the input together. + const comment: Comment = merge({}, defaults, input); - // TODO: Check for existence of the parent ID before we create the comment. + // TODO: Check for existence of the parent ID before we create the comment. - // TODO: Check for existence of the asset ID before we create the comment. + // TODO: Check for existence of the asset ID before we create the comment. - // Insert it into the database. - await collection(db).insertOne(comment); + // Insert it into the database. + await collection(db).insertOne(comment); - // TODO: update reply count of parent if exists. + // TODO: update reply count of parent if exists. - return comment; + return comment; } export async function retrieve( - db: Db, - tenantID: string, - id: string + db: Db, + tenantID: string, + id: string ): Promise> { - return collection(db).findOne({ id, tenant_id: tenantID }); + return collection(db).findOne({ id, tenant_id: tenantID }); } export async function retrieveMany( - db: Db, - tenantID: string, - ids: string[] + db: Db, + tenantID: string, + ids: string[] ): Promise[]> { - const cursor = await collection(db).find({ - id: { - $in: ids, - }, - tenant_id: tenantID, - }); + const cursor = await collection(db).find({ + id: { + $in: ids, + }, + tenant_id: tenantID, + }); - const comments = await cursor.toArray(); + const comments = await cursor.toArray(); - return ids.map(id => comments.find(comment => comment.id === id)); + return ids.map(id => comments.find(comment => comment.id === id)); } export enum CommentSort { - CREATED_AT_DESC = 'CREATED_AT_DESC', - CREATED_AT_ASC = 'CREATED_AT_ASC', - REPLIES_DESC = 'REPLIES_DESC', - RESPECT_DESC = 'RESPECT_DESC', + CREATED_AT_DESC = "CREATED_AT_DESC", + CREATED_AT_ASC = "CREATED_AT_ASC", + REPLIES_DESC = "REPLIES_DESC", + RESPECT_DESC = "RESPECT_DESC", } export interface ConnectionInput { - first: number; - orderBy: CommentSort; - after?: Cursor; + first: number; + orderBy: CommentSort; + after?: Cursor; } /** @@ -150,26 +150,26 @@ export interface ConnectionInput { * @param nodes nodes returned from the query */ function nodesToEdge( - input: ConnectionInput, - nodes: Comment[] + input: ConnectionInput, + nodes: Comment[] ): Edge[] { - let getCursor: (comment: Comment, index: number) => Cursor; - switch (input.orderBy) { - case CommentSort.CREATED_AT_DESC: - case CommentSort.CREATED_AT_ASC: - getCursor = comment => comment.created_at; - break; - case CommentSort.REPLIES_DESC: - case CommentSort.RESPECT_DESC: - getCursor = (_, index) => - (input.after ? (input.after as number) : 0) + index + 1; - break; - } + let getCursor: (comment: Comment, index: number) => Cursor; + switch (input.orderBy) { + case CommentSort.CREATED_AT_DESC: + case CommentSort.CREATED_AT_ASC: + getCursor = comment => comment.created_at; + break; + case CommentSort.REPLIES_DESC: + case CommentSort.RESPECT_DESC: + getCursor = (_, index) => + (input.after ? (input.after as number) : 0) + index + 1; + break; + } - return nodes.map((comment, index) => ({ - node: comment, - cursor: getCursor(comment, index), - })); + return nodes.map((comment, index) => ({ + node: comment, + cursor: getCursor(comment, index), + })); } /** @@ -181,21 +181,21 @@ function nodesToEdge( * @param input connection configuration */ export async function retrieveRepliesConnection( - db: Db, - tenantID: string, - assetID: string, - parentID: string, - input: ConnectionInput + db: Db, + tenantID: string, + assetID: string, + parentID: string, + input: ConnectionInput ): Promise>> { - // Create the query. - const query = new Query(collection(db)).where({ - tenant_id: tenantID, - asset_id: assetID, - parent_id: parentID, - }); + // Create the query. + const query = new Query(collection(db)).where({ + tenant_id: tenantID, + asset_id: assetID, + parent_id: parentID, + }); - // Return a connection for the comments query. - return retrieveConnection(input, query); + // Return a connection for the comments query. + return retrieveConnection(input, query); } /** @@ -207,20 +207,20 @@ export async function retrieveRepliesConnection( * @param input connection configuration */ export async function retrieveAssetConnection( - db: Db, - tenantID: string, - assetID: string, - input: ConnectionInput + db: Db, + tenantID: string, + assetID: string, + input: ConnectionInput ): Promise>> { - // Create the query. - const query = new Query(collection(db)).where({ - tenant_id: tenantID, - asset_id: assetID, - parent_id: null, - }); + // Create the query. + const query = new Query(collection(db)).where({ + tenant_id: tenantID, + asset_id: assetID, + parent_id: null, + }); - // Return a connection for the comments query. - return retrieveConnection(input, query); + // Return a connection for the comments query. + return retrieveConnection(input, query); } /** @@ -232,65 +232,65 @@ export async function retrieveAssetConnection( * configuration applied */ async function retrieveConnection( - input: ConnectionInput, - query: Query + input: ConnectionInput, + query: Query ): Promise>> { - // Apply some sorting options. - switch (input.orderBy) { - case CommentSort.CREATED_AT_DESC: - query.orderBy({ created_at: -1 }); - if (input.after) { - query.where({ created_at: { $lt: input.after as Date } }); - } - break; - case CommentSort.CREATED_AT_ASC: - query.orderBy({ created_at: 1 }); - if (input.after) { - query.where({ created_at: { $gt: input.after as Date } }); - } - break; - case CommentSort.REPLIES_DESC: - query.orderBy({ reply_count: -1, created_at: -1 }); - if (input.after) { - query.after(input.after as number); - } - break; - case CommentSort.RESPECT_DESC: - query.orderBy({ 'action_counts.respect': -1, created_at: -1 }); - if (input.after) { - query.after(input.after as number); - } - break; - } + // Apply some sorting options. + switch (input.orderBy) { + case CommentSort.CREATED_AT_DESC: + query.orderBy({ created_at: -1 }); + if (input.after) { + query.where({ created_at: { $lt: input.after as Date } }); + } + break; + case CommentSort.CREATED_AT_ASC: + query.orderBy({ created_at: 1 }); + if (input.after) { + query.where({ created_at: { $gt: input.after as Date } }); + } + break; + case CommentSort.REPLIES_DESC: + query.orderBy({ reply_count: -1, created_at: -1 }); + if (input.after) { + query.after(input.after as number); + } + break; + case CommentSort.RESPECT_DESC: + query.orderBy({ "action_counts.respect": -1, created_at: -1 }); + if (input.after) { + query.after(input.after as number); + } + break; + } - // We load one more than the limit so we can determine if there is - // another page of entries. - query.first(input.first + 1); + // We load one more than the limit so we can determine if there is + // another page of entries. + query.first(input.first + 1); - // Get the cursor. - const cursor = await query.exec(); + // Get the cursor. + const cursor = await query.exec(); - // Get the comments from the cursor. - const nodes = await cursor.toArray(); + // Get the comments from the cursor. + const nodes = await cursor.toArray(); - // The hasNextPage is always handled the same (ask for one more than we need, - // if there is one more, than there is more). - let hasNextPage = false; - if (input.first >= 0 && nodes.length > input.first) { - // There was one more than we expected! Set hasNextPage = true and remove - // the last item from the array that we requested. - hasNextPage = true; - nodes.splice(input.first, 1); - } + // The hasNextPage is always handled the same (ask for one more than we need, + // if there is one more, than there is more). + let hasNextPage = false; + if (input.first >= 0 && nodes.length > input.first) { + // There was one more than we expected! Set hasNextPage = true and remove + // the last item from the array that we requested. + hasNextPage = true; + nodes.splice(input.first, 1); + } - // Convert the nodes to edges. - const edges = nodesToEdge(input, nodes); + // Convert the nodes to edges. + const edges = nodesToEdge(input, nodes); - // Return the connection. - return { - edges, - pageInfo: { - hasNextPage, - }, - }; + // Return the connection. + return { + edges, + pageInfo: { + hasNextPage, + }, + }; } diff --git a/src/core/server/models/connection.ts b/src/core/server/models/connection.ts index 701d33351..9e0b87602 100644 --- a/src/core/server/models/connection.ts +++ b/src/core/server/models/connection.ts @@ -1,15 +1,15 @@ export type Cursor = Date | number | string; export interface Edge { - node: T; - cursor: Cursor; + node: T; + cursor: Cursor; } export interface PageInfo { - hasNextPage: boolean; + hasNextPage: boolean; } export interface Connection { - edges: Edge[]; - pageInfo: PageInfo; + edges: Edge[]; + pageInfo: PageInfo; } diff --git a/src/core/server/models/query.ts b/src/core/server/models/query.ts index 969837a48..3d61e78e1 100644 --- a/src/core/server/models/query.ts +++ b/src/core/server/models/query.ts @@ -1,7 +1,7 @@ -import { merge } from 'lodash'; -import { Collection, Cursor } from 'mongodb'; -import { FilterQuery as MongoFilterQuery } from 'mongodb'; -import { Writeable } from '../../common/types'; +import { merge } from "lodash"; +import { Collection, Cursor } from "mongodb"; +import { FilterQuery as MongoFilterQuery } from "mongodb"; +import { Writeable } from "../../common/types"; /** * FilterQuery ensures that given the type T, that the FilterQuery will be a @@ -15,78 +15,78 @@ export type FilterQuery = MongoFilterQuery>>; * provide easier complex query management. */ export default class Query { - public filter: FilterQuery; + public filter: FilterQuery; - private collection: Collection; - private skip?: number; - private limit?: number; - private sort?: Object; + private collection: Collection; + private skip?: number; + private limit?: number; + private sort?: Object; - constructor(collection: Collection) { - this.collection = collection; + constructor(collection: Collection) { + this.collection = collection; + } + + /** + * where will merge the given filter into the existing query. + * + * @param filter the filter to merge into the existing query + */ + public where(filter: FilterQuery): Query { + this.filter = merge({}, this.filter || {}, filter); + return this; + } + + /** + * after will skip the indicated number of documents. + * + * @param skip the number of documents to skip + */ + public after(skip: number): Query { + this.skip = skip; + return this; + } + + /** + * first will limit to the indicated number of documents. + * + * @param limit the number of documents to limit the result to + */ + public first(limit: number): Query { + this.limit = limit; + return this; + } + + /** + * orderBy will apply sorting to the query filter when executed. + * + * @param sort the sorting option for the documents + */ + public orderBy(sort: Object): Query { + this.sort = merge({}, this.sort || {}, sort); + return this; + } + + /** + * exec will return a cursor to the query. + */ + async exec(): Promise> { + let cursor = await this.collection.find(this.filter); + + if (this.limit) { + // Apply a limit if it exists. + cursor = cursor.limit(this.limit); } - /** - * where will merge the given filter into the existing query. - * - * @param filter the filter to merge into the existing query - */ - public where(filter: FilterQuery): Query { - this.filter = merge({}, this.filter || {}, filter); - return this; + if (this.sort) { + // Apply a sort if it exists. + cursor = cursor.sort(this.sort); } - /** - * after will skip the indicated number of documents. - * - * @param skip the number of documents to skip - */ - public after(skip: number): Query { - this.skip = skip; - return this; + if (this.skip) { + // Apply a skip if it exists. + cursor = cursor.skip(this.skip); } - /** - * first will limit to the indicated number of documents. - * - * @param limit the number of documents to limit the result to - */ - public first(limit: number): Query { - this.limit = limit; - return this; - } - - /** - * orderBy will apply sorting to the query filter when executed. - * - * @param sort the sorting option for the documents - */ - public orderBy(sort: Object): Query { - this.sort = merge({}, this.sort || {}, sort); - return this; - } - - /** - * exec will return a cursor to the query. - */ - async exec(): Promise> { - let cursor = await this.collection.find(this.filter); - - if (this.limit) { - // Apply a limit if it exists. - cursor = cursor.limit(this.limit); - } - - if (this.sort) { - // Apply a sort if it exists. - cursor = cursor.sort(this.sort); - } - - if (this.skip) { - // Apply a skip if it exists. - cursor = cursor.skip(this.skip); - } - - return cursor; - } + return cursor; + } } diff --git a/src/core/server/models/tenant.ts b/src/core/server/models/tenant.ts index 0b97ceb22..39e6b1e87 100644 --- a/src/core/server/models/tenant.ts +++ b/src/core/server/models/tenant.ts @@ -1,62 +1,62 @@ -import { Db, Collection } from 'mongodb'; -import { merge } from 'lodash'; -import dotize from 'dotize'; -import uuid from 'uuid'; -import { Omit, Sub } from 'talk-common/types'; +import { Db, Collection } from "mongodb"; +import { merge } from "lodash"; +import dotize from "dotize"; +import uuid from "uuid"; +import { Omit, Sub } from "talk-common/types"; function collection(db: Db): Collection { - return db.collection('tenants'); + return db.collection("tenants"); } export interface TenantResource { - readonly tenant_id: string; + readonly tenant_id: string; } export interface Wordlist { - banned: string[]; - suspect: string[]; + banned: string[]; + suspect: string[]; } export enum Moderation { - PRE = 'PRE', - POST = 'POST', + PRE = "PRE", + POST = "POST", } export interface Tenant { - readonly id: string; + readonly id: string; - // Domain is set when the tenant is created, and is used to retrieve the - // specific tenant that the API request pertains to. - domain: string; + // Domain is set when the tenant is created, and is used to retrieve the + // specific tenant that the API request pertains to. + domain: string; - moderation: Moderation; - requireEmailConfirmation: boolean; - infoBoxEnable: boolean; - infoBoxContent?: string; - questionBoxEnable: boolean; - questionBoxIcon?: string; - questionBoxContent?: string; - premodLinksEnable: boolean; - autoCloseStream: boolean; - closedTimeout: number; - closedMessage?: string; - customCssUrl?: string; - disableCommenting: boolean; - disableCommentingMessage?: string; + moderation: Moderation; + requireEmailConfirmation: boolean; + infoBoxEnable: boolean; + infoBoxContent?: string; + questionBoxEnable: boolean; + questionBoxIcon?: string; + questionBoxContent?: string; + premodLinksEnable: boolean; + autoCloseStream: boolean; + closedTimeout: number; + closedMessage?: string; + customCssUrl?: string; + disableCommenting: boolean; + 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: number; - charCountEnable: boolean; - charCount?: number; - organizationName: string; - organizationContactEmail: string; + // editCommentWindowLength is the length of time (in milliseconds) after a + // comment is posted that it can still be edited by the author. + editCommentWindowLength: number; + charCountEnable: boolean; + charCount?: number; + organizationName: string; + organizationContactEmail: string; - // wordlist stores all the banned/suspect words. - wordlist: Wordlist; + // wordlist stores all the banned/suspect words. + wordlist: Wordlist; - // domains is the set of whitelisted domains. - domains: string[]; + // domains is the set of whitelisted domains. + domains: string[]; } /** @@ -65,8 +65,8 @@ export interface Tenant { * are modifiable via the update method. */ export type CreateTenantInput = Pick< - Tenant, - 'domain' | 'organizationName' | 'organizationContactEmail' | 'domains' + Tenant, + "domain" | "organizationName" | "organizationContactEmail" | "domains" >; /** @@ -76,106 +76,106 @@ export type CreateTenantInput = Pick< * @param input the customizable parts of the Tenant available during creation */ export async function create( - db: Db, - input: CreateTenantInput + db: Db, + input: CreateTenantInput ): Promise> { - const defaults: Sub = { - // Create a new ID. - id: uuid.v4(), + const defaults: Sub = { + // Create a new ID. + id: uuid.v4(), - // Default to post moderation. - moderation: Moderation.POST, + // Default to post moderation. + moderation: Moderation.POST, - // Email confirmation is default off. - requireEmailConfirmation: false, - infoBoxEnable: false, - questionBoxEnable: false, - premodLinksEnable: false, - autoCloseStream: false, + // Email confirmation is default off. + requireEmailConfirmation: false, + infoBoxEnable: false, + questionBoxEnable: false, + premodLinksEnable: false, + autoCloseStream: false, - // Two weeks timeout. - closedTimeout: 60 * 60 * 24 * 7 * 2, - disableCommenting: false, - editCommentWindowLength: 30 * 1000, - charCountEnable: false, - wordlist: { - suspect: [], - banned: [], - }, - }; + // Two weeks timeout. + closedTimeout: 60 * 60 * 24 * 7 * 2, + disableCommenting: false, + editCommentWindowLength: 30 * 1000, + charCountEnable: false, + wordlist: { + suspect: [], + banned: [], + }, + }; - // Create the new Tenant by merging it together with the defaults. - const tenant = merge({}, input, defaults); + // Create the new Tenant by merging it together with the defaults. + const tenant = merge({}, input, defaults); - // Insert the Tenant into the database. - await collection(db).insert(tenant); + // Insert the Tenant into the database. + await collection(db).insert(tenant); - return tenant; + return tenant; } export async function retrieveByDomain( - db: Db, - domain: string + db: Db, + domain: string ): Promise> { - return collection(db).findOne({ domain }); + return collection(db).findOne({ domain }); } export async function retrieve(db: Db, id: string): Promise> { - return collection(db).findOne({ id }); + return collection(db).findOne({ id }); } export async function retrieveMany( - db: Db, - ids: string[] + db: Db, + ids: string[] ): Promise[]> { - const cursor = await collection(db).find({ - id: { - $in: ids, - }, - }); + const cursor = await collection(db).find({ + id: { + $in: ids, + }, + }); - const tenants = await cursor.toArray(); + const tenants = await cursor.toArray(); - return ids.map(id => tenants.find(tenant => tenant.id === id)); + return ids.map(id => tenants.find(tenant => tenant.id === id)); } export async function retrieveManyByDomain( - db: Db, - domains: string[] + db: Db, + domains: string[] ): Promise[]> { - const cursor = await collection(db).find({ - domain: { - $in: domains, - }, - }); + const cursor = await collection(db).find({ + domain: { + $in: domains, + }, + }); - const tenants = await cursor.toArray(); + const tenants = await cursor.toArray(); - return domains.map(domain => - tenants.find(tenant => tenant.domain === domain) - ); + return domains.map(domain => + tenants.find(tenant => tenant.domain === domain) + ); } export async function retrieveAll(db: Db): Promise[]> { - return collection(db) - .find({}) - .toArray(); + return collection(db) + .find({}) + .toArray(); } export async function update( - db: Db, - id: string, - update: Partial + db: Db, + id: string, + update: Partial ): Promise> { - // Get the tenant from the database. - const result = await collection(db).findOneAndUpdate( - { id }, - // Only update fields that have been updated. - { $set: dotize(update) }, - // False to return the updated document instead of the original - // document. - { returnOriginal: false } - ); + // Get the tenant from the database. + const result = await collection(db).findOneAndUpdate( + { id }, + // Only update fields that have been updated. + { $set: dotize(update) }, + // False to return the updated document instead of the original + // document. + { returnOriginal: false } + ); - return result.value; + return result.value; } diff --git a/src/core/server/models/user.ts b/src/core/server/models/user.ts index eb9c7c372..6831c905e 100644 --- a/src/core/server/models/user.ts +++ b/src/core/server/models/user.ts @@ -1,181 +1,181 @@ -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 { TenantResource } from 'talk-server/models/tenant'; +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 { TenantResource } from "talk-server/models/tenant"; function collection(db: Db): Collection { - return db.collection('users'); + return db.collection("users"); } export interface Profile { - readonly id: string; - provider: string; + readonly id: string; + provider: string; } export interface Token { - readonly id: string; - name: string; - active: boolean; + readonly id: string; + name: string; + active: boolean; } export enum UserUsernameStatus { - // UNSET is used when the username can be changed, and does not necessarily - // require moderator action to become active. This can be used when the user - // signs up with a social login and has the option of setting their own - // username. - UNSET = 'UNSET', + // UNSET is used when the username can be changed, and does not necessarily + // require moderator action to become active. This can be used when the user + // signs up with a social login and has the option of setting their own + // username. + UNSET = "UNSET", - // SET is used when the username has been set for the first time, but cannot - // change without the username being rejected by a moderator and that moderator - // agreeing that the username should be allowed to change. - SET = 'SET', + // SET is used when the username has been set for the first time, but cannot + // change without the username being rejected by a moderator and that moderator + // agreeing that the username should be allowed to change. + SET = "SET", - // APPROVED is used when the username was changed, and subsequently approved by - // said moderator. - APPROVED = 'APPROVED', + // APPROVED is used when the username was changed, and subsequently approved by + // said moderator. + APPROVED = "APPROVED", - // REJECTED is used when the username was changed, and subsequently rejected by - // said moderator. - REJECTED = 'REJECTED', + // REJECTED is used when the username was changed, and subsequently rejected by + // said moderator. + REJECTED = "REJECTED", - // CHANGED is used after a user has changed their username after it was - // rejected. - CHANGED = 'CHANGED', + // CHANGED is used after a user has changed their username after it was + // rejected. + CHANGED = "CHANGED", } export enum UserRole { - ADMIN = 'ADMIN', - MODERATOR = 'MODERATOR', - STAFF = 'STAFF', - COMMENTER = 'COMMENTER', + ADMIN = "ADMIN", + MODERATOR = "MODERATOR", + STAFF = "STAFF", + COMMENTER = "COMMENTER", } export interface UserStatusHistory { - status: T; // TODO: migrate field - assigned_by?: string; - reason?: string; // TODO: migrate field - created_at: Date; + status: T; // TODO: migrate field + assigned_by?: string; + reason?: string; // TODO: migrate field + created_at: Date; } export interface UserStatusItem { - status: T; // TODO: migrate field - history: UserStatusHistory[]; + status: T; // TODO: migrate field + history: UserStatusHistory[]; } export interface UserStatus { - username: UserStatusItem; - banned: UserStatusItem; - suspension: UserStatusItem; + username: UserStatusItem; + banned: UserStatusItem; + suspension: UserStatusItem; } export interface User extends TenantResource { - readonly id: string; - username: string; - password?: string; - profiles: Profile[]; - tokens: Token[]; - role: UserRole; - status: UserStatus; - action_counts: ActionCounts; - ignored_users: string[]; // TODO: migrate field - created_at: Date; + readonly id: string; + username: string; + password?: string; + profiles: Profile[]; + tokens: Token[]; + role: UserRole; + status: UserStatus; + action_counts: ActionCounts; + ignored_users: string[]; // TODO: migrate field + created_at: Date; } export type CreateUserInput = Omit< - User, - | 'id' - | 'tenant_id' - | 'tokens' - | 'status' - | 'role' - | 'action_counts' - | 'ignored_users' - | 'created_at' + User, + | "id" + | "tenant_id" + | "tokens" + | "status" + | "role" + | "action_counts" + | "ignored_users" + | "created_at" >; export async function create( - db: Db, - tenantID: string, - input: CreateUserInput + db: Db, + tenantID: string, + input: CreateUserInput ): Promise> { - const now = new Date(); + const now = new Date(); - // // Pull out some useful properties from the input. - // const { body, status } = input; + // // Pull out some useful properties from the input. + // const { body, status } = input; - // default are the properties set by the application when a new user is - // created. - const defaults: Sub = { - id: uuid.v4(), - tenant_id: tenantID, - role: UserRole.COMMENTER, - tokens: [], - action_counts: {}, - ignored_users: [], - status: { - banned: { - status: false, - history: [], - }, - suspension: { - status: null, - history: [], - }, - username: { - status: UserUsernameStatus.SET, - history: [], - }, - }, - created_at: now, - }; + // default are the properties set by the application when a new user is + // created. + const defaults: Sub = { + id: uuid.v4(), + tenant_id: tenantID, + role: UserRole.COMMENTER, + tokens: [], + action_counts: {}, + ignored_users: [], + status: { + banned: { + status: false, + history: [], + }, + suspension: { + status: null, + history: [], + }, + username: { + status: UserUsernameStatus.SET, + history: [], + }, + }, + created_at: now, + }; - // Merge the defaults and the input together. - const user: User = merge({}, defaults, input); + // Merge the defaults and the input together. + const user: User = merge({}, defaults, input); - // Insert it into the database. - await collection(db).insertOne(user); + // Insert it into the database. + await collection(db).insertOne(user); - return user; + return user; } export async function retrieve( - db: Db, - tenantID: string, - id: string + db: Db, + tenantID: string, + id: string ): Promise> { - return collection(db).findOne({ id, tenant_id: tenantID }); + return collection(db).findOne({ id, tenant_id: tenantID }); } export async function retrieveMany( - db: Db, - tenantID: string, - ids: string[] + db: Db, + tenantID: string, + ids: string[] ): Promise[]> { - const cursor = await collection(db).find({ - id: { - $in: ids, - }, - tenant_id: tenantID, - }); + const cursor = await collection(db).find({ + id: { + $in: ids, + }, + tenant_id: tenantID, + }); - const users = await cursor.toArray(); + const users = await cursor.toArray(); - return ids.map(id => users.find(comment => comment.id === id)); + return ids.map(id => users.find(comment => comment.id === id)); } export async function updateRole( - db: Db, - tenantID: string, - id: string, - role: UserRole + db: Db, + tenantID: string, + id: string, + role: UserRole ): Promise> { - const result = await collection(db).findOneAndUpdate( - { id, tenant_id: tenantID }, - { $set: { role } }, - { returnOriginal: false } - ); + const result = await collection(db).findOneAndUpdate( + { id, tenant_id: tenantID }, + { $set: { role } }, + { returnOriginal: false } + ); - return result.value; + return result.value; } diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 8e67030fe..511fb9a84 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -1,6 +1,6 @@ -import { Db } from 'mongodb'; -import { Comment } from 'talk-server/models/comment'; +import { Db } from "mongodb"; +import { Comment } from "talk-server/models/comment"; export async function create(db: Db): Promise { - return null; + return null; } diff --git a/src/core/server/services/mongodb/index.ts b/src/core/server/services/mongodb/index.ts index 2d869d3bd..f9e9e3d6b 100644 --- a/src/core/server/services/mongodb/index.ts +++ b/src/core/server/services/mongodb/index.ts @@ -1,5 +1,5 @@ -import { MongoClient, Db } from 'mongodb'; -import { Config } from 'talk-server/config'; +import { MongoClient, Db } from "mongodb"; +import { Config } from "talk-server/config"; /** * create will connect to the MongoDB instance identified in the configuration. @@ -7,10 +7,10 @@ import { Config } from 'talk-server/config'; * @param config application configuration. */ export async function createMongoDB(config: Config): Promise { - // Connect and create a client for MongoDB. - const client = await MongoClient.connect(config.get('mongodb')); + // Connect and create a client for MongoDB. + const client = await MongoClient.connect(config.get("mongodb")); - // Return the database handle, which defaults to the database name provided - // in the config connection string. - return client.db(); + // Return the database handle, which defaults to the database name provided + // in the config connection string. + return client.db(); } diff --git a/src/core/server/services/redis/index.ts b/src/core/server/services/redis/index.ts index 37623adaf..14b977b59 100644 --- a/src/core/server/services/redis/index.ts +++ b/src/core/server/services/redis/index.ts @@ -1,5 +1,5 @@ -import RedisClient, { Redis } from 'ioredis'; -import { Config } from 'talk-server/config'; +import RedisClient, { Redis } from "ioredis"; +import { Config } from "talk-server/config"; /** * create will connect to the Redis instance identified in the configuration. @@ -7,5 +7,5 @@ import { Config } from 'talk-server/config'; * @param config application configuration. */ export async function createRedisClient(config: Config): Promise { - return new RedisClient(config.get('redis'), {}); + return new RedisClient(config.get("redis"), {}); } diff --git a/src/core/server/services/tenant/cache.ts b/src/core/server/services/tenant/cache.ts index fb3ec68fa..990c36a2d 100644 --- a/src/core/server/services/tenant/cache.ts +++ b/src/core/server/services/tenant/cache.ts @@ -1,89 +1,89 @@ -import { Db } from 'mongodb'; -import { Redis } from 'ioredis'; -import DataLoader from 'dataloader'; +import { Db } from "mongodb"; +import { Redis } from "ioredis"; +import DataLoader from "dataloader"; -import { Tenant, retrieveAll, retrieveMany } from 'talk-server/models/tenant'; +import { Tenant, retrieveAll, retrieveMany } from "talk-server/models/tenant"; -const CacheUpdateChannel = 'tenant'; +const CacheUpdateChannel = "tenant"; // Cache provides an interface for retrieving tenant stored in local memory // rather than grabbing it from the database every single call. export default class Cache { - // private tenants: Map>>; - private tenants: DataLoader>; - private db: Db; + // private tenants: Map>>; + private tenants: DataLoader>; + private db: Db; - constructor(db: Db, subscriber: Redis) { - // Save the Db reference. - this.db = db; + constructor(db: Db, subscriber: Redis) { + // Save the Db reference. + this.db = db; - // Prepare the list of all tenant's maintained by this instance. - this.tenants = new DataLoader(ids => retrieveMany(db, ids)); + // Prepare the list of all tenant's maintained by this instance. + this.tenants = new DataLoader(ids => retrieveMany(db, ids)); - // Subscribe to tenant notifications. - subscriber.subscribe(CacheUpdateChannel); + // Subscribe to tenant notifications. + subscriber.subscribe(CacheUpdateChannel); - // Attach to messages on this connection so we can receive updates when - // the tenant are changed. - subscriber.on('message', this.onMessage); + // Attach to messages on this connection so we can receive updates when + // the tenant are changed. + subscriber.on("message", this.onMessage); + } + + /** + * primeAll will load all the tenants into the cache on startup. + */ + public async primeAll() { + // Grab all the tenants for this node. + const tenants = await retrieveAll(this.db); + + // Clear out all the items in the cache. + this.tenants.clearAll(); + + // Prime the cache with each of these tenants. + tenants.forEach(tenant => this.tenants.prime(tenant.id, tenant)); + } + + /** + * onMessage is fired every time the client gets a subscription event. + */ + private onMessage = async ( + channel: string, + message: string + ): Promise => { + // Only do things when the message is for tenant. + if (channel !== CacheUpdateChannel) { + return; } - /** - * primeAll will load all the tenants into the cache on startup. - */ - public async primeAll() { - // Grab all the tenants for this node. - const tenants = await retrieveAll(this.db); + try { + // Updated tenant come from the messages. + const tenant: Tenant = JSON.parse(message); - // Clear out all the items in the cache. - this.tenants.clearAll(); - - // Prime the cache with each of these tenants. - tenants.forEach(tenant => this.tenants.prime(tenant.id, tenant)); + // Update the tenant cache. + this.tenants.clear(tenant.id).prime(tenant.id, tenant); + } catch (err) { + // FIXME: handle the error } + }; - /** - * onMessage is fired every time the client gets a subscription event. - */ - private onMessage = async ( - channel: string, - message: string - ): Promise => { - // Only do things when the message is for tenant. - if (channel !== CacheUpdateChannel) { - return; - } + /** + * retrieve returns a promise that will resolve to the tenant for Talk. + */ + public async retrieve(id: string): Promise> { + return this.tenants.load(id); + } - try { - // Updated tenant come from the messages. - const tenant: Tenant = JSON.parse(message); + /** + * update will update the value for Tenant in the local cache and publish + * a change notification that will be used to keep the other nodes in sync. + * + * @param conn a redis connection used to publish the change notification + * @param tenant the updated Tenant object + */ + public async update(conn: Redis, tenant: Tenant): Promise { + // Update the tenant in the local cache. + this.tenants.clear(tenant.id).prime(tenant.id, tenant); - // Update the tenant cache. - this.tenants.clear(tenant.id).prime(tenant.id, tenant); - } catch (err) { - // FIXME: handle the error - } - }; - - /** - * retrieve returns a promise that will resolve to the tenant for Talk. - */ - public async retrieve(id: string): Promise> { - return this.tenants.load(id); - } - - /** - * update will update the value for Tenant in the local cache and publish - * a change notification that will be used to keep the other nodes in sync. - * - * @param conn a redis connection used to publish the change notification - * @param tenant the updated Tenant object - */ - public async update(conn: Redis, tenant: Tenant): Promise { - // Update the tenant in the local cache. - this.tenants.clear(tenant.id).prime(tenant.id, tenant); - - // Notify the other nodes about the tenant change. - await conn.publish(CacheUpdateChannel, JSON.stringify(tenant)); - } + // Notify the other nodes about the tenant change. + await conn.publish(CacheUpdateChannel, JSON.stringify(tenant)); + } } diff --git a/src/index.ts b/src/index.ts index e4625e79e..421c57eba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import createTalk from './core'; -import express from 'express'; +import createTalk from "./core"; +import express from "express"; // Create the app that will serve as the mounting point for the Talk Server. const app = express(); diff --git a/src/types/dotize.d.ts b/src/types/dotize.d.ts index 64c7e1a89..f8b3bca5f 100644 --- a/src/types/dotize.d.ts +++ b/src/types/dotize.d.ts @@ -1,5 +1,5 @@ -declare module 'dotize' { - export = dotize; +declare module "dotize" { + export = dotize; - function dotize(obj: any): { [_: string]: any }; + function dotize(obj: any): { [_: string]: any }; } diff --git a/src/types/express-static-gzip.d.ts b/src/types/express-static-gzip.d.ts index 04a5dd69f..2278f9ee0 100644 --- a/src/types/express-static-gzip.d.ts +++ b/src/types/express-static-gzip.d.ts @@ -1,4 +1,4 @@ -declare module 'express-static-gzip' { +declare module "express-static-gzip" { export = express_static_gzip; function express_static_gzip(rootFolder: any, options: any): any;