Skip to content

Commit

Permalink
Merge pull request #8 from fanout/http-server-api
Browse files Browse the repository at this point in the history
Http server api
  • Loading branch information
gobengo committed May 30, 2019
2 parents 5f64aa1 + 152444c commit 7dd4642
Show file tree
Hide file tree
Showing 9 changed files with 456 additions and 9 deletions.
266 changes: 261 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Expand Up @@ -17,6 +17,7 @@
"@types/graphql": "^14.2.0",
"@types/lambda-tester": "^3.5.1",
"@types/lodash": "^4.14.123",
"@types/micro": "^7.3.3",
"@types/node-fetch": "^2.3.2",
"@types/uuid": "^3.4.4",
"alsatian": "^2.4.0",
Expand All @@ -26,15 +27,17 @@
"apollo-link-ws": "^1.0.17",
"apollo-server": "^2.4.8",
"apollo-server-lambda": "^2.4.8",
"apollo-server-micro": "^2.5.0",
"aws-serverless-express": "^3.3.6",
"body-parser": "^1.18.3",
"cross-fetch": "^3.0.2",
"fp-ts": "^1.17.0",
"graphql": "^14.2.1",
"graphql": "^14.3.1",
"grip": "^1.3.0",
"killable": "^1.0.1",
"lambda-tester": "^3.5.0",
"lodash": "^4.17.11",
"micro": "^9.3.4",
"node-fetch": "^2.3.0",
"rxjs": "^6.5.1",
"tap-bark": "^1.0.0",
Expand Down
57 changes: 57 additions & 0 deletions src/examples/http-request-listener-api.ts
@@ -0,0 +1,57 @@
/**
* API Demo of adding WebSocketOverHttp support to any http.RequestListener function (e.g. (req, res) => void).
* Almost all node.js web libraries support creating one of these from the underlying Application object.
* In this example, we use zeit/micro, but you can do something similar with koa, express, raw node http, etc.
*/

import { buildSchemaFromTypeDefinitions, PubSub } from "apollo-server";
import { ApolloServer } from "apollo-server-micro";
import * as http from "http";
import { run as microRun } from "micro";
import FanoutGraphqlApolloConfig, {
FanoutGraphqlTypeDefs,
} from "../FanoutGraphqlApolloConfig";
import EpcpPubSubMixin from "../graphql-epcp-pubsub/EpcpPubSubMixin";
import { MapSimpleTable } from "../SimpleTable";
import GraphqlWsOverWebSocketOverHttpRequestListener from "../subscriptions-transport-ws-over-http/GraphqlWsOverWebSocketOverHttpRequestListener";

// This is what you need to support EPCP Publishes (make sure it gets to your resolvers who call pubsub.publish)
const pubsub = EpcpPubSubMixin({
grip: {
url: process.env.GRIP_URL || "http://localhost:5561",
},
// Build a schema from typedefs here but without resolvers (since they will need the resulting pubsub to publish to)
schema: buildSchemaFromTypeDefinitions(FanoutGraphqlTypeDefs(true)),
})(new PubSub());

const apolloServer = new ApolloServer(
FanoutGraphqlApolloConfig({
pubsub,
subscriptions: true,
tables: {
notes: MapSimpleTable(),
},
}),
);

// This won't throw, but it also won't result in working WebSocket Subscriptions (when you create the subscription via gql api, a response comes back mentioning:
// { "error": { "name": "TypeError", "message": "Cannot read property 'addListener' of undefined" }
// But there is nothing useful on stderr of the server.
// apolloServer.installSubscriptionHandlers(httpServer)

// In micro 9.3.5, the default export of micro(handler) will return an http.RequestListener (after https://github.com/zeit/micro/pull/399).
// As of this authoring, only 9.3.4 is out, which returns an http.Server. So we manually build the RequestListner here.
// After 9.3.5, the following will work:
// import micro from "micro"
// const microRequestListener = micro(apolloServer.createHandler())
const microRequestListener: http.RequestListener = (req, res) =>
microRun(req, res, apolloServer.createHandler());

const httpServer = http.createServer(
GraphqlWsOverWebSocketOverHttpRequestListener(microRequestListener),
);

const port = process.env.PORT || 57410;
httpServer.listen(port, () => {
console.log(`Server is now running on http://localhost:${port}`);
});
50 changes: 50 additions & 0 deletions src/examples/http-server-api.ts
@@ -0,0 +1,50 @@
/**
* API Demo of adding WebSocketOverHttp support patching any http.Server
* Almost all node.js web libraries support creating one of these from the underlying Application object.
* In this example, we use zeit/micro, but you can do something similar with koa, express, raw node http, etc.
*/

import { buildSchemaFromTypeDefinitions, PubSub } from "apollo-server";
import { ApolloServer } from "apollo-server-micro";
import * as http from "http";
import micro from "micro";
import FanoutGraphqlApolloConfig, {
FanoutGraphqlTypeDefs,
} from "../FanoutGraphqlApolloConfig";
import EpcpPubSubMixin from "../graphql-epcp-pubsub/EpcpPubSubMixin";
import { MapSimpleTable } from "../SimpleTable";
import GraphqlWsOverWebSocketOverHttpSubscriptionHandlerInstaller from "../subscriptions-transport-ws-over-http/GraphqlWsOverWebSocketOverHttpSubscriptionHandlerInstaller";

