Skip to content

Commit 3550d33

Browse files
feat(dynamodb): Implement the DynamoDB Storage Adapter
1 parent 1725255 commit 3550d33

File tree

10 files changed

+3055
-1
lines changed

10 files changed

+3055
-1
lines changed

packages/dynamodb/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5+
6+
## 0.1.0: Initial version

packages/dynamodb/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021-2025 grammyjs, thelleboid-tsr
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/dynamodb/README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# DynamoDB Storage for grammY
2+
3+
This package provides a [DynamoDB](https://aws.amazon.com/dynamodb/) storage adapter for [grammY](https://grammy.dev) sessions.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @grammyjs/storage-dynamodb @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
9+
```
10+
11+
## Usage
12+
13+
### With Sessions
14+
15+
```typescript
16+
import { Bot, Context, session, SessionFlavor } from 'grammy';
17+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
18+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
19+
import { DynamoDBAdapter } from '@grammyjs/storage-dynamodb';
20+
21+
// Define the shape of our session.
22+
interface SessionData {
23+
counter: number;
24+
}
25+
type MyContext = Context & SessionFlavor<SessionData>;
26+
27+
const bot = new Bot<MyContext>('your-bot-token');
28+
29+
// Build your own DynamoDBClient. You may need to pass credentials here
30+
const client = new DynamoDBClient({
31+
region: 'us-east-1',
32+
});
33+
const docClient = DynamoDBDocumentClient.from(client);
34+
35+
bot.use(
36+
session({
37+
initial: () => ({ counter: 0 }),
38+
storage: new DynamoDBAdapter({
39+
instance: docClient,
40+
tableName: 'telegram_sessions',
41+
ttl: 60 * 60 * 24 * 30, // 30 days
42+
}),
43+
})
44+
);
45+
```
46+
47+
### With Conversations
48+
49+
```typescript
50+
import { Bot, Context } from 'grammy';
51+
import { ConversationFlavor, conversations } from '@grammyjs/conversations';
52+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
53+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
54+
import { DynamoDBAdapter } from '@grammyjs/storage-dynamodb';
55+
56+
// Build your own DynamoDBClient. You may need to pass credentials here
57+
const client = new DynamoDBClient({
58+
region: 'us-east-1',
59+
});
60+
const docClient = DynamoDBDocumentClient.from(client);
61+
62+
const bot = new Bot<ConversationFlavor<Context>>('your-bot-token');
63+
64+
bot.use(
65+
conversations({
66+
storage: new DynamoDBAdapter({
67+
instance: docClient,
68+
tableName: 'ConversationSessions',
69+
ttl: 24 * 60 * 60, // 24 hours in seconds
70+
}),
71+
})
72+
);
73+
```
74+
75+
## Configuration
76+
77+
The `DynamoDBAdapter` constructor accepts the following options:
78+
79+
- `instance` (required): An instance of `DynamoDBDocumentClient`
80+
- `tableName` (required): The name of the DynamoDB table
81+
- `ttl` (optional): Session time to live in SECONDS. If not provided, uncleaned sessions (due to crash) may stay forever
82+
- `sessionKey` (optional): The name of the primary key field in the DynamoDB table. Defaults to `'sessionKey'`
83+
- `ttlKey` (optional): The name of the TTL field in the DynamoDB table. Defaults to `'ttl'`
84+
85+
## DynamoDB Table Setup
86+
87+
You need to create a DynamoDB table with the following structure:
88+
89+
### Table Configuration
90+
91+
- **Table name**: `GrammySessions` (or your custom table name)
92+
- **Partition key**: `sessionKey` (or the value you've set for `sessionKey`)
93+
- **Sort key**: None
94+
95+
### Using AWS CLI
96+
97+
```bash
98+
aws dynamodb create-table \
99+
--table-name GrammySessions \
100+
--attribute-definitions AttributeName=sessionKey,AttributeType=S \
101+
--key-schema AttributeName=sessionKey,KeyType=HASH \
102+
--billing-mode PAY_PER_REQUEST
103+
```
104+
105+
### Using Terraform
106+
107+
```hcl
108+
resource "aws_dynamodb_table" "grammy_sessions" {
109+
name = "GrammySessions"
110+
billing_mode = "PAY_PER_REQUEST"
111+
hash_key = "sessionKey"
112+
113+
attribute {
114+
name = "sessionKey"
115+
type = "S"
116+
}
117+
}
118+
```
119+
120+
## TTL (Time To Live)
121+
122+
Grammy automatically cleans session and conversation data. For session, [data is removed the next time the respective session data is read](https://grammy.dev/plugins/session#timeouts). For conversation, data is removed when the conversation ends.
123+
124+
This adapter allows to leverage the [native DynamoDB TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) to remove items after some time. This helps prevent the table from growing indefinitely. You can enable TTL on your DynamoDB table to automatically delete expired items:
125+
126+
```bash
127+
aws dynamodb update-time-to-live \
128+
--table-name GrammySessions \
129+
--time-to-live-specification Enabled=true,AttributeName=ttl
130+
```
131+
132+
## Authentication
133+
134+
Since you pass the DynamoDB client instance yourself, you have full control over authentication. The AWS SDK supports several authentication methods:
135+
136+
1. **Environment variables**: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
137+
2. **AWS credentials file**: `~/.aws/credentials`
138+
3. **IAM roles** (when running on EC2/Lambda/ECS)
139+
4. **Explicit credentials** in the client constructor
140+
141+
## Required IAM Permissions
142+
143+
Make sure your AWS credentials have the following DynamoDB permissions:
144+
145+
```json
146+
{
147+
"Version": "2012-10-17",
148+
"Statement": [
149+
{
150+
"Effect": "Allow",
151+
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"],
152+
"Resource": "arn:aws:dynamodb:region:account-id:table/GrammySessions"
153+
}
154+
]
155+
}
156+
```
157+
158+
## Error Handling
159+
160+
The adapter includes built-in error handling and logging. Errors during read operations return `undefined`, while write and delete operations will throw errors.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Bot, session } from 'grammy';
2+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
4+
import { DynamoDBAdapter } from '@grammyjs/storage-dynamodb';
5+
6+
const client = new DynamoDBClient({
7+
region: 'us-east-1',
8+
// Optional: provide credentials if not using AWS environment
9+
// credentials: {
10+
// accessKeyId: 'your-access-key-id',
11+
// secretAccessKey: 'your-secret-access-key'
12+
// }
13+
});
14+
15+
const docClient = DynamoDBDocumentClient.from(client);
16+
17+
const adapter = new DynamoDBAdapter({
18+
instance: docClient,
19+
tableName: 'GrammySessions',
20+
ttl: 24 * 60 * 60, // 24 hours in seconds
21+
});
22+
23+
interface SessionData {
24+
counter: number;
25+
lastMessage?: string;
26+
}
27+
28+
const bot = new Bot<SessionData>('your-bot-token');
29+
30+
bot.use(session({
31+
initial: () => ({ counter: 0 }),
32+
storage: adapter,
33+
}));
34+
35+
bot.command('start', (ctx) => {
36+
ctx.session.counter++;
37+
ctx.reply(`Welcome! This is your visit #${ctx.session.counter}`);
38+
});
39+
40+
bot.on('message:text', (ctx) => {
41+
ctx.session.counter++;
42+
ctx.session.lastMessage = ctx.message.text;
43+
ctx.reply(`Message #${ctx.session.counter}: "${ctx.message.text}"`);
44+
});
45+
46+
bot.start();

0 commit comments

Comments
 (0)