Skip to content

Split handler between websocket and REST #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ apollo-lambda-websocket.iml
.idea
.aws-sam
*.zip

cdk/package-lock.json
src/package-lock.json
35 changes: 24 additions & 11 deletions cdk/lib/apollo-lambda-websocket-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ export class ApolloLambdaWebsocketStack extends Stack {
autoDeploy: true,
});

const requestHandlerLambda = new SimpleLambda(this, 'RequestHandler', {
entryFilename: 'graphql-query-handler.ts',
handler: 'handleMessage',
name: 'RequestHandler',
description: 'Handles GraphQL queries sent via websocket and REST. Stores (connectionId, topic) tuple in DynamoDB for subscriptions requests. Sends events to EventBridge for mutation requests',
const wsRequestHandlerLambda = new SimpleLambda(this, 'WSRequestHandler', {
entryFilename: 'graphql-query-ws-handler.ts',
handler: 'handleWSMessage',
name: 'WSRequestHandler',
description: 'Handles GraphQL queries sent via websocket. Stores (connectionId, topic) tuple in DynamoDB for subscriptions requests. Sends events to EventBridge for mutation requests',
envVariables: {
BUS_NAME: eventBus.eventBusName,
TABLE_NAME: connectionTable.tableName,
Expand All @@ -138,10 +138,10 @@ export class ApolloLambdaWebsocketStack extends Stack {
},
});

connectionTable.grantFullAccess(requestHandlerLambda.fn);
eventBus.grantPutEventsTo(requestHandlerLambda.fn);
connectionTable.grantFullAccess(wsRequestHandlerLambda.fn);
eventBus.grantPutEventsTo(wsRequestHandlerLambda.fn);

requestHandlerLambda.fn.addToRolePolicy(
wsRequestHandlerLambda.fn.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
resources: [
Expand All @@ -153,7 +153,7 @@ export class ApolloLambdaWebsocketStack extends Stack {

this.webSocketApi.addRoute('$default', {
integration: new LambdaWebSocketIntegration({
handler: requestHandlerLambda.fn,
handler: wsRequestHandlerLambda.fn,
}),
});

Expand Down Expand Up @@ -242,7 +242,6 @@ export class ApolloLambdaWebsocketStack extends Stack {

eventBus.grantPutEventsTo(translateToFrenchLambda.fn);
eventBus.grantPutEventsTo(translateToGermanLambda.fn);
eventBus.grantPutEventsTo(requestHandlerLambda.fn);

const restApi = new RestApi(this, 'ApolloRestApi', {
description: 'A Rest API that handles GraphQl queries via POST to /graphql.',
Expand All @@ -253,9 +252,23 @@ export class ApolloLambdaWebsocketStack extends Stack {
restApiName: 'RestApi',
});

const restRequestHandlerLambda = new SimpleLambda(this, 'RestRequestHandler', {
entryFilename: 'graphql-query-rest-handler.ts',
handler: 'handleRESTMessage',
name: 'RestRequestHandler',
description: 'Handles GraphQL queries sent via REST.',
envVariables: {
BUS_NAME: eventBus.eventBusName,
REQUEST_EVENT_DETAIL_TYPE,
},
});

// connectionTable(restRequestHandlerLambda.fn);
eventBus.grantPutEventsTo(restRequestHandlerLambda.fn);

restApi.root
.addResource('graphql')
.addMethod(HttpMethod.POST, new LambdaIntegration(requestHandlerLambda.fn));
.addMethod(HttpMethod.POST, new LambdaIntegration(restRequestHandlerLambda.fn));

new CfnOutput(this, 'WebsocketApiEndpoint', {
value: `${this.webSocketApi.apiEndpoint}/${websocketStage.stageName}`,
Expand Down
138 changes: 0 additions & 138 deletions src/lambda/graphql-query-handler.ts

This file was deleted.

21 changes: 21 additions & 0 deletions src/lambda/graphql-query-rest-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { mutationAndQueryHandler, schema } from './graphql-query';
import { generateLambdaProxyResponse } from './utils';

const { parse, validate } = require('graphql');


export async function handleRESTMessage(event: any): Promise<any> {
const operation = JSON.parse(event.body.replace(/\n/g, ''));
const graphqlDocument = parse(operation.query);
const validationErrors = validate(schema, graphqlDocument);

if (validationErrors.length > 0) {
return generateLambdaProxyResponse(400, JSON.stringify(validationErrors));
}

if (graphqlDocument.definitions[0].operation === 'subscription') {
return generateLambdaProxyResponse(400, 'Subscription not support via REST');
}

return mutationAndQueryHandler(event);
}
57 changes: 57 additions & 0 deletions src/lambda/graphql-query-ws-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { mutationAndQueryHandler, schema } from './graphql-query';
import { generateApolloCompatibleEventFromWebsocketEvent, generateLambdaProxyResponse } from './utils';

const { parse, validate } = require('graphql');

const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));

const dynamoDbClient = new AWS.DynamoDB.DocumentClient({
apiVersion: '2012-08-10',
region: process.env.AWS_REGION,
});

const gatewayClient = new AWS.ApiGatewayManagementApi({
apiVersion: '2018-11-29',
endpoint: process.env.API_GATEWAY_ENDPOINT,
});

export async function handleWSMessage(event: any): Promise<any> {
const operation = JSON.parse(event.body.replace(/\n/g, ''));
const graphqlDocument = parse(operation.query);
const validationErrors = validate(schema, graphqlDocument);

if (validationErrors.length > 0) {
await gatewayClient.postToConnection({
ConnectionId: event.requestContext.connectionId,
Data: JSON.stringify(validationErrors),
}).promise();
return generateLambdaProxyResponse(400, JSON.stringify(validationErrors));
}

if (graphqlDocument.definitions[0].operation === 'subscription') {
const { connectionId } = event.requestContext;
const chatId: string = graphqlDocument.definitions[0].selectionSet.selections[0].arguments[0].value.value;

const oneHourFromNow = Math.round(Date.now() / 1000 + 3600);
await dynamoDbClient.put({
TableName: process.env.TABLE_NAME!,
Item: {
chatId,
connectionId,
ttl: oneHourFromNow,
},
}).promise();

return generateLambdaProxyResponse(200, 'Ok');
}

const response = await mutationAndQueryHandler(generateApolloCompatibleEventFromWebsocketEvent(event));
await gatewayClient.postToConnection({
ConnectionId: event.requestContext.connectionId,
Data: response.body,
}).promise();

return response;
}

74 changes: 74 additions & 0 deletions src/lambda/graphql-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const { ApolloServer, gql } = require('apollo-server-lambda');
const { makeExecutableSchema } = require('@graphql-tools/schema');

const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));

const REQUEST_EVENT_DETAIL_TYPE = process.env.REQUEST_EVENT_DETAIL_TYPE!;

const eventBridge = new AWS.EventBridge({
region: process.env.AWS_REGION,
});

// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type EventDetails {
EventId: String
ErrorMessage: String
ErrorCode: String
}

type Mutation {
putEvent(message: String!, chatId: String!): Result
}

type Query {
getEvent: String
}

type Result {
Entries: [EventDetails]
FailedEntries: Int
}

type Subscription {
chat(chatId: String!): String
}

schema {
query: Query
mutation: Mutation
subscription: Subscription
}
`;

// Provide resolver functions for your schema fields

const resolvers = {
Mutation: {
// tslint:disable-next-line:no-any
putEvent: async (_: any, { message, chatId }: any) => eventBridge.putEvents({
Entries: [
{
EventBusName: process.env.BUS_NAME,
Source: 'apollo',
DetailType: REQUEST_EVENT_DETAIL_TYPE,
Detail: JSON.stringify({
message,
chatId,
}),
},
],
}).promise(),
},
Query: {
getEvent: () => 'Hello from Apollo!',
},
};
export const schema = makeExecutableSchema({ typeDefs, resolvers });

const server = new ApolloServer({
schema,
});

export const mutationAndQueryHandler = server.createHandler();