Files
talk/src/core/server/index.ts
T
Vinh c9a0ab8848 [CORL-878] Upgrade dependencies (#2867)
* chore: upgrade fluent

* chore: upgrade metascraper

* chore: upgrade akismet-api

* chore: upgrade apollo-server-express

* chore: upgrade archiver

* chore: upgrade bull

* chore: upgrade express, cheerio, content-security-policy-builder

* chore: upgrade convict

* chore: upgrade cors, cron

* chore: upgrade csv-stringify

* chore: upgrade dompurify

* chore: upgrade dotenv

* chore: upgrade express-static-gzip

* chore: upgrade fs-extra

* chore: upgrade graphql-js

* chore: upgrade graphql packages

* chore: upgrade html-minifier

* chore: upgrade html-to-text

* chore: upgrade ioredis

* chore: upgrade joi

* chore: upgrade jsdom

* chore: upgrade jsonwebtoken

* chore: upgrade juice

* chore: upgrade jwks-rsa ad linkifyjs

* chore: upgrade lodash

* chore: upgrade luxon

* chore: upgrade metascraper

* chore: upgrade mongodb

* chore: upgrade ms

* chore: upgrade node and node-fetch types

* chore: upgrade nodemailer nunjucks and typescript-eslint

* chore: Upgrade passport

* upgrade: prom-client react-helmet source-map-support stack-utils

* chore: upgrade uuid

* chore: upgrade @babel packages

* chore: upgrade types

* chore: upgrade autoprefixer

* chore: upgrade jest

* chore: upgrade ts-jest

* chore: remove linkify.d.ts

* chore: upgrade bowser

* chore: case-sensitive-paths-webpack-plugin

* chore: upgrade classnames

* chore: upgrade commander

* chore: upgrade comment-json

* chore: upgrade cross-spawn compression-webpack-plugin del

* chore: upgrade build and watch related dependencies

* chore: upgrade css-vars-ponyfill

* chore: upgrade eslint and css-vars-ponyfill

* chore: upgrade enzyme and eventemitter2

* fix: form bug

* chore: upgrade farce

* chore: upgrade final form

* chore: upgrade react-popper

* chore: upgrade flat and fork-ts-checker-webpack-plugin

* chore: upgrade husky and gulp related, intersection observer

* chore: upgrade lint-staged

* chore: upgrade marked, loader-utils, mini-css-extract-plugin

* chore: upgrade postcss-nested, proxy-polyfill, pstree.remy

* chore: upgrade prettier

* chore: fix prettier changes, upgrade react

* chore: mute createFactory deprecated message

* chore: upgrade react-copy-to-clipbard, react-axe, react-dom, react-test-renderer, react-timeago

* chore: upgrade react-transistion-group, react-responsive

* chore: upgrade types

* chore: upgrade react-dev-utils, react-error-overlay regenerator-runtime

* chore: upgrade types, sinon, sockjs-client, strip-ansi

* chore: upgrade types, fonts

* chore: upgrade nunjucks, ts-node, typescript-snapshot-plugin, wait-for-expect

* chore: upgrade eslint packages

* chore: upgrade fluent, types

* chore: upgrade jsdom dep

* chore: upgrade mongo

* chore: upgrade deps

* chore: upgrade typescript, recompose

* chore: upgrade prettier

* chore: remove obsolete prettier config

* chore: upgrade jsdom types

* chore: upgrade typescript-eslint

* chore: upgrad deps

* chore: upgrade deps

* chore: upgrade relay related modules

* chore: upgrade docz WIP

* chore: upgrade docz

* chore: add guard

* chore: remove obsolete line

* chore: comment

* chore: refactors

* fix: hook count change error
2020-04-15 18:15:31 +02:00

404 lines
13 KiB
TypeScript

import cluster from "cluster";
import express, { Express } from "express";
import { GraphQLSchema } from "graphql";
import { RedisPubSub } from "graphql-redis-subscriptions";
import http from "http";
import { Db } from "mongodb";
import { AggregatorRegistry, collectDefaultMetrics } from "prom-client";
import { SubscriptionServer } from "subscriptions-transport-ws";
import { LanguageCode } from "coral-common/helpers/i18n/locales";
import { AppOptions, createApp, listenAndServe } from "coral-server/app";
import { basicAuth } from "coral-server/app/middleware/basicAuth";
import { noCacheMiddleware } from "coral-server/app/middleware/cacheHeaders";
import { JSONErrorHandler } from "coral-server/app/middleware/error";
import { accessLogger, errorLogger } from "coral-server/app/middleware/logging";
import { notFoundMiddleware } from "coral-server/app/middleware/notFound";
import config, { Config } from "coral-server/config";
import startScheduledTasks, { ScheduledJobGroups } from "coral-server/cron";
import getTenantSchema from "coral-server/graph/schema";
import { createPubSubClient } from "coral-server/graph/subscriptions/pubsub";
import { createSubscriptionServer } from "coral-server/graph/subscriptions/server";
import logger from "coral-server/logger";
import { createQueue, TaskQueue } from "coral-server/queue";
import { I18n } from "coral-server/services/i18n";
import {
createJWTSigningConfig,
JWTSigningConfig,
} from "coral-server/services/jwt";
import { createMetrics } from "coral-server/services/metrics";
import { MigrationManager } from "coral-server/services/migrate";
import { createMongoDB } from "coral-server/services/mongodb";
import { PersistedQueryCache } from "coral-server/services/queries";
import {
AugmentedRedis,
createAugmentedRedisClient,
createRedisClient,
} from "coral-server/services/redis";
import TenantCache from "coral-server/services/tenant/cache";
import {
NotifierCoralEventListener,
PerspectiveCoralEventListener,
SlackCoralEventListener,
SubscriptionCoralEventListener,
WebhookCoralEventListener,
} from "./events/listeners";
import CoralEventListenerBroker from "./events/publisher";
import { isInstalled } from "./services/tenant";
export interface ServerOptions {
/**
* config when specified will specify the configuration to load.
*/
config?: Config;
}
export interface ServerStartOptions {
parent?: Express;
}
/**
* Server provides an interface to create, start, and manage a Coral Server.
*/
class Server {
// parentApp is the root application that the server will bind to.
private parentApp: Express;
// schema is the GraphQL Schema that relates to the given Tenant.
private schema: GraphQLSchema;
// 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;
// subscriptionServer is the running instance of the HTTP server that will
// bind to the requested port to serve websocket traffic.
public subscriptionServer: SubscriptionServer;
// scheduledTasks are tasks managed by cron.
public scheduledTasks: ScheduledJobGroups;
// tasks stores a reference to the queues that can process operations.
private tasks: TaskQueue;
// redis stores the redis connection used by the application.
private redis: AugmentedRedis;
// pubsub stores the pubsub engine used by the application.
private pubsub: RedisPubSub;
// mongo stores the mongo connection used by the application.
private mongo: Db;
// tenantCache stores the tenant cache used by the application.
private tenantCache: TenantCache;
// connected when true, indicates that `connect()` was already called.
private connected = false;
// processing when true, indicates that `process()` was already called.
private processing = false;
// i18n is the server reference to the i18n framework.
private i18n: I18n;
// signingConfig is the server reference to the signing configuration.
private signingConfig: JWTSigningConfig;
// persistedQueryCache is the cache of persisted queries used by the GraphQL
// server to handle persisted queries.
private persistedQueryCache: PersistedQueryCache;
// migrationManager is the manager for performing migrations on Coral.
private migrationManager: MigrationManager;
/**
* broker stores a reference to all of the listeners that can be used in
* conjunction with an event to publish activity occurring inside Coral.
*/
private broker: CoralEventListenerBroker;
constructor(options: ServerOptions) {
this.parentApp = express();
// Load the configuration.
this.config = config
.load(options.config || {})
.validate({ allowed: "strict" });
logger.debug({ config: this.config.toString() }, "loaded configuration");
// Do some extra validation for production.
if (this.config.get("env") === "production") {
// Ensure that the signing secret has been specified.
if (
this.config.get("signing_secret") ===
this.config.default("signing_secret")
) {
throw new Error(
"SIGNING_SECRET is required in production environments"
);
}
}
// Load the graph schemas.
this.schema = getTenantSchema();
// Get the default locale. This is asserted here because the LanguageCode
// is verified via Convict, but not typed, so this resolves that.
const defaultLocale = this.config.get("default_locale") as LanguageCode;
// Setup the translation framework.
this.i18n = new I18n(defaultLocale);
// Create the signing config.
this.signingConfig = createJWTSigningConfig(
this.config.get("signing_secret"),
this.config.get("signing_algorithm")
);
}
/**
* connect will connect to all the databases and start priming data needed for
* runtime.
*/
public async connect() {
// Guard against double connecting.
if (this.connected) {
throw new Error("server has already connected");
}
this.connected = true;
// Load the translations.
await this.i18n.load();
// Setup MongoDB.
this.mongo = await createMongoDB(config);
// Setup Redis.
this.redis = await createAugmentedRedisClient(config);
// Create the TenantCache.
this.tenantCache = new TenantCache(
this.mongo,
createRedisClient(this.config),
config
);
// Create the migration manager.
this.migrationManager = new MigrationManager({
tenantCache: this.tenantCache,
i18n: this.i18n,
});
// Load and upsert the persisted queries.
this.persistedQueryCache = new PersistedQueryCache({ mongo: this.mongo });
// Prime the tenant cache so it'll be ready to serve now.
await this.tenantCache.primeAll();
// Create the Job Queue.
this.tasks = createQueue({
config: this.config,
mongo: this.mongo,
redis: this.redis,
tenantCache: this.tenantCache,
i18n: this.i18n,
signingConfig: this.signingConfig,
});
// Create the pubsub client.
this.pubsub = createPubSubClient(
createRedisClient(this.config),
createRedisClient(this.config)
);
// Setup the broker.
this.broker = new CoralEventListenerBroker();
this.broker.register(new NotifierCoralEventListener(this.tasks.notifier));
this.broker.register(new SlackCoralEventListener());
this.broker.register(new SubscriptionCoralEventListener());
this.broker.register(new WebhookCoralEventListener(this.tasks.webhook));
this.broker.register(new PerspectiveCoralEventListener());
// Setup the metrics collectors.
collectDefaultMetrics();
}
/**
* process will start the job processors and ancillary operations.
*/
public async process() {
// Guard against double connecting.
if (this.processing) {
throw new Error("server has already processing");
}
this.processing = true;
// Run migrations if there is already a Tenant installed.
if (await isInstalled(this.tenantCache)) {
await this.migrationManager.executePendingMigrations(
this.mongo,
this.redis
);
await this.tenantCache.primeAll();
} else {
logger.info("no tenants are installed, skipping running migrations");
}
// Prime the queries in the database.
await this.persistedQueryCache.prime();
// Launch all of the job processors.
if (!this.config.get("disable_job_processors")) {
logger.info("job processing is enabled, starting job processors");
this.tasks.mailer.process();
this.tasks.scraper.process();
this.tasks.notifier.process();
this.tasks.webhook.process();
this.tasks.rejector.process();
} else {
logger.info("job processing is disabled, not starting job processors");
}
// Start up the cron job processors.
this.scheduledTasks = startScheduledTasks({
mongo: this.mongo,
config: this.config,
mailerQueue: this.tasks.mailer,
tenantCache: this.tenantCache,
signingConfig: this.signingConfig,
});
// If we are running in concurrency mode, and we are the master, we should
// setup the aggregator for the cluster metrics.
if (cluster.isMaster && this.config.get("concurrency") > 1) {
// Create the aggregator registry for metrics.
const aggregatorRegistry = new AggregatorRegistry();
// Setup the cluster metrics server.
const metricsServer = express();
// Setup access logger.
metricsServer.use(accessLogger);
// Add basic auth if provided.
const username = this.config.get("metrics_username");
const password = this.config.get("metrics_password");
if (username && password) {
metricsServer.use("/cluster_metrics", basicAuth(username, password));
logger.info("adding authentication to metrics endpoint");
} else {
logger.info(
"not adding authentication to metrics endpoint, credentials not provided"
);
}
// Cluster metrics will be served on /cluster_metrics.
metricsServer.get(
"/cluster_metrics",
noCacheMiddleware,
(req, res, next) => {
aggregatorRegistry.clusterMetrics((err, metrics) => {
if (err) {
return next(err);
}
res.set("Content-Type", aggregatorRegistry.contentType);
res.send(metrics);
});
}
);
// Error handling.
metricsServer.use(notFoundMiddleware);
metricsServer.use(errorLogger);
metricsServer.use(JSONErrorHandler());
const port = this.config.get("cluster_metrics_port");
// Star the server listening for cluster metrics.
await listenAndServe(metricsServer, port);
logger.info(
{ port, path: "/cluster_metrics" },
"now listening for cluster metrics"
);
}
}
/**
* 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 }: ServerStartOptions) {
// Guard against not being connected.
if (!this.connected) {
throw new Error("server has not connected yet");
}
const port = this.config.get("port");
// Ensure we have an app to bind to.
parent = parent ? parent : this.parentApp;
// Disables the client routes to serve bundles etc. Useful for developing with
// Webpack Dev Server.
const disableClientRoutes = this.config.get("disable_client_routes");
const options: AppOptions = {
parent,
broker: this.broker,
pubsub: this.pubsub,
mongo: this.mongo,
redis: this.redis,
signingConfig: this.signingConfig,
tenantCache: this.tenantCache,
config: this.config,
schema: this.schema,
i18n: this.i18n,
mailerQueue: this.tasks.mailer,
scraperQueue: this.tasks.scraper,
rejectorQueue: this.tasks.rejector,
webhookQueue: this.tasks.webhook,
notifierQueue: this.tasks.notifier,
disableClientRoutes,
persistedQueryCache: this.persistedQueryCache,
persistedQueriesRequired:
this.config.get("env") === "production" &&
!this.config.get("enable_graphiql"),
migrationManager: this.migrationManager,
};
// Only enable the metrics server if concurrency is set to 1.
if (this.config.get("concurrency") === 1) {
options.metrics = createMetrics();
}
// Create the Coral App, branching off from the parent app.
const app: Express = await createApp(options);
// Start the application and store the resulting http.Server. The server
// will return when the server starts listening. The NodeJS application will
// not exit until all tasks are handled, which for an open socket, is never.
this.httpServer = await listenAndServe(app, port);
// Setup subscriptions and attach it to the httpServer.
this.subscriptionServer = createSubscriptionServer(
this.httpServer,
this.schema,
options
);
logger.info({ port }, "now listening");
}
}
export default Server;