Skip to content

Commit b0dfa90

Browse files
committed
Dialogs state: DynamoDB implementation
1 parent 5878cc7 commit b0dfa90

File tree

13 files changed

+278
-1
lines changed

13 files changed

+278
-1
lines changed

.deploy/lambda/lib/JProfByBotStack.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export class JProfByBotStack extends cdk.Stack {
2525
partitionKey: { name: 'chat', type: dynamodb.AttributeType.NUMBER },
2626
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
2727
});
28+
const dialogStatesTable = new dynamodb.Table(this, 'jprof-by-bot-table-dialog-states', {
29+
tableName: 'jprof-by-bot-table-dialog-states',
30+
partitionKey: { name: 'userId', type: dynamodb.AttributeType.NUMBER },
31+
sortKey: { name: 'chatId', type: dynamodb.AttributeType.NUMBER },
32+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
33+
});
2834
const layerLibGL = new lambda.LayerVersion(this, 'jprof-by-bot-lambda-layer-libGL', {
2935
code: lambda.Code.fromAsset('layers/libGL.zip'),
3036
compatibleRuntimes: [lambda.Runtime.JAVA_11],
@@ -50,6 +56,7 @@ export class JProfByBotStack extends cdk.Stack {
5056
'TABLE_VOTES': votesTable.tableName,
5157
'TABLE_YOUTUBE_CHANNELS_WHITELIST': youtubeChannelsWhitelistTable.tableName,
5258
'TABLE_KOTLIN_MENTIONS': kotlinMentionsTable.tableName,
59+
'TABLE_DIALOG_STATES': dialogStatesTable.tableName,
5360
'TOKEN_TELEGRAM_BOT': props.telegramToken,
5461
'TOKEN_YOUTUBE_API': props.youtubeToken,
5562
},
@@ -58,6 +65,7 @@ export class JProfByBotStack extends cdk.Stack {
5865
votesTable.grantReadWriteData(lambdaWebhook);
5966
youtubeChannelsWhitelistTable.grantReadData(lambdaWebhook);
6067
kotlinMentionsTable.grantReadWriteData(lambdaWebhook);
68+
dialogStatesTable.grantReadWriteData(lambdaWebhook);
6169

6270
const api = new apigateway.RestApi(this, 'jprof-by-bot-api', {
6371
restApiName: 'jprof-by-bot-api',

.github/workflows/default.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ jobs:
5757
- run: votes/dynamodb/src/test/resources/seed.sh
5858
- run: youtube/dynamodb/src/test/resources/seed.sh
5959
- run: kotlin/dynamodb/src/test/resources/seed.sh
60+
- run: dialogs/dynamodb/src/test/resources/seed.sh
6061
- run: ./gradlew clean dbTest
6162
- uses: actions/upload-artifact@v2
6263
if: always()

dialogs/dynamodb/build.gradle.kts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
plugins {
2+
kotlin("jvm")
3+
}
4+
5+
dependencies {
6+
api(project.projects.dialogs)
7+
api(libs.dynamodb)
8+
implementation(project.projects.utils.dynamodb)
9+
implementation(libs.kotlinx.coroutines.jdk8)
10+
implementation(libs.kotlinx.serialization.json)
11+
12+
testImplementation(libs.junit.jupiter.api)
13+
testImplementation(libs.junit.jupiter.params)
14+
testImplementation(libs.aws.junit5.dynamo.v2)
15+
testImplementation(project.projects.utils.awsJunit5)
16+
testRuntimeOnly(libs.junit.jupiter.engine)
17+
}
18+
19+
tasks {
20+
val dbTest by registering(Test::class) {
21+
group = LifecycleBasePlugin.VERIFICATION_GROUP
22+
description = "Runs the DB tests."
23+
shouldRunAfter("test")
24+
outputs.upToDateWhen { false }
25+
useJUnitPlatform {
26+
includeTags("db")
27+
}
28+
}
29+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package by.jprof.telegram.bot.dialogs.dynamodb.dao
2+
3+
import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO
4+
import by.jprof.telegram.bot.dialogs.model.DialogState
5+
import by.jprof.telegram.bot.utils.dynamodb.toAttributeValue
6+
import by.jprof.telegram.bot.utils.dynamodb.toString
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.future.await
9+
import kotlinx.coroutines.withContext
10+
import kotlinx.serialization.decodeFromString
11+
import kotlinx.serialization.encodeToString
12+
import kotlinx.serialization.json.Json
13+
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient
14+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
15+
16+
class DialogStateDAO(
17+
private val dynamoDb: DynamoDbAsyncClient,
18+
private val table: String
19+
) : DialogStateDAO {
20+
override suspend fun get(chatId: Long, userId: Long): DialogState? {
21+
return withContext(Dispatchers.IO) {
22+
dynamoDb.getItem {
23+
it.tableName(table)
24+
it.key(
25+
mapOf(
26+
"userId" to userId.toAttributeValue(),
27+
"chatId" to chatId.toAttributeValue(),
28+
)
29+
)
30+
}.await()?.item()?.takeUnless { it.isEmpty() }?.toDialogState()
31+
}
32+
}
33+
34+
override suspend fun save(dialogState: DialogState) {
35+
withContext(Dispatchers.IO) {
36+
dynamoDb.putItem {
37+
it.tableName(table)
38+
it.item(dialogState.toAttributes())
39+
}.await()
40+
}
41+
}
42+
43+
override suspend fun delete(chatId: Long, userId: Long) {
44+
withContext(Dispatchers.IO) {
45+
dynamoDb.deleteItem {
46+
it.tableName(table)
47+
it.key(
48+
mapOf(
49+
"userId" to userId.toAttributeValue(),
50+
"chatId" to chatId.toAttributeValue(),
51+
)
52+
)
53+
}
54+
}
55+
}
56+
}
57+
58+
private val json = Json { serializersModule = DialogState.serializers }
59+
60+
fun Map<String, AttributeValue>.toDialogState(): DialogState = json.decodeFromString(this["value"].toString("value"))
61+
62+
fun DialogState.toAttributes(): Map<String, AttributeValue> = mapOf(
63+
"userId" to this.userId.toAttributeValue(),
64+
"chatId" to this.chatId.toAttributeValue(),
65+
"value" to json.encodeToString(this).toAttributeValue(),
66+
)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package by.jprof.telegram.bot.dialogs.dynamodb.dao
2+
3+
import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion
4+
import by.jprof.telegram.bot.utils.aws_junit5.Endpoint
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.runBlocking
7+
import kotlinx.coroutines.withTimeout
8+
import me.madhead.aws_junit5.common.AWSClient
9+
import me.madhead.aws_junit5.dynamo.v2.DynamoDB
10+
import org.junit.jupiter.api.*
11+
import org.junit.jupiter.api.extension.ExtendWith
12+
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient
13+
14+
@Tag("db")
15+
@ExtendWith(DynamoDB::class)
16+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
17+
internal class DialogStateDAOTest {
18+
@AWSClient(endpoint = Endpoint::class)
19+
private lateinit var dynamoDB: DynamoDbAsyncClient
20+
private lateinit var sut: DialogStateDAO
21+
22+
@BeforeAll
23+
internal fun setup() {
24+
sut = DialogStateDAO(dynamoDB, "dialog-states")
25+
}
26+
27+
@Test
28+
fun get() = runBlocking {
29+
Assertions.assertEquals(
30+
WaitingForQuestion(
31+
chatId = 1,
32+
userId = 1,
33+
),
34+
sut.get(1, 1)
35+
)
36+
}
37+
38+
@Test
39+
fun save() = runBlocking {
40+
sut.save(
41+
WaitingForQuestion(
42+
chatId = 1,
43+
userId = 2,
44+
)
45+
)
46+
47+
Assertions.assertEquals(
48+
WaitingForQuestion(
49+
chatId = 1,
50+
userId = 2,
51+
),
52+
sut.get(1, 2)
53+
)
54+
}
55+
56+
@Test
57+
fun delete() = runBlocking {
58+
sut.save(
59+
WaitingForQuestion(
60+
chatId = 1,
61+
userId = 3,
62+
)
63+
)
64+
sut.delete(1, 3)
65+
66+
withTimeout(3_000) {
67+
while (null != sut.get(1, 3)) {
68+
delay(100)
69+
}
70+
}
71+
}
72+
73+
@Test
74+
fun getUnexisting() = runBlocking {
75+
Assertions.assertNull(sut.get(0, 0))
76+
}
77+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package by.jprof.telegram.bot.dialogs.dynamodb.dao
2+
3+
import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion
4+
import org.junit.jupiter.api.Assertions
5+
import org.junit.jupiter.api.Test
6+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
7+
8+
internal class DialogStateTest {
9+
@Test
10+
fun waitingForQuestionToAttributes() {
11+
Assertions.assertEquals(
12+
mapOf(
13+
"userId" to AttributeValue.builder().n("2").build(),
14+
"chatId" to AttributeValue.builder().n("1").build(),
15+
"value" to AttributeValue
16+
.builder()
17+
.s("{\"type\":\"WaitingForQuestion\",\"chatId\":1,\"userId\":2}")
18+
.build(),
19+
),
20+
WaitingForQuestion(1, 2).toAttributes()
21+
)
22+
}
23+
24+
@Test
25+
fun attributesToWaitingForQuestionTo() {
26+
Assertions.assertEquals(
27+
WaitingForQuestion(1, 2),
28+
mapOf(
29+
"userId" to AttributeValue.builder().n("2").build(),
30+
"chatId" to AttributeValue.builder().n("1").build(),
31+
"value" to AttributeValue
32+
.builder()
33+
.s("{\"type\":\"WaitingForQuestion\",\"chatId\":1,\"userId\":2}")
34+
.build(),
35+
).toDialogState()
36+
)
37+
}
38+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"dialog-states": [
3+
{
4+
"PutRequest": {
5+
"Item": {
6+
"userId": {
7+
"N": "1"
8+
},
9+
"chatId": {
10+
"N": "1"
11+
},
12+
"value": {
13+
"S": "{\n \"userId\": 1,\n \"chatId\": 1,\n \"type\": \"WaitingForQuestion\"\n}\n"
14+
}
15+
}
16+
}
17+
}
18+
]
19+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"TableName": "dialog-states",
3+
"AttributeDefinitions": [
4+
{
5+
"AttributeName": "chatId",
6+
"AttributeType": "N"
7+
},
8+
{
9+
"AttributeName": "userId",
10+
"AttributeType": "N"
11+
}
12+
],
13+
"KeySchema": [
14+
{
15+
"AttributeName": "userId",
16+
"KeyType": "HASH"
17+
},
18+
{
19+
"AttributeName": "chatId",
20+
"KeyType": "RANGE"
21+
}
22+
],
23+
"ProvisionedThroughput": {
24+
"ReadCapacityUnits": 1,
25+
"WriteCapacityUnits": 1
26+
}
27+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env sh
2+
3+
set -x
4+
5+
aws --version
6+
aws --endpoint-url "${DYNAMODB_URL}" dynamodb delete-table --table-name dialog-states || true
7+
aws --endpoint-url "${DYNAMODB_URL}" dynamodb create-table --cli-input-json file://dialogs/dynamodb/src/test/resources/dialog-states.table.json
8+
aws --endpoint-url "${DYNAMODB_URL}" dynamodb batch-write-item --request-items file://dialogs/dynamodb/src/test/resources/dialog-states.items.json

dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import by.jprof.telegram.bot.dialogs.model.DialogState
55
interface DialogStateDAO {
66
suspend fun get(chatId: Long, userId: Long): DialogState?
77

8-
suspend fun save(entity: DialogState)
8+
suspend fun save(dialogState: DialogState)
99

1010
suspend fun delete(chatId: Long, userId: Long)
1111
}

0 commit comments

Comments
 (0)