refactored design

This commit is contained in:
Wyatt Johnson
2018-06-18 15:50:42 -06:00
parent 7177988059
commit 1917a17929
10 changed files with 194 additions and 120 deletions
+12 -8
View File
@@ -1,19 +1,23 @@
# HTTP Routes
# Design
## Stream API
## HTTP Routes
/api/tenant/:tenantID/graphql
/api/tenant/:tenantID/auth
### Stream API
## Tenant Management API
/api/tenant/graphql
/api/tenant/auth
/api/graphql
/api/auth
### Tenant Management API
# Folder structure
/api/management/graphql
/api/management/auth
## Folder structure
```text
/graph/tenant <-- tenant's api (comments, assets, ...)
/graph/management <-- tenant management api
```
1. No tenants
2. Create a tenant <-- consuming the TMA
+54 -34
View File
@@ -1,78 +1,86 @@
import express, { Express, Router } from 'express';
import { Db } from 'mongodb';
import http from 'http';
import { Config } from 'talk-server/config';
import { create } from 'talk-server/services/mongodb';
import tenantGraphMiddleware from 'talk-server/graph/tenant/middleware';
import managementGraphMiddleware from 'talk-server/graph/management/middleware';
import serveStatic from './middleware/serveStatic';
import playground from './middleware/playground';
import {
access as accessLogger,
error as errorLogger,
} from './middleware/logging';
import tenantGraphMiddleware from 'talk-server/graph/tenant/middleware';
import managementGraphMiddleware from 'talk-server/graph/management/middleware';
import { Redis } from 'ioredis';
async function createManagementRouter(config: Config, db: Db): Promise<Router> {
export interface AppOptions {
config: Config;
mongo: Db;
redis: Redis;
}
async function createManagementRouter(opts: AppOptions): Promise<Router> {
const router = express.Router();
if (config.get('env') === 'development') {
if (opts.config.get('env') === 'development') {
// GraphiQL
router.get(
'/graphiql',
playground(() => ({
endpoint: `/api/management/graphql`,
}))
playground({
endpoint: '/api/management/graphql',
})
);
}
// Tenant API
router.use('/graphql', express.json(), managementGraphMiddleware(db));
// Management API
router.use(
'/graphql',
express.json(),
managementGraphMiddleware(opts.mongo)
);
return router;
}
async function createTenantRouter(config: Config, db: Db): Promise<Router> {
const router = express.Router({ mergeParams: true });
async function createTenantRouter(opts: AppOptions): Promise<Router> {
const router = express.Router();
if (config.get('env') === 'development') {
if (opts.config.get('env') === 'development') {
// GraphiQL
router.get(
'/graphiql',
playground(req => ({
endpoint: `/api/tenant/${req.params.tenantID}/graphql`,
}))
playground({
endpoint: '/api/tenant/graphql',
})
);
}
// Tenant API
router.use('/graphql', express.json(), tenantGraphMiddleware(db));
router.use('/graphql', express.json(), tenantGraphMiddleware(opts.mongo));
return router;
}
async function createAPIRouter(config: Config, db: Db): Promise<Router> {
async function createAPIRouter(opts: AppOptions): Promise<Router> {
// Create a router.
const router = express.Router({ mergeParams: true });
const router = express.Router();
// Configure the tenant routes.
router.use('/tenant/:tenantID', await createTenantRouter(config, db));
router.use('/tenant', await createTenantRouter(opts));
// Configure the management routes.
router.use('/management', await createManagementRouter(config, db));
router.use('/management', await createManagementRouter(opts));
return router;
}
async function createRouter(config: Config): Promise<Router> {
// Setup MongoDB.
const db = await create(config);
async function createRouter(opts: AppOptions): Promise<Router> {
// Create a router.
const router = express.Router({ mergeParams: true });
const router = express.Router();
router.use('/api', await createAPIRouter(config, db));
router.use('/api', await createAPIRouter(opts));
return router;
}
@@ -83,20 +91,32 @@ async function createRouter(config: Config): Promise<Router> {
* @param parent the root application to attach the Talk routes/middleware to.
*/
export async function createApp(
app: Express,
config: Config
parent: Express,
options: AppOptions
): Promise<Express> {
// Logging
app.use(accessLogger);
parent.use(accessLogger);
// Static Files
app.use(serveStatic);
parent.use(serveStatic);
// Mount the router.
app.use(await createRouter(config));
parent.use(await createRouter(options));
// Error Handling
app.use(errorLogger);
parent.use(errorLogger);
return app;
return parent;
}
/**
* startApp will start the given express application.
*
* @param port the port to listen on
* @param app the express application to start
*/
export const startApp = (port: number, app: Express): Promise<http.Server> =>
new Promise(async resolve => {
// Listen on the designated port.
const httpServer = app.listen(port, () => resolve(httpServer));
});
+1 -13
View File
@@ -1,16 +1,4 @@
import { Request, RequestHandler } from 'express';
import { MiddlewareOptions } from 'graphql-playground-html';
import playground from 'graphql-playground-middleware-express';
export type PlaygroundFn = (req: Request) => MiddlewareOptions;
export default (fn: PlaygroundFn): RequestHandler => (req, res, next) => {
// Generate the options.
const options: MiddlewareOptions = fn(req);
// Create the playground handler.
const handler = playground(options);
// Execute it.
handler(req, res, next);
};
export default (options: MiddlewareOptions) => playground(options);
+3 -3
View File
@@ -1,13 +1,13 @@
import { Db } from 'mongodb';
export interface ContextOptions {
export interface ManagementContextOptions {
db: Db;
}
export default class TenantContext {
export default class ManagementContext {
public db: Db;
constructor({ db }: ContextOptions) {
constructor({ db }: ManagementContextOptions) {
this.db = db;
}
}
@@ -0,0 +1,5 @@
import Cursor from '../../common/scalars/cursor';
export default {
Cursor,
};
@@ -1,18 +1,15 @@
import { addMockFunctionsToSchema } from 'graphql-tools';
import { addResolveFunctionsToSchema } from 'graphql-tools';
import { getGraphQLProjectConfig } from 'graphql-config';
import resolvers from '../resolvers';
// Load the configuration from the provided `.graphqlconfig` file.
const config = getGraphQLProjectConfig(__dirname, 'management');
// Get the GraphQLSchema from the configuration.
const schema = config.getSchema();
// Attach resolvers to the schema.
addMockFunctionsToSchema({
schema,
mocks: {
Cursor: () => new Date().toISOString(),
},
}); // FIXME: remove mocks
// Attach the resolvers to the schema.
addResolveFunctionsToSchema({ schema, resolvers });
export default schema;
+2 -2
View File
@@ -2,7 +2,7 @@ import loaders from './loaders';
import { Db } from 'mongodb';
import { Tenant } from 'talk-server/models/tenant';
export interface ContextOptions {
export interface TenantContextOptions {
tenant?: Tenant;
db: Db;
}
@@ -12,7 +12,7 @@ export default class TenantContext {
public db: Db;
public tenant?: Tenant;
constructor({ tenant, db }: ContextOptions) {
constructor({ tenant, db }: TenantContextOptions) {
this.tenant = tenant;
this.loaders = loaders(this);
this.db = db;
+3 -5
View File
@@ -1,10 +1,8 @@
import {
addMockFunctionsToSchema,
addResolveFunctionsToSchema,
} from 'graphql-tools';
import resolvers from '../resolvers';
import { addResolveFunctionsToSchema } from 'graphql-tools';
import { getGraphQLProjectConfig } from 'graphql-config';
import resolvers from '../resolvers';
// Load the configuration from the provided `.graphqlconfig` file.
const config = getGraphQLProjectConfig(__dirname, 'tenant');
+33 -18
View File
@@ -1,28 +1,34 @@
import config, { Config } from './config';
import express, { Express } from 'express';
import http from 'http';
import { createApp } from './app';
import { createApp, startApp } from './app';
import logger from './logger';
import { create as createMongoDB } from './services/mongodb';
import { create as createRedis } from 'talk-server/services/redis';
export interface ServerOptions {}
export interface ServerOptions {
config?: Config;
}
/**
* Server provides an interface to create, start, and manage a Talk Server.
*/
class Server {
// app is the root application that the server will bind to.
private app: Express;
// parentApp is the root application that the server will bind to.
private parentApp: Express;
// config exposes application specific configuration.
public config: Config;
// httpServer is the running instance of the HTTP server that will bind to the
// requested port.
// httpServer is the running instance of the HTTP server that will bind to
// the requested port.
public httpServer: http.Server;
constructor(options: ServerOptions) {
this.app = express();
this.config = config.validate({ allowed: 'strict' });
this.parentApp = express();
this.config = config
.load(options.config || {})
.validate({ allowed: 'strict' });
}
/**
@@ -31,21 +37,30 @@ class Server {
*
* @param parent the optional express application to bind the server to.
*/
start = (parent?: Express): Promise<Server> =>
new Promise(async resolve => {
const port = this.config.get('port');
public async start(parent?: Express) {
const port = this.config.get('port');
// Ensure we have an app to bind to.
parent = parent ? parent : this.app;
// Ensure we have an app to bind to.
parent = parent ? parent : this.parentApp;
// Create the Talk App, branching off from the parent app.
const app: Express = await createApp(parent, this.config);
// Setup MongoDB.
const mongo = await createMongoDB(config);
logger.info({ port }, 'now listening');
// Setup Redis.
const redis = await createRedis(config);
// Listen on the designated port.
this.httpServer = app.listen(port, () => resolve(this));
// Create the Talk App, branching off from the parent app.
const app = await createApp(parent, {
mongo,
redis,
config: this.config,
});
// Start the application.
this.httpServer = await startApp(port, app);
logger.info({ port }, 'now listening');
}
}
export default Server;
+76 -29
View File
@@ -1,8 +1,8 @@
import { Db, Collection } from 'mongodb';
import { defaultsDeep } from 'lodash';
import { merge } from 'lodash';
import dotize from 'dotize';
import uuid from 'uuid';
import { Omit } from 'talk-common/types';
import { Omit, Sub } from 'talk-common/types';
function collection(db: Db): Collection<Tenant> {
return db.collection<Tenant>('tenants');
@@ -25,6 +25,10 @@ export enum Moderation {
export interface Tenant {
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;
moderation: Moderation;
requireEmailConfirmation: boolean;
infoBoxEnable: boolean;
@@ -45,8 +49,8 @@ export interface Tenant {
editCommentWindowLength: number;
charCountEnable: boolean;
charCount?: number;
organizationName?: string;
organizationContactEmail?: string;
organizationName: string;
organizationContactEmail: string;
// wordlist stores all the banned/suspect words.
wordlist: Wordlist;
@@ -55,41 +59,67 @@ export interface Tenant {
domains: string[];
}
export type CreateTenantInput = Omit<Tenant, 'id'>;
const defaults: CreateTenantInput = {
// Default to post moderation.
moderation: Moderation.POST,
// 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: [],
},
domains: [],
};
/**
* CreateTenantInput is the set of properties that can be set when a given
* Tenant is created. The remainder of the properties are set from defaults and
* are modifiable via the update method.
*/
export type CreateTenantInput = Pick<
Tenant,
'domain' | 'organizationName' | 'organizationContactEmail' | 'domains'
>;
/**
* create will create a new Tenant.
*
* @param db the MongoDB connection used to create the tenant.
* @param input the customizable parts of the Tenant available during creation
*/
export async function create(
db: Db,
input: Partial<CreateTenantInput>
input: CreateTenantInput
): Promise<Readonly<Tenant>> {
const tenant = defaultsDeep({ id: uuid.v4() }, input, defaults);
const defaults: Sub<Tenant, CreateTenantInput> = {
// Create a new ID.
id: uuid.v4(),
// Default to post moderation.
moderation: Moderation.POST,
// 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: [],
},
};
// 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);
return tenant;
}
export async function retrieveByDomain(
db: Db,
domain: string
): Promise<Readonly<Tenant>> {
return collection(db).findOne({ domain });
}
export async function retrieve(db: Db, id: string): Promise<Readonly<Tenant>> {
return collection(db).findOne({ id });
}
@@ -109,6 +139,23 @@ export async function retrieveMany(
return ids.map(id => tenants.find(tenant => tenant.id === id));
}
export async function retrieveManyByDomain(
db: Db,
domains: string[]
): Promise<Readonly<Tenant>[]> {
const cursor = await collection(db).find({
domain: {
$in: domains,
},
});
const tenants = await cursor.toArray();
return domains.map(domain =>
tenants.find(tenant => tenant.domain === domain)
);
}
export async function retrieveAll(db: Db): Promise<Readonly<Tenant>[]> {
return collection(db)
.find({})