diff --git a/.gitignore b/.gitignore index 480161f..07f5504 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ apollo-lambda-websocket.iml .idea .aws-sam *.zip + +cdk/package-lock.json +src/package-lock.json diff --git a/cdk/lib/apollo-lambda-websocket-stack.ts b/cdk/lib/apollo-lambda-websocket-stack.ts index 3a2f973..03fab68 100644 --- a/cdk/lib/apollo-lambda-websocket-stack.ts +++ b/cdk/lib/apollo-lambda-websocket-stack.ts @@ -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, @@ -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: [ @@ -153,7 +153,7 @@ export class ApolloLambdaWebsocketStack extends Stack { this.webSocketApi.addRoute('$default', { integration: new LambdaWebSocketIntegration({ - handler: requestHandlerLambda.fn, + handler: wsRequestHandlerLambda.fn, }), }); @@ -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.', @@ -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}`, diff --git a/src/lambda/graphql-query-handler.ts b/src/lambda/graphql-query-handler.ts deleted file mode 100644 index 9d30d34..0000000 --- a/src/lambda/graphql-query-handler.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { generateApolloCompatibleEventFromWebsocketEvent, generateLambdaProxyResponse } from './utils'; - -const { ApolloServer, gql } = require('apollo-server-lambda'); -const { makeExecutableSchema } = require('@graphql-tools/schema'); - -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, -}); - -const eventBridge = new AWS.EventBridge({ - region: process.env.AWS_REGION, -}); - -const REQUEST_EVENT_DETAIL_TYPE = process.env.REQUEST_EVENT_DETAIL_TYPE!; - -// 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!', - }, -}; -const schema = makeExecutableSchema({ typeDefs, resolvers }); - -const server = new ApolloServer({ - schema, -}); - -const mutationAndQueryHandler = server.createHandler(); - -export async function handleMessage(event: any): Promise { - const operation = JSON.parse(event.body.replace(/\n/g, '')); - const graphqlDocument = parse(operation.query); - const validationErrors = validate(schema, graphqlDocument); - const isWsConnection: boolean = !event.resource; - - if (validationErrors.length > 0) { - if (isWsConnection) { - await gatewayClient.postToConnection({ - ConnectionId: event.requestContext.connectionId, - Data: JSON.stringify(validationErrors), - }).promise(); - } - - return generateLambdaProxyResponse(400, JSON.stringify(validationErrors)); - } - - if (graphqlDocument.definitions[0].operation === 'subscription') { - if (!isWsConnection) { - return generateLambdaProxyResponse(400, 'Subscription not support via REST'); - } - 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'); - } - - if (isWsConnection) { - const response = await mutationAndQueryHandler(generateApolloCompatibleEventFromWebsocketEvent(event)); - await gatewayClient.postToConnection({ - ConnectionId: event.requestContext.connectionId, - Data: response.body, - }).promise(); - - return response; - } - - return mutationAndQueryHandler(event); -} diff --git a/src/lambda/graphql-query-rest-handler.ts b/src/lambda/graphql-query-rest-handler.ts new file mode 100644 index 0000000..80e4938 --- /dev/null +++ b/src/lambda/graphql-query-rest-handler.ts @@ -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 { + 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); +} diff --git a/src/lambda/graphql-query-ws-handler.ts b/src/lambda/graphql-query-ws-handler.ts new file mode 100644 index 0000000..8702161 --- /dev/null +++ b/src/lambda/graphql-query-ws-handler.ts @@ -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 { + 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; +} + diff --git a/src/lambda/graphql-query.ts b/src/lambda/graphql-query.ts new file mode 100644 index 0000000..1b717e1 --- /dev/null +++ b/src/lambda/graphql-query.ts @@ -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(); \ No newline at end of file