@verisure-italy/dynamo-kit
v1.8.6
Published
DynamoDB utility library
Readme
DynamoDB Kit
A TypeScript toolkit for working with AWS DynamoDB, providing a simplified client interface with automatic timestamp management, type-safe operations, and repository pattern implementation.
Installation
npm install @verisure-italy/dynamo-kit
# or
pnpm add @verisure-italy/dynamo-kitFeatures
- 🔧 Easy Client Configuration: Simplified DynamoDB client setup with smart defaults
- ⏰ Automatic Timestamps: Auto-managed
createdAtandupdatedAtfields - 🎯 Type Safety: Full TypeScript support with generic types
- 🏗️ Repository Pattern: High-level repository abstraction for common operations
- 🚀 Performance: Built-in client caching and optimized marshalling
- 🌍 Environment Aware: Automatic local/offline detection
- 📊 Query Builder: Fluent query and expression builders
Quick Start
import { getDynamoClient, createRepo } from '@verisure-italy/dynamo-kit'
// Define your entity type
interface User {
id: string
email: string
name: string
age?: number
}
// Create a repository
const userRepo = createRepo<User>({
baseTableName: 'users',
id: { idField: 'id' }
})
// Use the repository
const user = await userRepo.put({
id: 'user-123',
email: '[email protected]',
name: 'John Doe',
age: 30
})
console.log(user) // Includes createdAt and updatedAt timestampsClient Configuration
Creating a DynamoDB Client
import { getDynamoClient } from '@verisure-italy/dynamo-kit'
// Use default configuration (reads from environment variables)
const client = getDynamoClient()
// Override specific settings
const client = getDynamoClient({
region: 'us-east-1',
endpoint: 'http://localhost:8000', // for local DynamoDB
tablePrefix: 'dev',
credentials: {
accessKeyId: 'your-access-key',
secretAccessKey: 'your-secret-key'
}
})Configuration Options
The client accepts the following configuration options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| region | string | 'eu-west-1' | AWS region |
| endpoint | string | undefined | Custom DynamoDB endpoint (auto-detected for local) |
| tablePrefix | string | '' | Prefix for all table names |
| credentials | AwsCredentialIdentity | undefined | AWS credentials |
Environment Variables
The kit automatically reads these environment variables:
AWS_REGION: Sets the default regionDYNAMO_ENDPOINT: Custom DynamoDB endpointDYNAMO_TABLE_PREFIX: Default table prefixIS_OFFLINE/AWS_SAM_LOCAL: Auto-enables local development mode
Local Development Setup
// Automatic local detection
const client = getDynamoClient() // Detects IS_OFFLINE=true
// Manual local configuration
const localClient = getDynamoClient({
region: 'local',
endpoint: 'http://localhost:8000'
})Repository Pattern
Creating a Repository
import { createRepo } from '@verisure-italy/dynamo-kit'
interface Product {
productId: string
name: string
price: number
category: string
}
const productRepo = createRepo<Product>({
baseTableName: 'products',
id: { idField: 'productId' }, // Simple ID field
// Optional: provide custom client
// client: getDynamoClient({ region: 'us-west-2' })
})Complex Key Configuration
For composite keys or custom key generation:
interface Order {
customerId: string
orderId: string
items: OrderItem[]
total: number
}
const orderRepo = createRepo<Order>({
baseTableName: 'orders',
id: {
keyOf: (orderOrId) => {
if (typeof orderOrId === 'string') {
// If just orderId is provided
const [customerId, orderId] = orderOrId.split('#')
return { customerId, orderId }
}
// If full order object is provided
return {
customerId: orderOrId.customerId,
orderId: orderOrId.orderId
}
}
}
})Repository Operations
Create/Update Items
// Create new item (auto-adds createdAt and updatedAt)
const newUser = await userRepo.put({
id: 'user-123',
email: '[email protected]',
name: 'John Doe'
})
// Update existing item (preserves createdAt, updates updatedAt)
const existingUser = await userRepo.put({
id: 'user-123',
email: '[email protected]',
name: 'John Doe Updated',
createdAt: 1697234567 // Existing timestamp preserved
})Retrieve Items
// Get item by ID
const user = await userRepo.get('user-123')
// Get specific fields only
const userEmail = await userRepo.get('user-123', ['email', 'name'])Update Items
// Partial update (auto-updates updatedAt)
const updatedUser = await userRepo.update('user-123', {
name: 'John Smith',
age: 31
})
// Remove fields by setting to undefined
const cleanUser = await userRepo.update('user-123', {
age: undefined // This field will be removed
})Delete Items
await userRepo.remove('user-123')Scan Operations
// Scan all items
const allUsers = await userRepo.scan()
// Scan with pagination
const page1 = await userRepo.scan({ Limit: 10 })
const page2 = await userRepo.scan({
Limit: 10,
ExclusiveStartKey: page1.nextToken
})
// Scan specific fields only
const userNames = await userRepo.scan({
fields: ['id', 'name'],
Limit: 100
})Query Global Secondary Indexes
// Query by email index
const usersByEmail = await userRepo.queryIndex({
index: 'email-index',
hash: { field: 'email', value: '[email protected]' }
})
// Query with range condition
const recentOrders = await userRepo.queryIndex({
index: 'customer-date-index',
hash: { field: 'customerId', value: 'customer-123' },
range: {
field: 'createdAt',
op: '>=',
value: Date.now() / 1000 - 86400 // Last 24 hours
}
})
// Query with BETWEEN range
const ordersInRange = await userRepo.queryIndex({
index: 'customer-date-index',
hash: { field: 'customerId', value: 'customer-123' },
range: {
field: 'createdAt',
op: 'between',
value: [startTimestamp, endTimestamp]
},
scanIndexForward: false, // Descending order
limit: 50
})
// Query with begins_with
const usersByPrefix = await userRepo.queryIndex({
index: 'name-index',
hash: { field: 'status', value: 'active' },
range: {
field: 'name',
op: 'begins_with',
value: 'John'
},
fields: ['id', 'name', 'email']
})Range Operators
Available operators for range conditions:
'=': Exact match'<': Less than'<=': Less than or equal'>': Greater than'>=': Greater than or equal'begins_with': String prefix match'between': Value between two bounds (inclusive)
Advanced Filtering
Both scan and queryIndex support advanced filtering with FilterExpression:
import { FilterCondition } from '@verisure-italy/dynamo-kit'
// Filter with scan
const activeAdults = await userRepo.scan({
filters: [
{ field: 'status', op: '=', value: 'active' },
{ field: 'age', op: '>=', value: 18 }
],
Limit: 50
})
// Filter with query
const results = await userRepo.queryIndex({
index: 'status-index',
hash: { field: 'status', value: 'active' },
filters: [
{ field: 'age', op: 'between', value: [18, 65] },
{ field: 'country', op: 'in', value: ['US', 'UK', 'CA'] }
],
limit: 100
})Filter Operators
Available filter operators:
- Comparison:
'=','<>','<','<=','>','>=' - String:
'begins_with','contains' - Range/Array:
'between','in' - Existence:
'attribute_exists','attribute_not_exists'
Filter Examples
// Equality
{ field: 'status', op: '=', value: 'active' }
// Greater than
{ field: 'age', op: '>', value: 18 }
// Between (inclusive)
{ field: 'age', op: 'between', value: [18, 65] }
// In array
{ field: 'country', op: 'in', value: ['US', 'UK', 'CA'] }
// String prefix
{ field: 'email', op: 'begins_with', value: 'admin' }
// String contains
{ field: 'description', op: 'contains', value: 'urgent' }
// Attribute exists
{ field: 'phoneNumber', op: 'attribute_exists' }
// Attribute does not exist
{ field: 'deletedAt', op: 'attribute_not_exists' }Multiple Filters (AND Logic)
All filters are combined with AND logic:
const results = await userRepo.scan({
filters: [
{ field: 'status', op: '=', value: 'active' },
{ field: 'age', op: '>=', value: 18 },
{ field: 'country', op: 'in', value: ['US', 'UK'] },
{ field: 'emailVerified', op: '=', value: true }
]
})
// Returns items where status='active' AND age>=18 AND country IN ('US','UK') AND emailVerified=truePagination
All scan and query operations return a Page<T> object:
interface Page<T> {
items: T[] // The retrieved items
count: number // Number of items in this page
nextToken: Record<string, any> | null // Token for next page
}
// Example pagination loop
let nextToken = null
do {
const page = await userRepo.scan({ Limit: 100, ExclusiveStartKey: nextToken })
console.log(`Found ${page.count} users`)
page.items.forEach(user => console.log(user.name))
nextToken = page.nextToken
} while (nextToken)Automatic Timestamps
All write operations automatically manage timestamp fields:
// PUT operation
const user = await userRepo.put({ id: '123', name: 'John' })
// Result: { id: '123', name: 'John', createdAt: 1697234567, updatedAt: 1697234567 }
// UPDATE operation
const updated = await userRepo.update('123', { name: 'Jane' })
// Result: { id: '123', name: 'Jane', createdAt: 1697234567, updatedAt: 1697234890 }To override timestamps for specific operations:
// Preserve existing createdAt when using put()
const user = await userRepo.put({
id: '123',
name: 'John',
createdAt: existingTimestamp // This will be preserved
})Advanced Examples
Multi-Environment Setup
import { getDynamoClient, createRepo } from '@verisure-italy/dynamo-kit'
// Development
const devClient = getDynamoClient({
endpoint: 'http://localhost:8000',
tablePrefix: 'dev'
})
// Staging
const stagingClient = getDynamoClient({
region: 'eu-west-1',
tablePrefix: 'staging'
})
// Production
const prodClient = getDynamoClient({
region: 'eu-west-1',
tablePrefix: 'prod'
})
// Create environment-specific repositories
const devUserRepo = createRepo<User>({
baseTableName: 'users',
id: { idField: 'id' },
client: devClient
})Complex Queries with Extra Parameters
// Query with additional DynamoDB parameters
const results = await userRepo.queryIndex({
index: 'status-created-index',
hash: { field: 'status', value: 'active' },
range: { field: 'createdAt', op: '>=', value: lastWeek },
limit: 100,
scanIndexForward: false,
extra: {
FilterExpression: '#age > :minAge AND attribute_exists(#email)',
ExpressionAttributeNames: {
'#age': 'age',
'#email': 'email'
},
ExpressionAttributeValues: {
':minAge': 18
}
}
})Custom Table Names
// Use resolved table name directly
const customRepo = createRepo<User>({
baseTableName: 'users', // This gets prefixed
// OR use tableName to bypass prefixing
tableName: 'my-exact-table-name', // This is used as-is
id: { idField: 'id' }
})Type Safety
The kit provides full TypeScript support:
interface User {
id: string
email: string
name: string
age?: number
}
const repo = createRepo<User>({
baseTableName: 'users',
id: { idField: 'id' }
})
// TypeScript ensures type safety
const user = await repo.get('123') // Returns User | null
const updated = await repo.update('123', {
name: 'New Name' // ✅ Valid
// invalidField: 'value' // ❌ TypeScript error
})Error Handling
try {
const user = await userRepo.get('nonexistent')
// user will be null if not found
} catch (error) {
// Handle DynamoDB errors
console.error('DynamoDB error:', error)
}Performance Tips
- Client Caching: Clients are automatically cached based on configuration
- Field Selection: Use
fieldsparameter to retrieve only needed attributes - Pagination: Always use pagination for large datasets
- Batch Operations: For bulk operations, consider using the raw DynamoDB client
- Indexes: Design GSIs for your query patterns
Migration from Raw DynamoDB
// Before (raw DynamoDB)
const params = {
TableName: 'users',
Key: { id: 'user-123' },
UpdateExpression: 'SET #name = :name, #updatedAt = :now',
ExpressionAttributeNames: { '#name': 'name', '#updatedAt': 'updatedAt' },
ExpressionAttributeValues: { ':name': 'New Name', ':now': Date.now() / 1000 },
ReturnValues: 'ALL_NEW'
}
const result = await client.send(new UpdateCommand(params))
// After (with dynamo-kit)
const user = await userRepo.update('user-123', { name: 'New Name' })License
MIT