// This is what you need to support EPCP Publishes (make sure it gets to your resolvers who call pubsub.publish)
const pubsub = EpcpPubSubMixin({
grip: {
url: process.env.GRIP_URL || "http://localhost:5561",
},
// Build a schema from typedefs here but without resolvers (since they will need the resulting pubsub to publish to)
schema: buildSchemaFromTypeDefinitions(FanoutGraphqlTypeDefs(true)),
})(new PubSub());

const apolloServer = new ApolloServer(
FanoutGraphqlApolloConfig({
pubsub,
subscriptions: true,
tables: {
notes: MapSimpleTable(),
},
}),
);

// Note: In micro 9.3.5 this will return an http.RequestListener instead (after https://github.com/zeit/micro/pull/399)
// Provide it to http.createServer to create an http.Server
const httpServer: http.Server = micro(apolloServer.createHandler());

// This won't throw, but it also won't result in working WebSocket Subscriptions (when you create the subscription via gql api, a response comes back mentioning:
// { "error": { "name": "TypeError", "message": "Cannot read property 'addListener' of undefined" }
// But there is nothing useful on stderr of the server.
// apolloServer.installSubscriptionHandlers(httpServer)
GraphqlWsOverWebSocketOverHttpSubscriptionHandlerInstaller()(httpServer);

const port = process.env.PORT || 57410;
httpServer.listen(port, () => {
console.log(`Server is now running on http://localhost:${port}`);
});
@@ -1,6 +1,6 @@
import { getMainDefinition } from "apollo-utilities";
import * as grip from "grip";
import gql from "graphql-tag";
import * as grip from "grip";

interface IOnOpenResponse {
/** response headers */
Expand Down
@@ -1,6 +1,6 @@
import * as express from "express";
import WebSocketOverHttpExpress from "../WebSocketOverHttpExpress";
import AcceptAllGraphqlSubscriptionsMessageHandler from "../graphql-ws/AcceptAllGraphqlSubscriptionsMessageHandler";
import WebSocketOverHttpExpress from "../WebSocketOverHttpExpress";
import GraphqlWebSocketOverHttpConnectionListener from "./GraphqlWebSocketOverHttpConnectionListener";

/**
Expand Down
@@ -0,0 +1,20 @@
import * as express from "express";
import * as http from "http";
import GraphqlWsOverWebSocketOverHttpExpressMiddleware from "./GraphqlWsOverWebSocketOverHttpExpressMiddleware";

/**
* GraphqlWsOverWebSocketOverHttpRequestListener.
* Given an http RequestListener, return a new one that will respond to incoming WebSocket-Over-Http requests that are graphql-ws
* Subscriptions and accept the subscriptions.
*/
export default (
originalRequestListener: http.RequestListener,
): http.RequestListener => (req, res) => {
const handleWebSocketOverHttpRequestHandler: http.RequestListener = express()
.use(GraphqlWsOverWebSocketOverHttpExpressMiddleware())
.use((expressRequest, expressResponse) => {
// It wasn't handled by GraphqlWsOverWebSocketOverHttpExpressMiddleware
originalRequestListener(req, res);
});
handleWebSocketOverHttpRequestHandler(req, res);
};
@@ -0,0 +1,61 @@
import { EventEmitter } from "events";
import * as express from "express";
import * as http from "http";
import GraphqlWsOverWebSocketOverHttpExpressMiddleware from "./GraphqlWsOverWebSocketOverHttpExpressMiddleware";

/**
* Create a function that will patch an http.Server instance such that it responds to incoming graphql-ws over WebSocket-Over-Http requests in a way that will allow all GraphQL Subscriptions to initiate.
* If the incoming request is not of this specific kind, it will be handled however the http.Server normally would.
*/
export default () => (httpServer: http.Server) => {
interceptRequests(httpServer, (request, response, next) => {
const handleWebSocketOverHttpRequestHandler: http.RequestListener = express()
.use(GraphqlWsOverWebSocketOverHttpExpressMiddleware())
.use((expressRequest, expressResponse) => {
// It wasn't handled by GraphqlWsOverWebSocketOverHttpExpressMiddleware
next();
});
handleWebSocketOverHttpRequestHandler(request, response);
});
};

type AnyFunction = (...args: any[]) => any;

/** NodeJS.EventEmitter properties that do exist but are not documented and aren't on the TypeScript types */
interface IEventEmitterPrivates {
/** Internal state holding refs to all listeners */
_events: Record<string, AnyFunction | AnyFunction[] | undefined>;
}
/** Use declaration merigng to add IEventEmitterPrivates to NodeJs.EventEmitters like http.Server used below */
declare module "events" {
// EventEmitter
// tslint:disable-next-line:interface-name no-empty-interface
interface EventEmitter extends IEventEmitterPrivates {}
}

type RequestInterceptor = (
request: http.IncomingMessage,
response: http.ServerResponse,
next: () => void,
) => void;

/** Patch an httpServer to pass all incoming requests through an interceptor before doing what it would normally do */
function interceptRequests(
httpServer: http.Server,
intercept: RequestInterceptor,
) {
const originalRequestListeners = httpServer._events.request;
httpServer._events.request = (
request: http.IncomingMessage,
response: http.ServerResponse,
) => {
intercept(request, response, () => {
const listeners = originalRequestListeners
? Array.isArray(originalRequestListeners)
? originalRequestListeners
: [originalRequestListeners]
: [];
listeners.forEach(listener => listener(request, response));
});
};
}
2 changes: 1 addition & 1 deletion tslint.json
Expand Up @@ -22,7 +22,7 @@
"rulesDirectory": [],
"linterOptions": {
"exclude": [
"node_modules/**",
"node_modules/**"
]
}
}

0 comments on commit 7dd4642

Please sign in to comment.