Skip to content

Commit 1407c26

Browse files
committed
fix(lunchbot): added table for storing restaurants and visits into them
1 parent 8d7e5ec commit 1407c26

File tree

11 files changed

+612
-38
lines changed

11 files changed

+612
-38
lines changed

.projen/deps.json

Lines changed: 8 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projen/tasks.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.projenrc.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,18 @@ const project = new awscdk.AwsCdkTypeScriptApp({
1919
},
2020
projenrcTs: true,
2121
deps: [
22+
'@types/aws-lambda',
2223
'@aws-lambda-powertools/logger',
2324
'@aws-lambda-powertools/metrics',
2425
'@aws-lambda-powertools/tracer',
2526
'@middy/core',
2627
'@aws-sdk/client-lex-runtime-v2',
2728
'@aws-sdk/client-dynamodb',
2829
'@aws-sdk/lib-dynamodb',
30+
'@aws-sdk/util-dynamodb',
2931
],
3032
devDeps: [
3133
'eslint-plugin-functional@6.6.3',
32-
'@types/aws-lambda',
3334
'commitizen',
3435
'cz-conventional-changelog',
3536
'semantic-release',

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/fulfillment/lunch-stream.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { AttributeValue } from '@aws-sdk/client-dynamodb'
2+
import { UpdateCommand, UpdateCommandInput } from '@aws-sdk/lib-dynamodb'
3+
import { unmarshall } from '@aws-sdk/util-dynamodb'
4+
import { DynamoDBStreamHandler } from 'aws-lambda'
5+
import { CustomSlot } from './customSlot'
6+
import { Lunch } from './lunchState'
7+
import { dbClient } from '../common/dbClient'
8+
import { logger } from '../common/powertools'
9+
import { ensureError } from '../ensureError'
10+
11+
/**
12+
* Increase visit by one for given restaurant
13+
* @param lunches
14+
*/
15+
const updateVisitsToDynamoDb = async (lunches: Lunch[]): Promise<void> => {
16+
// Map each lunch for a promise that updates the visits count
17+
const updatePromises = lunches.map((lunch) => {
18+
logger.debug('Lunch state', { lunch })
19+
20+
const restaurant = lunch.restaurant
21+
const officeLocation = lunch.officeLocation
22+
if (!restaurant) {
23+
return Promise.reject(new Error('Restaurant not found'))
24+
}
25+
26+
const input: UpdateCommandInput = {
27+
TableName: process.env.RESTAURANT_TABLE!,
28+
Key: {
29+
restaurant,
30+
officeLocation,
31+
},
32+
UpdateExpression: 'ADD #visits :inc',
33+
ExpressionAttributeNames: {
34+
'#visits': 'visits',
35+
},
36+
ExpressionAttributeValues: {
37+
':inc': 1,
38+
},
39+
}
40+
41+
logger.debug('Updating state', { input })
42+
return dbClient.send(new UpdateCommand(input))
43+
})
44+
45+
try {
46+
// Wait for all update operations to complete
47+
await Promise.all(updatePromises)
48+
} catch (e) {
49+
const error = ensureError(e)
50+
// Handle errors here, potentially logging them or taking other actions
51+
logger.error('Error updating visits to DynamoDB:', error)
52+
}
53+
}
54+
export const handler: DynamoDBStreamHandler = async (event) => {
55+
logger.debug('Received event', { event })
56+
const records: Lunch[] = event.Records.flatMap((record) => {
57+
return record.dynamodb?.NewImage
58+
? unmarshall(
59+
record.dynamodb.NewImage as { [key: string]: AttributeValue },
60+
)
61+
: []
62+
})
63+
const lunches = records.filter(
64+
(record) => record.slot === CustomSlot.Restaurants,
65+
)
66+
await updateVisitsToDynamoDb(lunches)
67+
}

src/fulfillment/lunchState.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { CustomSlot } from './customSlot'
2+
3+
export interface LunchState {
4+
sessionId: string
5+
slot: CustomSlot
6+
slotValue: { [key: string]: string }
7+
expireAt: number
8+
}
9+
10+
export interface Lunch {
11+
sessionId: string
12+
slot: CustomSlot
13+
restaurant?: string
14+
officeLocation?: string
15+
expireAt: number
16+
}

src/fulfillment/processSlots.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,10 @@ export const processSlots: NextSlotHandler = async (
231231
await storeState({
232232
sessionId,
233233
slot: CustomSlot.Restaurants,
234-
slotValue: { restaurant: extractedRestaurant },
234+
slotValue: {
235+
restaurant: extractedRestaurant,
236+
officeLocation: officeLocation,
237+
},
235238
expireAt: Math.floor(Date.now() / 1000) + ONE_WEEK_IN_SECONDS,
236239
})
237240
return createCloseAction(sessionAttributes, intent, [

src/fulfillment/storeState.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,28 @@
11
import { PutCommand, PutCommandInput } from '@aws-sdk/lib-dynamodb'
2-
import { CustomSlot } from './customSlot'
32
import { dbClient } from '../common/dbClient'
43
import { logger } from '../common/powertools'
54
import { ensureError } from '../ensureError'
5+
import { LunchState } from './lunchState'
66

77
const stateTableName = process.env.STATE_TABLE!
88

9-
interface LunchState {
10-
sessionId: string
11-
slot: CustomSlot
12-
slotValue: { [key: string]: string }
13-
expireAt: number
14-
}
15-
169
/**
1710
* If the item already exists (i.e., an item with the same id and intentName)
1811
* @return null
1912
* @param sessionId
20-
* @param intentName
13+
* @param slot
2114
* @param slotValue
15+
* @param expireAt
2216
* @example
23-
* // Example usage with a dynamic slotValue key
17+
* // Example usage with multiple slotValues
2418
* const sessionId = 'session123';
25-
* const slot = { name: 'lunchSlot', type: 'type1' };
26-
* const slotValue = { myDynamicKey: 'myDynamicValue' };
19+
* const slot = CustomSlot.LunchSlot;
20+
* const slotValue = { restaurant: 'Place1', officeLocation: 'Location1' };
2721
* await storeState({
2822
* sessionId,
2923
* slot,
30-
* slotValue
24+
* slotValue,
25+
* expireAt: Math.floor(Date.now() / 1000) + ONE_WEEK_IN_SECONDS
3126
* });
3227
*/
3328
export const storeState = async ({
@@ -36,19 +31,23 @@ export const storeState = async ({
3631
slotValue,
3732
expireAt,
3833
}: LunchState): Promise<void | null> => {
39-
const [key, value] = Object.entries(slotValue)[0]
34+
// Using object spread to build the item
35+
const item: PutCommandInput['Item'] = {
36+
id: sessionId,
37+
slot,
38+
expireAt,
39+
...slotValue,
40+
}
41+
4042
const input: PutCommandInput = {
4143
TableName: stateTableName,
42-
Item: {
43-
id: sessionId,
44-
slot: slot,
45-
[key]: value,
46-
expireAt,
47-
},
44+
Item: item,
4845
ConditionExpression:
49-
'attribute_not_exists(id) AND attribute_not_exists(intentName)',
46+
'attribute_not_exists(id) AND attribute_not_exists(slot)',
5047
}
48+
5149
logger.debug('Updating state', { input })
50+
5251
try {
5352
await dbClient.send(new PutCommand(input))
5453
return // Return if the new item was created successfully

src/lunch.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import {
2-
aws_glue as glue,
2+
Aws,
33
aws_dynamodb as dynamodb,
4+
aws_glue as glue,
5+
aws_iam as iam,
46
aws_lambda as lambda,
7+
aws_lambda_event_sources as event_sources,
58
aws_lambda_nodejs as nodejs,
6-
aws_iam as iam,
9+
aws_logs as logs,
710
RemovalPolicy,
8-
Aws,
9-
// Aws,
1011
} from 'aws-cdk-lib'
11-
import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'
1212
import { Construct } from 'constructs'
1313

1414
export class Lunch extends Construct {
@@ -44,12 +44,13 @@ export class Lunch extends Construct {
4444
name: 'id',
4545
},
4646
sortKey: {
47-
type: AttributeType.STRING,
47+
type: dynamodb.AttributeType.STRING,
4848
name: 'slot',
4949
},
5050
deletionProtection: false,
5151
timeToLiveAttribute: 'expireAt',
5252
removalPolicy: RemovalPolicy.DESTROY,
53+
dynamoStream: dynamodb.StreamViewType.NEW_IMAGE,
5354
})
5455
const crawlerRole = new iam.Role(this, 'CrawlerRole', {
5556
assumedBy: new iam.ServicePrincipal('glue.amazonaws.com'),
@@ -116,6 +117,34 @@ export class Lunch extends Construct {
116117
entry: 'src/fulfillment/fulfillment.ts',
117118
},
118119
)
120+
const restaurantTable = new dynamodb.TableV2(this, 'Restaurant', {
121+
partitionKey: {
122+
type: dynamodb.AttributeType.STRING,
123+
name: 'restaurant',
124+
},
125+
sortKey: {
126+
type: dynamodb.AttributeType.STRING,
127+
name: 'officeLocation',
128+
},
129+
removalPolicy: RemovalPolicy.DESTROY,
130+
})
131+
const streamLambda = new nodejs.NodejsFunction(this, 'streamLambda', {
132+
runtime: lambda.Runtime.NODEJS_20_X,
133+
logRetention: logs.RetentionDays.ONE_MONTH,
134+
environment: {
135+
SERVICE_NAME: 'lunchbot',
136+
POWERTOOLS_LOG_LEVEL: 'ERROR',
137+
RESTAURANT_TABLE: restaurantTable.tableName,
138+
},
139+
entry: 'src/fulfillment/lunch-stream.ts',
140+
})
141+
streamLambda.addEventSource(
142+
new event_sources.DynamoEventSource(stateTable, {
143+
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
144+
}),
145+
)
146+
restaurantTable.grantReadWriteData(streamLambda)
147+
stateTable.grantStreamRead(streamLambda)
119148
stateTable.grantReadWriteData(this.fulfillmentLambda)
120149
lunchTable.grantReadData(this.fulfillmentLambda)
121150
}

0 commit comments

Comments
 (0)