mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 17:02:45 +08:00
linting
This commit is contained in:
+8
-2
@@ -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
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.ts]
|
||||
max_line_length = 80
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
+53
-53
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export type Diff<T extends keyof any, U extends keyof any> = ({ [P in T]: P } &
|
||||
{ [P in U]: never } & { [x: string]: never })[T];
|
||||
{ [P in U]: never } & { [x: string]: never })[T];
|
||||
|
||||
export type Omit<U, K extends keyof U> = Pick<U, Diff<keyof U, K>>;
|
||||
|
||||
|
||||
+4
-4
@@ -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<Server> {
|
||||
// Create the server with the provided options.
|
||||
return new Server(options);
|
||||
// Create the server with the provided options.
|
||||
return new Server(options);
|
||||
}
|
||||
|
||||
@@ -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<Express> {
|
||||
// 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<Express> {
|
||||
* @param port the port to listen on
|
||||
*/
|
||||
export const listenAndServe = (
|
||||
app: Express,
|
||||
port: number
|
||||
app: Express,
|
||||
port: number
|
||||
): Promise<http.Server> =>
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"), {});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+62
-62
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<RedisPubSub> {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Cursor from '../../common/scalars/cursor';
|
||||
import Cursor from "../../common/scalars/cursor";
|
||||
|
||||
export default {
|
||||
Cursor,
|
||||
Cursor,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
import { GraphQLSchema } from "graphql";
|
||||
|
||||
export interface Schemas {
|
||||
management: GraphQLSchema;
|
||||
tenant: GraphQLSchema;
|
||||
management: GraphQLSchema;
|
||||
tenant: GraphQLSchema;
|
||||
}
|
||||
|
||||
@@ -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<typeof loaders>;
|
||||
public db: Db;
|
||||
public tenant?: Tenant;
|
||||
public loaders: ReturnType<typeof loaders>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, Asset>(ids =>
|
||||
retrieveManyAssets(ctx.db, ctx.tenant.id, ids)
|
||||
),
|
||||
asset: new DataLoader<string, Asset>(ids =>
|
||||
retrieveManyAssets(ctx.db, ctx.tenant.id, ids)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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<string, User>(ids =>
|
||||
retrieveMany(ctx.db, ctx.tenant.id, ids)
|
||||
),
|
||||
user: new DataLoader<string, User>(ids =>
|
||||
retrieveMany(ctx.db, ctx.tenant.id, ids)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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<Asset> => {
|
||||
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<Asset> => {
|
||||
return ctx.loaders.Assets.asset.load(id);
|
||||
},
|
||||
settings: async (parent: any, args: any, ctx: TenantContext) => ctx.tenant,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+60
-60
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface ActionCounts {
|
||||
[_: string]: number;
|
||||
[_: string]: number;
|
||||
}
|
||||
|
||||
@@ -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<Asset> {
|
||||
return db.collection<Asset>('assets');
|
||||
return db.collection<Asset>("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<Asset, 'id' | 'url'>;
|
||||
export type CreateAssetInput = Pick<Asset, "id" | "url">;
|
||||
|
||||
export async function create(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
input: CreateAssetInput
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
input: CreateAssetInput
|
||||
): Promise<Asset> {
|
||||
const now = new Date();
|
||||
const now = new Date();
|
||||
|
||||
// Construct the filter.
|
||||
const query = new Query<Asset>(collection(db)).where({
|
||||
tenant_id: tenantID,
|
||||
// Construct the filter.
|
||||
const query = new Query<Asset>(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<Asset>("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<Asset>('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<Asset> {
|
||||
return await db
|
||||
.collection<Asset>('assets')
|
||||
.findOne({ id, tenant_id: tenantID });
|
||||
return await db
|
||||
.collection<Asset>("assets")
|
||||
.findOne({ id, tenant_id: tenantID });
|
||||
}
|
||||
|
||||
export async function retrieveMany(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
ids: string[]
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
ids: string[]
|
||||
): Promise<Array<Asset>> {
|
||||
const cursor = await db
|
||||
.collection<Asset>('assets')
|
||||
.find({ id: { $in: ids }, tenant_id: tenantID });
|
||||
const cursor = await db
|
||||
.collection<Asset>("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<Asset>,
|
||||
'id' | 'tenant_id' | 'url' | 'created_at'
|
||||
Partial<Asset>,
|
||||
"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<Readonly<Asset>> {
|
||||
const result = await db.collection<Asset>('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<Asset>("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;
|
||||
}
|
||||
|
||||
+193
-193
@@ -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<Comment> {
|
||||
return db.collection<Comment>('comments');
|
||||
return db.collection<Comment>("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<Readonly<Comment>> {
|
||||
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<Comment, CreateCommentInput> = {
|
||||
id: uuid.v4(),
|
||||
tenant_id: tenantID,
|
||||
// default are the properties set by the application when a new comment is
|
||||
// created.
|
||||
const defaults: Sub<Comment, CreateCommentInput> = {
|
||||
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<Readonly<Comment>> {
|
||||
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<Readonly<Comment>[]> {
|
||||
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<Comment>[] {
|
||||
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<Readonly<Connection<Comment>>> {
|
||||
// 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<Readonly<Connection<Comment>>> {
|
||||
// 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<Comment>
|
||||
input: ConnectionInput,
|
||||
query: Query<Comment>
|
||||
): Promise<Readonly<Connection<Comment>>> {
|
||||
// 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
export type Cursor = Date | number | string;
|
||||
|
||||
export interface Edge<T> {
|
||||
node: T;
|
||||
cursor: Cursor;
|
||||
node: T;
|
||||
cursor: Cursor;
|
||||
}
|
||||
|
||||
export interface PageInfo {
|
||||
hasNextPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
export interface Connection<T> {
|
||||
edges: Edge<T>[];
|
||||
pageInfo: PageInfo;
|
||||
edges: Edge<T>[];
|
||||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
@@ -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<T> ensures that given the type T, that the FilterQuery will be a
|
||||
@@ -15,78 +15,78 @@ export type FilterQuery<T> = MongoFilterQuery<Writeable<Partial<T>>>;
|
||||
* provide easier complex query management.
|
||||
*/
|
||||
export default class Query<T> {
|
||||
public filter: FilterQuery<T>;
|
||||
public filter: FilterQuery<T>;
|
||||
|
||||
private collection: Collection<T>;
|
||||
private skip?: number;
|
||||
private limit?: number;
|
||||
private sort?: Object;
|
||||
private collection: Collection<T>;
|
||||
private skip?: number;
|
||||
private limit?: number;
|
||||
private sort?: Object;
|
||||
|
||||
constructor(collection: Collection<T>) {
|
||||
this.collection = collection;
|
||||
constructor(collection: Collection<T>) {
|
||||
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<T>): Query<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
this.sort = merge({}, this.sort || {}, sort);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* exec will return a cursor to the query.
|
||||
*/
|
||||
async exec(): Promise<Cursor<T>> {
|
||||
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<T>): Query<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
this.sort = merge({}, this.sort || {}, sort);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* exec will return a cursor to the query.
|
||||
*/
|
||||
async exec(): Promise<Cursor<T>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+110
-110
@@ -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<Tenant> {
|
||||
return db.collection<Tenant>('tenants');
|
||||
return db.collection<Tenant>("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<Readonly<Tenant>> {
|
||||
const defaults: Sub<Tenant, CreateTenantInput> = {
|
||||
// Create a new ID.
|
||||
id: uuid.v4(),
|
||||
const defaults: Sub<Tenant, CreateTenantInput> = {
|
||||
// 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<Readonly<Tenant>> {
|
||||
return collection(db).findOne({ domain });
|
||||
return collection(db).findOne({ domain });
|
||||
}
|
||||
|
||||
export async function retrieve(db: Db, id: string): Promise<Readonly<Tenant>> {
|
||||
return collection(db).findOne({ id });
|
||||
return collection(db).findOne({ id });
|
||||
}
|
||||
|
||||
export async function retrieveMany(
|
||||
db: Db,
|
||||
ids: string[]
|
||||
db: Db,
|
||||
ids: string[]
|
||||
): Promise<Readonly<Tenant>[]> {
|
||||
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<Readonly<Tenant>[]> {
|
||||
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<Readonly<Tenant>[]> {
|
||||
return collection(db)
|
||||
.find({})
|
||||
.toArray();
|
||||
return collection(db)
|
||||
.find({})
|
||||
.toArray();
|
||||
}
|
||||
|
||||
export async function update(
|
||||
db: Db,
|
||||
id: string,
|
||||
update: Partial<CreateTenantInput>
|
||||
db: Db,
|
||||
id: string,
|
||||
update: Partial<CreateTenantInput>
|
||||
): Promise<Readonly<Tenant>> {
|
||||
// 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;
|
||||
}
|
||||
|
||||
+123
-123
@@ -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<User> {
|
||||
return db.collection<User>('users');
|
||||
return db.collection<User>("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<T> {
|
||||
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<T> {
|
||||
status: T; // TODO: migrate field
|
||||
history: UserStatusHistory<T>[];
|
||||
status: T; // TODO: migrate field
|
||||
history: UserStatusHistory<T>[];
|
||||
}
|
||||
|
||||
export interface UserStatus {
|
||||
username: UserStatusItem<UserUsernameStatus>;
|
||||
banned: UserStatusItem<boolean>;
|
||||
suspension: UserStatusItem<Date>;
|
||||
username: UserStatusItem<UserUsernameStatus>;
|
||||
banned: UserStatusItem<boolean>;
|
||||
suspension: UserStatusItem<Date>;
|
||||
}
|
||||
|
||||
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<Readonly<User>> {
|
||||
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<User, CreateUserInput> = {
|
||||
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<User, CreateUserInput> = {
|
||||
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<Readonly<User>> {
|
||||
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<Readonly<User>[]> {
|
||||
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<Readonly<User>> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<Comment> {
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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<Db> {
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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<Redis> {
|
||||
return new RedisClient(config.get('redis'), {});
|
||||
return new RedisClient(config.get("redis"), {});
|
||||
}
|
||||
|
||||
@@ -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<string, Promise<Readonly<Tenant>>>;
|
||||
private tenants: DataLoader<string, Readonly<Tenant>>;
|
||||
private db: Db;
|
||||
// private tenants: Map<string, Promise<Readonly<Tenant>>>;
|
||||
private tenants: DataLoader<string, Readonly<Tenant>>;
|
||||
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<void> => {
|
||||
// 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<void> => {
|
||||
// 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<Readonly<Tenant>> {
|
||||
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<void> {
|
||||
// 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<Readonly<Tenant>> {
|
||||
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<void> {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
Vendored
+3
-3
@@ -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 };
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user