mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 18:07:26 +08:00
refactored design
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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({})
|
||||
|
||||
Reference in New Issue
Block a user