diff --git a/.gitignore b/.gitignore index f077fd4d3..25251254a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist .env -*.js \ No newline at end of file +*.js +yarn.lock \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c01e4d9e4..207de292a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -173,6 +173,16 @@ "@types/node": "*" } }, + "@types/ws": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-5.1.2.tgz", + "integrity": "sha512-NkTXUKTYdXdnPE2aUUbGOXE1XfMK527SCvU/9bj86kyFF6kZ9ZnOQ3mK5jADn98Y2vEUD/7wKDgZa7Qst2wYOg==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -226,9 +236,9 @@ }, "dependencies": { "@types/node": { - "version": "9.6.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.21.tgz", - "integrity": "sha512-zQS6mHzxEstR8Vvnpc3JDUCDGWnHFzMTcBu9UCZoVLuj1Uvkkk0qFKJQEhlvbsX34m3xt12ejV09eO/ljZcn7A==" + "version": "9.6.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.22.tgz", + "integrity": "sha512-RIg9EkxzVMkNH0M4sLRngK23f5QiigJC0iODQmu4nopzstt8AjegYund3r82iMrd2BNCjcZVnklaItvKHaGfBA==" } } }, @@ -265,9 +275,12 @@ } }, "apollo-utilities": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.0.13.tgz", - "integrity": "sha512-WIbKDsFsLXMgPPGmlB2pL2noHT13TOLU2eRSyPjQMQwcz9lO9UKRkwK8pRJ8b4Zo/9qQHm30F/xlfP/OSr2ZSw==" + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.0.16.tgz", + "integrity": "sha512-5oKnElKqkV920KRbitiyISLeG63tUGAyNdotg58bQSX9Omr+smoNDTIRMRLbyIdKOYLaw3LpDaRepOPqljj0NQ==", + "requires": { + "fast-json-stable-stringify": "^2.0.0" + } }, "argparse": { "version": "1.0.10", @@ -324,12 +337,22 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "atob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", "dev": true }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -941,6 +964,11 @@ "through": "~2.3.1" } }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==" + }, "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", @@ -1122,6 +1150,11 @@ } } }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1874,6 +1907,16 @@ "graphql-playground-html": "1.6.0" } }, + "graphql-redis-subscriptions": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.5.0.tgz", + "integrity": "sha512-4R/rv3qg61/UuB/9enCdWJM9s4x6TRwXYubjAlPWXJuNhGcZXn6oELu9mrhm+8QuA924/GvOo8Z7hCqE617SeQ==", + "requires": { + "graphql-subscriptions": "^0.5.6", + "ioredis": "^3.1.2", + "iterall": "^1.1.3" + } + }, "graphql-request": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.6.0.tgz", @@ -1882,6 +1925,14 @@ "cross-fetch": "2.0.0" } }, + "graphql-subscriptions": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz", + "integrity": "sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ==", + "requires": { + "iterall": "^1.2.1" + } + }, "graphql-tools": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-3.0.2.tgz", @@ -2355,6 +2406,16 @@ "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" }, + "lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, "lodash.keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.2.0.tgz", @@ -3460,6 +3521,26 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "subscriptions-transport-ws": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.11.tgz", + "integrity": "sha512-B8fwTIJy2buUcBXM6Ffbax30XcEqvCqL8RXwbivBAiB3X9ezrTcF5nYMmNGZ47sxrDYA1XfQ5W3aTgJEm8BFJA==", + "requires": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "lodash.assign": "^4.2.0", + "lodash.isobject": "^3.0.2", + "lodash.isstring": "^4.0.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -3836,6 +3917,14 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.0.tgz", + "integrity": "sha512-c18dMeW+PEQdDFzkhDsnBAlS4Z8KGStBQQUcQ5mf7Nf689jyGk0594L+i9RaQuf4gog6SvWLJorz2NfSaqxZ7w==", + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index f50adf144..ad3248aa7 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "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", @@ -28,6 +29,7 @@ "luxon": "^1.2.1", "mongodb": "^3.0.10", "performance-now": "^2.1.0", + "subscriptions-transport-ws": "^0.9.11", "uuid": "^3.2.1" }, "devDependencies": { @@ -42,7 +44,7 @@ "@types/luxon": "^0.5.3", "@types/mongodb": "^3.0.19", "@types/uuid": "^3.4.3", - "graphql-playground-html": "^1.6.0", + "@types/ws": "^5.1.2", "graphql-playground-middleware-express": "^1.7.0", "nodemon": "^1.17.5", "prettier": "^1.13.4", diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 6766cced7..3d9ff8036 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -1,99 +1,34 @@ -import express, { Express, Router } from 'express'; +import { Express } from 'express'; import { Db } from 'mongodb'; import http from 'http'; +import { Redis } from 'ioredis'; 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 { 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 playground from './middleware/playground'; import { access as accessLogger, error as errorLogger, } from './middleware/logging'; -import { Redis } from 'ioredis'; export interface AppOptions { + parent: Express; config: Config; mongo: Db; redis: Redis; -} - -async function createManagementRouter(opts: AppOptions): Promise { - const router = express.Router(); - - if (opts.config.get('env') === 'development') { - // GraphiQL - router.get( - '/graphiql', - playground({ - endpoint: '/api/management/graphql', - }) - ); - } - - // Management API - router.use( - '/graphql', - express.json(), - managementGraphMiddleware(opts.mongo) - ); - - return router; -} - -async function createTenantRouter(opts: AppOptions): Promise { - const router = express.Router(); - - if (opts.config.get('env') === 'development') { - // GraphiQL - router.get( - '/graphiql', - playground({ - endpoint: '/api/tenant/graphql', - }) - ); - } - - // Tenant API - router.use('/graphql', express.json(), tenantGraphMiddleware(opts.mongo)); - - return router; -} - -async function createAPIRouter(opts: AppOptions): Promise { - // Create a router. - const router = express.Router(); - - // Configure the tenant routes. - router.use('/tenant', await createTenantRouter(opts)); - - // Configure the management routes. - router.use('/management', await createManagementRouter(opts)); - - return router; -} - -async function createRouter(opts: AppOptions): Promise { - // Create a router. - const router = express.Router(); - - router.use('/api', await createAPIRouter(opts)); - - return router; + schemas: Schemas; } /** * createApp will create a Talk Express app that can be used to handle requests. - * - * @param parent the root application to attach the Talk routes/middleware to. */ -export async function createApp( - parent: Express, - options: AppOptions -): Promise { +export async function createApp(options: AppOptions): Promise { + // Pull the parent out of the options. + const { parent } = options; + // Logging parent.use(accessLogger); @@ -110,13 +45,40 @@ export async function createApp( } /** - * startApp will start the given express application. + * listenAndServe will start the given express application. * - * @param port the port to listen on * @param app the express application to start + * @param port the port to listen on */ -export const startApp = (port: number, app: Express): Promise => +export const listenAndServe = ( + app: Express, + port: number +): Promise => 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 + * handle websocket traffic by upgrading their http connections to websocket. + * + * @param schemas schemas for every schema this application handles + * @param server the http.Server to attach the websocket upgraders to + */ +export async function attachSubscriptionHandlers( + schemas: Schemas, + server: http.Server +) { + // 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', + }); +} diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts new file mode 100644 index 000000000..6c9db8a08 --- /dev/null +++ b/src/core/server/app/router.ts @@ -0,0 +1,85 @@ +import express, { Router } from 'express'; + +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'; + +async function createManagementRouter(opts: AppOptions) { + 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 + ) + ); + + return router; +} + +async function createTenantRouter(opts: AppOptions) { + 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 + ) + ); + + return router; +} + +async function createAPIRouter(opts: AppOptions) { + // Create a router. + const router = express.Router(); + + // Configure the tenant routes. + router.use('/tenant', await createTenantRouter(opts)); + + // Configure the management routes. + router.use('/management', await createManagementRouter(opts)); + + return router; +} + +export async function createRouter(opts: AppOptions) { + // Create a router. + const router = express.Router(); + + router.use('/api', await createAPIRouter(opts)); + + return router; +} diff --git a/src/core/server/config.ts b/src/core/server/config.ts index ac093bf44..fd40913fb 100644 --- a/src/core/server/config.ts +++ b/src/core/server/config.ts @@ -32,7 +32,7 @@ convict.addFormat({ }, }); -export const config = convict({ +const config = convict({ env: { doc: 'The application environment.', format: ['production', 'development', 'test'], @@ -67,6 +67,13 @@ export const config = convict({ env: 'SECRET', arg: 'secret', }, + logging_level: { + doc: 'The logging level to print to the console', + format: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'], + default: 'info', + env: 'LOGGING_LEVEL', + arg: 'logging', + }, }); export type Config = typeof config; diff --git a/src/core/server/graph/common/middleware/index.ts b/src/core/server/graph/common/middleware/index.ts new file mode 100644 index 000000000..c3d0a6a24 --- /dev/null +++ b/src/core/server/graph/common/middleware/index.ts @@ -0,0 +1,52 @@ +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'; + +// 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] + ) + ); + } + }, +}); + +export const graphqlMiddleware = ( + config: Config, + baseOptions: GraphQLOptions | ExpressGraphQLOptionsFunction +) => { + // Generate the validation rules. + const validationRules: Array<(context: ValidationContext) => any> = []; + + 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 + ); + + // 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; + }); +}; diff --git a/src/core/server/graph/common/schema/index.ts b/src/core/server/graph/common/schema/index.ts new file mode 100644 index 000000000..222599952 --- /dev/null +++ b/src/core/server/graph/common/schema/index.ts @@ -0,0 +1,15 @@ +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); + + // Get the GraphQLSchema from the configuration. + const schema = config.getSchema(); + + // Attach the resolvers to the schema. + addResolveFunctionsToSchema({ schema, resolvers }); + + return schema; +} diff --git a/src/core/server/graph/common/subscriptions/middleware.ts b/src/core/server/graph/common/subscriptions/middleware.ts new file mode 100644 index 000000000..a1bc2d921 --- /dev/null +++ b/src/core/server/graph/common/subscriptions/middleware.ts @@ -0,0 +1,29 @@ +import http from 'http'; +import { SubscriptionServer } from 'subscriptions-transport-ws'; +import { GraphQLSchema, execute, subscribe } from 'graphql'; + +export interface SubscriptionMiddlewareOptions { + schema: GraphQLSchema; + path: string; +} + +export function handleSubscriptions( + server: http.Server, + { schema, path }: SubscriptionMiddlewareOptions +): SubscriptionServer { + // 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, + }; + + return new SubscriptionServer(options, socketOption); +} diff --git a/src/core/server/graph/common/subscriptions/pubsub.ts b/src/core/server/graph/common/subscriptions/pubsub.ts new file mode 100644 index 000000000..f858f510f --- /dev/null +++ b/src/core/server/graph/common/subscriptions/pubsub.ts @@ -0,0 +1,15 @@ +import { RedisPubSub } from 'graphql-redis-subscriptions'; +import { createRedisClient } from 'talk-server/services/redis'; +import { Config } from 'talk-server/config'; + +export async function createPubSub(config: Config): Promise { + // Create the Redis clients for the PubSub server. + const publisher = await createRedisClient(config); + const subscriber = await createRedisClient(config); + + // Create the new PubSub manager. + return new RedisPubSub({ + publisher, + subscriber, + }); +} diff --git a/src/core/server/graph/management/middleware.ts b/src/core/server/graph/management/middleware.ts index 95013eba2..d33a6f4cd 100644 --- a/src/core/server/graph/management/middleware.ts +++ b/src/core/server/graph/management/middleware.ts @@ -1,12 +1,13 @@ -import { graphqlExpress } from 'apollo-server-express'; -import schema from './schema'; -import Context from './context'; import { Db } from 'mongodb'; +import { GraphQLSchema } from 'graphql'; -export default (db: Db) => - graphqlExpress(async req => { - return { - schema, - context: new Context({ db }), - }; - }); +import { graphqlMiddleware } from 'talk-server/graph/common/middleware'; +import { Config } from 'talk-server/config'; + +import Context from './context'; + +export default (schema: GraphQLSchema, config: Config, db: Db) => + graphqlMiddleware(config, async () => ({ + schema, + context: new Context({ db }), + })); diff --git a/src/core/server/graph/management/schema/index.ts b/src/core/server/graph/management/schema/index.ts index 5db233b2d..8a616743a 100644 --- a/src/core/server/graph/management/schema/index.ts +++ b/src/core/server/graph/management/schema/index.ts @@ -1,15 +1,6 @@ -import { addResolveFunctionsToSchema } from 'graphql-tools'; -import { getGraphQLProjectConfig } from 'graphql-config'; +import loadSchema from 'talk-server/graph/common/schema'; +import resolvers from 'talk-server/graph/management/resolvers'; -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 the resolvers to the schema. -addResolveFunctionsToSchema({ schema, resolvers }); - -export default schema; +export default function getManagementSchema() { + return loadSchema('management', resolvers); +} diff --git a/src/core/server/graph/schemas.ts b/src/core/server/graph/schemas.ts new file mode 100644 index 000000000..729d4d9c6 --- /dev/null +++ b/src/core/server/graph/schemas.ts @@ -0,0 +1,6 @@ +import { GraphQLSchema } from 'graphql'; + +export interface Schemas { + management: GraphQLSchema; + tenant: GraphQLSchema; +} diff --git a/src/core/server/graph/tenant/middleware.ts b/src/core/server/graph/tenant/middleware.ts index edc028608..5835195d7 100644 --- a/src/core/server/graph/tenant/middleware.ts +++ b/src/core/server/graph/tenant/middleware.ts @@ -1,13 +1,24 @@ -import { graphqlExpress } from 'apollo-server-express'; -import schema from './schema'; -import TenantContext from './context'; import { Db } from 'mongodb'; -import { Tenant } from 'talk-server/models/tenant'; +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 TenantContext from './context'; + +export default async (schema: GraphQLSchema, config: Config, db: Db) => { + // 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); -export default (db: Db) => - graphqlExpress(async req => { return { schema, - context: new TenantContext({ db, tenant: { id: '1' } as Tenant }), + context: new TenantContext({ db, tenant }), }; }); +}; diff --git a/src/core/server/graph/tenant/resolvers/query.ts b/src/core/server/graph/tenant/resolvers/query.ts index d62313ff3..9c3a7167f 100644 --- a/src/core/server/graph/tenant/resolvers/query.ts +++ b/src/core/server/graph/tenant/resolvers/query.ts @@ -1,12 +1,13 @@ -import Context from 'talk-server/graph/tenant/context'; +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: Context + ctx: TenantContext ): Promise => { return ctx.loaders.Assets.asset.load(id); }, + settings: async (parent: any, args: any, ctx: TenantContext) => ctx.tenant, }; diff --git a/src/core/server/graph/tenant/schema/index.ts b/src/core/server/graph/tenant/schema/index.ts index 73a4d7019..3b4e1482f 100644 --- a/src/core/server/graph/tenant/schema/index.ts +++ b/src/core/server/graph/tenant/schema/index.ts @@ -1,15 +1,6 @@ -import { addResolveFunctionsToSchema } from 'graphql-tools'; -import { getGraphQLProjectConfig } from 'graphql-config'; +import loadSchema from 'talk-server/graph/common/schema'; +import resolvers from 'talk-server/graph/tenant/resolvers'; -import resolvers from '../resolvers'; - -// Load the configuration from the provided `.graphqlconfig` file. -const config = getGraphQLProjectConfig(__dirname, 'tenant'); - -// Get the GraphQLSchema from the configuration. -const schema = config.getSchema(); - -// Attach the resolvers to the schema. -addResolveFunctionsToSchema({ schema, resolvers }); - -export default schema; +export default function getTenantSchema() { + return loadSchema('tenant', resolvers); +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2ea16a848..42e2df93e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -1,10 +1,14 @@ -import config, { Config } from './config'; import express, { Express } from 'express'; import http from 'http'; -import { createApp, startApp } from './app'; + +import config, { Config } from './config'; +import { createApp, listenAndServe, attachSubscriptionHandlers } from './app'; import logger from './logger'; -import { create as createMongoDB } from './services/mongodb'; -import { create as createRedis } from 'talk-server/services/redis'; +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; @@ -17,6 +21,10 @@ class Server { // 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; + // config exposes application specific configuration. public config: Config; @@ -29,6 +37,12 @@ class Server { this.config = config .load(options.config || {}) .validate({ allowed: 'strict' }); + + // Load the graph schemas. + this.schemas = { + management: getManagementSchema(), + tenant: getTenantSchema(), + }; } /** @@ -47,17 +61,22 @@ class Server { const mongo = await createMongoDB(config); // Setup Redis. - const redis = await createRedis(config); + const redis = await createRedisClient(config); // Create the Talk App, branching off from the parent app. - const app = await createApp(parent, { + const app: Express = await createApp({ + parent, mongo, redis, config: this.config, + schemas: this.schemas, }); - // Start the application. - this.httpServer = await startApp(port, app); + // 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); logger.info({ port }, 'now listening'); } diff --git a/src/core/server/services/mongodb/index.ts b/src/core/server/services/mongodb/index.ts index bebdb4420..2d869d3bd 100644 --- a/src/core/server/services/mongodb/index.ts +++ b/src/core/server/services/mongodb/index.ts @@ -6,7 +6,7 @@ import { Config } from 'talk-server/config'; * * @param config application configuration. */ -export async function create(config: Config): Promise { +export async function createMongoDB(config: Config): Promise { // Connect and create a client for MongoDB. const client = await MongoClient.connect(config.get('mongodb')); diff --git a/src/core/server/services/redis/index.ts b/src/core/server/services/redis/index.ts index f6c1d1ec1..37623adaf 100644 --- a/src/core/server/services/redis/index.ts +++ b/src/core/server/services/redis/index.ts @@ -6,6 +6,6 @@ import { Config } from 'talk-server/config'; * * @param config application configuration. */ -export async function create(config: Config): Promise { +export async function createRedisClient(config: Config): Promise { return new RedisClient(config.get('redis'), {}); }