diff --git a/backend/serverless.ts b/backend/serverless.ts index 7999396..9825b7c 100644 --- a/backend/serverless.ts +++ b/backend/serverless.ts @@ -1,5 +1,5 @@ -// import { Stack } from '@aws-cdk/core'; -import { app, stack/*, table*/ } from '@resources/index'; +import { Stack } from '@aws-cdk/core'; +import { app, stack, table } from '@resources/index'; import { functions } from '@functions/index'; @@ -30,18 +30,18 @@ const serverlessConfiguration: AWS = { }, }, }, - // iamRoleStatements: [ - // { - // Effect: 'Allow', - // Action: [ - // 'dynamodb:Query', - // 'dynamodb:PutItem', - // 'dynamodb:DeleteItem', - // 'dynamodb:ListStreams', - // ], - // Resource: [Stack.of(stack).resolve(table.tableArn)], - // }, - // ], + iamRoleStatements: [ + { + Effect: 'Allow', + Action: [ + 'dynamodb:Query', + 'dynamodb:PutItem', + 'dynamodb:DeleteItem', + 'dynamodb:ListStreams', + ], + Resource: [Stack.of(stack).resolve(table.tableArn)], + }, + ], logs: { restApi: true, }, diff --git a/backend/src/functions/index.ts b/backend/src/functions/index.ts index acaf824..0f3df8f 100644 --- a/backend/src/functions/index.ts +++ b/backend/src/functions/index.ts @@ -2,10 +2,16 @@ import hello from './hello'; import getVirus from './virus-get'; import killVirus from './virus-kill'; import createVirus from './virus-create'; +import wsConnect from './ws-connect'; +import wsDisconnect from './ws-disconnect'; +import sendMessageToClient from './sendMessageToClient'; export const functions = { hello, getVirus, killVirus, - createVirus + createVirus, + wsConnect, + wsDisconnect, + sendMessageToClient }; diff --git a/backend/src/functions/sendMessageToClient/handler.ts b/backend/src/functions/sendMessageToClient/handler.ts new file mode 100644 index 0000000..fc558fe --- /dev/null +++ b/backend/src/functions/sendMessageToClient/handler.ts @@ -0,0 +1,35 @@ +import { DynamoDBStreamEvent } from 'aws-lambda'; +import { Converter } from 'aws-sdk/clients/dynamodb'; +import { getAllConnections } from '@libs/connections'; +import { sendMessageToConnection } from '@libs/websocket'; +import { Item } from '@libs/types'; +import { Virus } from '@functions/types'; + +const sendMessageToEachConnection = async (message: any): Promise => { + const connections = await getAllConnections(); + await Promise.all( + connections.map(({ connectionId, endpoint }) => { + sendMessageToConnection({ + connectionId, + endpoint, + message, + }); + } + ), + ); +}; + +const isVirus = (item: Item): item is Virus => item.partitionKey === 'Virus'; + +export const main = async (event: DynamoDBStreamEvent): Promise => { + await Promise.all( + event.Records.map(({ eventName, dynamodb }) => { + if (eventName === 'INSERT' && dynamodb && dynamodb.NewImage) { + const newItem = Converter.unmarshall(dynamodb.NewImage) as Item; + if (isVirus(newItem)) { + return sendMessageToEachConnection({ virusId: newItem.sortKey }); + } + } + }), + ); +}; \ No newline at end of file diff --git a/backend/src/functions/sendMessageToClient/index.ts b/backend/src/functions/sendMessageToClient/index.ts new file mode 100644 index 0000000..b249ec3 --- /dev/null +++ b/backend/src/functions/sendMessageToClient/index.ts @@ -0,0 +1,17 @@ +import { Stack } from '@aws-cdk/core'; +import { stack, table } from '@resources/index'; +import { handlerPath } from '@libs/handlerResolver'; + +export default { + handler: `${handlerPath(__dirname)}/handler.main`, + events: [ + { + stream: { + // @ts-ignore + type: 'dynamodb', + // @ts-ignore + arn: Stack.of(stack).resolve(table.tableStreamArn), + }, + }, + ], +} \ No newline at end of file diff --git a/backend/src/functions/virus-create/handler.ts b/backend/src/functions/virus-create/handler.ts index 28a443e..a3b4904 100644 --- a/backend/src/functions/virus-create/handler.ts +++ b/backend/src/functions/virus-create/handler.ts @@ -2,11 +2,25 @@ import { formatJSONResponse } from '@libs/apiGateway'; import { APIGatewayProxyHandler } from 'aws-lambda'; import { middyfyWithoutBodyParser } from '@libs/lambda'; import uuid from 'uuid'; +import { + PutCommand, + DynamoDBDocumentClient +} from '@aws-sdk/lib-dynamodb'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -const create: APIGatewayProxyHandler = async () => { +const documentClient = DynamoDBDocumentClient.from(new DynamoDBClient({})); + +const kill: APIGatewayProxyHandler = async () => { const id = uuid(); - console.log('Virus created'); + + await documentClient.send( + new PutCommand({ + TableName: 'dojo-serverless-table', + Item: { partitionKey: 'Virus', sortKey: id }, + }), + ); + return formatJSONResponse({ id }); } -export const main = middyfyWithoutBodyParser(create); \ No newline at end of file +export const main = middyfyWithoutBodyParser(kill); \ No newline at end of file diff --git a/backend/src/functions/virus-get/handler.ts b/backend/src/functions/virus-get/handler.ts index a5c5d4f..6c31df4 100644 --- a/backend/src/functions/virus-get/handler.ts +++ b/backend/src/functions/virus-get/handler.ts @@ -1,16 +1,24 @@ import { formatJSONResponse } from '@libs/apiGateway'; +import { Virus } from '../types'; import { APIGatewayProxyHandler } from 'aws-lambda'; import { middyfyWithoutBodyParser } from '@libs/lambda'; -import uuid from 'uuid'; +import { + QueryCommand, + DynamoDBDocumentClient +} from '@aws-sdk/lib-dynamodb'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; + +const documentClient = DynamoDBDocumentClient.from(new DynamoDBClient({})); const get: APIGatewayProxyHandler = async () => { - const viruses = [ - { id: uuid() }, - { id: uuid() }, - { id: uuid() }, - { id: uuid() }, - ]; - return formatJSONResponse({ viruses }); + const { Items = [] } = await documentClient.send( + new QueryCommand({ + TableName: 'dojo-serverless-table', + KeyConditionExpression: 'partitionKey = :partitionKey', + ExpressionAttributeValues: { ':partitionKey': 'Virus' }, + }), + ); + return formatJSONResponse({ viruses: (Items as Virus[]).map(({ sortKey }) => ({ id: sortKey })) }); } export const main = middyfyWithoutBodyParser(get); \ No newline at end of file diff --git a/backend/src/functions/virus-kill/handler.ts b/backend/src/functions/virus-kill/handler.ts index 9349e08..66fa217 100644 --- a/backend/src/functions/virus-kill/handler.ts +++ b/backend/src/functions/virus-kill/handler.ts @@ -1,9 +1,23 @@ import { middyfyWithoutBodyParser } from '@libs/lambda'; import { formatJSONResponse, IdParamRequiredEventAPIGatewayProxyHandler } from '@libs/apiGateway'; +import { + DeleteCommand, + DynamoDBDocumentClient +} from '@aws-sdk/lib-dynamodb'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; + +const documentClient = DynamoDBDocumentClient.from(new DynamoDBClient({})); const kill: IdParamRequiredEventAPIGatewayProxyHandler = async (event) => { const { id } = event.pathParameters; + await documentClient.send( + new DeleteCommand({ + TableName: 'dojo-serverless-table', + Key: { partitionKey: 'Virus', sortKey: id }, + }), + ); + console.log('Virus killed'); return formatJSONResponse({ id }); } diff --git a/backend/src/functions/ws-connect/handler.ts b/backend/src/functions/ws-connect/handler.ts new file mode 100644 index 0000000..ca3af9c --- /dev/null +++ b/backend/src/functions/ws-connect/handler.ts @@ -0,0 +1,9 @@ +import { formatJSONResponse } from '@libs/apiGateway'; +import { WebSocketConnectRequestEvent, extractEndpointFromEvent } from '@libs/websocket'; +import { createConnection } from '@libs/connections'; + +export const main = async (event: WebSocketConnectRequestEvent) => { + const endpoint = extractEndpointFromEvent(event); + await createConnection(event.requestContext.connectionId, endpoint); + return formatJSONResponse({}); +} \ No newline at end of file diff --git a/backend/src/functions/ws-connect/index.ts b/backend/src/functions/ws-connect/index.ts new file mode 100644 index 0000000..9cecdce --- /dev/null +++ b/backend/src/functions/ws-connect/index.ts @@ -0,0 +1,12 @@ +import { handlerPath } from '@libs/handlerResolver'; + +export default { + handler: `${handlerPath(__dirname)}/handler.main`, + events: [ + { + websocket: { + route: '$connect' + } + } + ] +} \ No newline at end of file diff --git a/backend/src/functions/ws-disconnect/handler.ts b/backend/src/functions/ws-disconnect/handler.ts new file mode 100644 index 0000000..6ad0b9f --- /dev/null +++ b/backend/src/functions/ws-disconnect/handler.ts @@ -0,0 +1,8 @@ +import { formatJSONResponse } from '@libs/apiGateway'; +import { WebSocketDisconnectRequestEvent } from '@libs/websocket'; +import { deleteConnection } from '@libs/connections'; + +export const main = async (event: WebSocketDisconnectRequestEvent) => { + await deleteConnection(event.requestContext.connectionId); + return formatJSONResponse({}); +} diff --git a/backend/src/functions/ws-disconnect/index.ts b/backend/src/functions/ws-disconnect/index.ts new file mode 100644 index 0000000..c28dd0c --- /dev/null +++ b/backend/src/functions/ws-disconnect/index.ts @@ -0,0 +1,12 @@ +import { handlerPath } from '@libs/handlerResolver'; + +export default { + handler: `${handlerPath(__dirname)}/handler.main`, + events: [ + { + websocket: { + route: '$disconnect' + } + } + ] +} \ No newline at end of file diff --git a/backend/src/libs/websocket.ts b/backend/src/libs/websocket.ts index b82f272..b7d63ea 100644 --- a/backend/src/libs/websocket.ts +++ b/backend/src/libs/websocket.ts @@ -33,19 +33,22 @@ export const sendMessageToConnection = async ({ endpoint: string; message: any; }): Promise => { - const apiGatewayCLient = new ApiGatewayManagementApiClient({ + const apiGatewayClient = new ApiGatewayManagementApiClient({ apiVersion: '2018-11-29', - endpoint, + endpoint: `https://${endpoint.slice(0, endpoint.length - 4)}`, }); try { - await apiGatewayCLient.send( + console.log('(1)(1) apiGatewayClient', apiGatewayClient); + await apiGatewayClient.send( new PostToConnectionCommand({ ConnectionId: connectionId, Data: Buffer.from(JSON.stringify(message)), }), ); + console.log('(1)(2)'); } catch (error) { if (error.statusCode !== 410) { + console.log('(error)'); throw error; } console.log(`Found stale connection, deleting ${connectionId}`); diff --git a/backend/src/resources/index.ts b/backend/src/resources/index.ts index c4196e0..9cd5f2a 100644 --- a/backend/src/resources/index.ts +++ b/backend/src/resources/index.ts @@ -1,9 +1,9 @@ import { App, Stack } from '@aws-cdk/core'; -// import initDynamodb from './dynamodb'; +import initDynamodb from './dynamodb'; import initApiGatewayErrors from './apiGatewayErrors'; export const app = new App(); export const stack = new Stack(app, 'Stack'); -// export const table = initDynamodb(stack); +export const table = initDynamodb(stack); initApiGatewayErrors(stack); \ No newline at end of file diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index e05db74..178bd3f 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -11,6 +11,8 @@ import virus4 from 'assets/Virus4.png'; import virus5 from 'assets/Virus5.png'; import virus6 from 'assets/Virus6.png'; +import { websocketConnexion } from '../services/networking/websocket'; + const VirusImgs = [virus1, virus2, virus3, virus4, virus5, virus6]; const { Title, Text } = Typography; @@ -77,7 +79,7 @@ export default () => { { method: 'POST' }, ); const { id } = await response.json(); - setViruses((prevViruses) => prevViruses.concat(getRandomVirus(id))); + // setViruses((prevViruses) => prevViruses.concat(getRandomVirus(id))); }; const killVirus = async (virusId: string) => { @@ -88,6 +90,16 @@ export default () => { setViruses((prevViruses) => prevViruses.filter(({ id }) => id !== virusId)); }; + websocketConnexion.onmessage = (message) => { + const data = JSON.parse(message.data); + const virusId = data.virusId; + console.log('message', message); + console.log('virusId', virusId); + if (virusId) { + setViruses((prevViruses) => prevViruses.concat(getRandomVirus(virusId))); + } + }; + return ( <>