@ftschopp/dynatable-core
v1.2.0
Published
Core library for DynamoDB single table design
Readme
@ftschopp/dynatable-core
A type-safe, functional programming library for AWS DynamoDB with Single Table Design support. Built with TypeScript and designed to make DynamoDB development elegant and productive.
Features
- 🔐 Type-Safe - Full TypeScript support with end-to-end type inference
- 🎯 Single Table Design - Built-in support for DynamoDB best practices
- 🔄 Functional API - Chainable, composable operations with immutable builders
- ⚡️ Auto-generated IDs - ULID/UUID generation for unique identifiers
- 🕒 Automatic Timestamps - Auto-manage
createdAtandupdatedAt - 🔒 Transactions - Atomic operations with
TransactWriteandTransactGet - 📦 Batch Operations - Efficient
BatchGetandBatchWriteoperations - 🎨 Query Builder - Intuitive, type-safe API for complex queries
- ✅ Validation - Built-in Zod schema validation
- 🧪 Testable - Easy to mock and test with AWS SDK client mock support
Installation
npm install @ftschopp/dynatable-core
# or
yarn add @ftschopp/dynatable-core
# or
pnpm add @ftschopp/dynatable-coreQuick Start
import { Table } from '@ftschopp/dynatable-core';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
// Define your schema
const schema = {
format: 'dynatable:1.0.0',
version: '1.0.0',
indexes: {
primary: { hash: 'PK', sort: 'SK' },
},
models: {
User: {
key: {
PK: { type: String, value: 'USER#${username}' },
SK: { type: String, value: 'USER#${username}' },
},
attributes: {
username: { type: String, required: true },
name: { type: String, required: true },
email: { type: String, required: true },
},
},
},
params: {
timestamps: true,
isoDates: true,
},
} as const;
// Create DynamoDB client
const ddbClient = new DynamoDBClient({ region: 'us-east-1' });
const client = DynamoDBDocumentClient.from(ddbClient);
// Create table instance
const table = new Table({
name: 'MyTable',
client,
schema,
});
// Use it!
async function example() {
// Create
const user = await table.entities.User.put({
username: 'alice',
name: 'Alice Smith',
email: '[email protected]',
}).execute();
// Read
const retrieved = await table.entities.User.get({
username: 'alice',
}).execute();
// Update
await table.entities.User.update({ username: 'alice' })
.set('name', 'Alice Johnson')
.execute();
// Query
const users = await table.entities.User.query()
.where((attr, op) => op.eq(attr.username, 'alice'))
.execute();
// Delete
await table.entities.User.delete({ username: 'alice' }).execute();
}Core Concepts
Schema Definition
Define your data models with full type inference:
const schema = {
format: 'dynatable:1.0.0',
version: '1.0.0',
indexes: {
primary: { hash: 'PK', sort: 'SK' },
gsi1: { hash: 'GSI1PK', sort: 'GSI1SK' },
},
models: {
User: {
key: {
PK: { type: String, value: 'USER#${username}' },
SK: { type: String, value: 'USER#${username}' },
},
index: {
GSI1PK: { type: String, value: 'USER' },
GSI1SK: { type: String, value: 'USER#${username}' },
},
attributes: {
username: { type: String, required: true },
name: { type: String, required: true },
email: { type: String },
userId: { type: String, generate: 'ulid' },
followerCount: { type: Number, default: 0 },
// Nested object with typed schema
address: {
type: Object,
schema: {
street: { type: String },
city: { type: String, required: true },
country: { type: String, required: true },
},
},
// Typed array with item schema
tags: {
type: Array,
default: [],
items: { type: String },
},
},
},
},
params: {
timestamps: true, // Auto createdAt/updatedAt
isoDates: true, // Use ISO 8601 dates
cleanInternalKeys: false, // Hide PK/SK from results
},
} as const;Nested Objects and Arrays
Attributes support typed nested schemas for full TypeScript inference:
Story: {
attributes: {
// Typed array of objects
frames: {
type: Array,
default: [],
items: {
type: Object,
schema: {
url: { type: String, required: true },
duration: { type: Number },
mediaType: { type: String },
},
},
},
// Nested object with a typed schema
location: {
type: Object,
schema: {
city: { type: String },
country: { type: String },
lat: { type: Number },
lng: { type: Number },
},
},
},
}Use ArrayItem<T> to extract item types from array attributes:
import type { InferModelFromSchema, ArrayItem } from '@ftschopp/dynatable-core';
type StoryEntity = InferModelFromSchema<typeof schema, 'Story'>;
type StoryFrame = ArrayItem<StoryEntity['frames']>;
// → { url: string; duration?: number; mediaType?: string }Type Inference
Extract types from your schema:
import type {
InferModel,
InferInput,
InferKeyInput,
InferModelFromSchema,
InferInputFromSchema,
ArrayItem,
} from '@ftschopp/dynatable-core';
// Preferred: infer from the full schema (timestamps included automatically)
type User = InferModelFromSchema<typeof schema, 'User'>;
// { username: string; name: string; email?: string; userId: string; followerCount: number; createdAt: string; updatedAt: string }
type UserInput = InferInputFromSchema<typeof schema, 'User'>;
// { username: string; name: string; email?: string; followerCount?: number }
// Full model type (deprecated — use InferModelFromSchema)
type UserLegacy = InferModel<typeof schema.models.User>;
// Input type (deprecated — use InferInputFromSchema)
type UserInputLegacy = InferInput<typeof schema.models.User>;
// Key input type (only key template variables)
type UserKey = InferKeyInput<typeof schema.models.User>;
// { username: string }
// Extract item type from an array attribute
type StoryEntity = InferModelFromSchema<typeof schema, 'Story'>;
type StoryFrame = ArrayItem<StoryEntity['frames']>;
// → { url: string; duration?: number; mediaType?: string }Builder Operations
All operations use the immutable builder pattern:
// GET - Retrieve item
const user = await table.entities.User.get({ username: 'alice' })
.select(['name', 'email'])
.consistentRead()
.execute();
// PUT - Insert/replace item
await table.entities.User.put({
username: 'alice',
name: 'Alice',
email: '[email protected]',
})
.ifNotExists()
.returning('ALL_OLD')
.execute();
// UPDATE - Modify attributes
await table.entities.User.update({ username: 'alice' })
.set('name', 'Alice Johnson')
.add('followerCount', 1)
.remove('email')
.returning('ALL_NEW')
.where((attr, op) => op.gt(attr.followerCount, 0))
.execute();
// DELETE - Remove item
await table.entities.User.delete({ username: 'alice' })
.returning('ALL_OLD')
.where((attr, op) => op.exists(attr.email))
.execute();
// QUERY - Query with conditions
const photos = await table.entities.Photo.query()
.where((attr, op) =>
op.and(
op.eq(attr.username, 'alice'),
op.gt(attr.likesCount, 10)
)
)
.limit(20)
.scanIndexForward(false)
.execute();
// SCAN - Full table scan with filter
const activeUsers = await table.entities.User.scan()
.where((attr, op) => op.gt(attr.followerCount, 1000))
.limit(50)
.execute();
// BATCH GET - Retrieve multiple items
const users = await table.entities.User.batchGet([
{ username: 'alice' },
{ username: 'bob' },
{ username: 'charlie' },
]).execute();
// BATCH WRITE - Write multiple items
await table.entities.User.batchWrite([
{ username: 'alice', name: 'Alice', email: '[email protected]' },
{ username: 'bob', name: 'Bob', email: '[email protected]' },
]).execute();Transactions
Atomic operations across multiple items:
// TransactWrite - Atomic writes
await table.transactWrite()
.addPut(
table.entities.Like.put({
photoId: 'photo1',
likingUsername: 'alice',
})
.ifNotExists()
.dbParams()
)
.addUpdate(
table.entities.Photo.update({ photoId: 'photo1' })
.add('likesCount', 1)
.dbParams()
)
.execute();
// TransactGet - Atomic reads
const result = await table.transactGet()
.addGet(table.entities.User.get({ username: 'alice' }).dbParams())
.addGet(table.entities.Photo.get({ photoId: 'photo1' }).dbParams())
.execute();
const [user, photo] = result.items;Available Operators
Build complex conditions with type-safe operators:
Comparison
eq(attr, value)- Equalsne(attr, value)- Not equalslt(attr, value)- Less thanlte(attr, value)- Less than or equalgt(attr, value)- Greater thangte(attr, value)- Greater than or equalbetween(attr, low, high)- Between values
String
beginsWith(attr, prefix)- Begins with prefixcontains(attr, value)- Contains value (strings, sets, lists)
Existence
exists(attr)- Attribute existsnotExists(attr)- Attribute doesn't exist
Advanced
attributeType(attr, type)- Check attribute type ('S', 'N', 'M', 'L', etc.)in(attr, values[])- Value in arraysize(attr)- Get size, returns object with.eq(),.gt(), etc.
Logical
and(...conditions)- Combine with ANDor(...conditions)- Combine with ORnot(condition)- Negate condition
Examples
// Exists check
await table.entities.User.update({ username: 'alice' })
.set('email', '[email protected]')
.where((attr, op) => op.notExists(attr.email))
.execute();
// Contains
const users = await table.entities.User.query()
.where((attr, op) =>
op.and(
op.eq(attr.username, 'alice'),
op.contains(attr.tags, 'premium')
)
)
.execute();
// IN operator
const activeUsers = await table.entities.User.scan()
.where((attr, op) => op.in(attr.status, ['active', 'pending']))
.execute();
// Size function
const posts = await table.entities.Post.query()
.where((attr, op) =>
op.and(
op.eq(attr.userId, 'alice'),
op.size(attr.tags).gte(3)
)
)
.execute();
// Complex nested conditions
await table.entities.Photo.query()
.where((attr, op) =>
op.and(
op.eq(attr.username, 'alice'),
op.or(
op.gt(attr.likesCount, 100),
op.and(
op.gt(attr.commentCount, 50),
op.exists(attr.featured)
)
)
)
)
.execute();DynamoDB Logger
Debug your DynamoDB operations:
import { createDynamoDBLogger } from '@ftschopp/dynatable-core';
const logger = createDynamoDBLogger({
enabled: true,
logParams: true,
logResponse: false,
});
const table = new Table({
name: 'MyTable',
client,
schema,
logger, // Attach logger
});
// All operations now logged to consolePagination
Built-in pagination support:
// Execute with pagination
const page1 = await table.entities.Post.query()
.where((attr, op) => op.eq(attr.userId, 'alice'))
.limit(20)
.executeWithPagination();
// Get next page
if (page1.lastEvaluatedKey) {
const page2 = await table.entities.Post.query()
.where((attr, op) => op.eq(attr.userId, 'alice'))
.startFrom(page1.lastEvaluatedKey)
.limit(20)
.executeWithPagination();
}API Reference
Core Exports
export {
Table, // Main Table class
type SchemaDefinition, // Schema type
type ModelDefinition, // Model type
type AttributeDefinition, // Union of all attribute types
type ScalarAttributeDefinition, // String/Number/Boolean/Date attribute
type ObjectAttributeDefinition, // Nested object attribute with schema
type ArrayAttributeDefinition, // Typed array attribute with items
type PrimaryKeyDefinition, // PK + SK key definition
type KeyDefinition, // Single key definition
type IndexDefinition, // Index (hash + optional sort)
type IndexesDefinition, // All table indexes
type SchemaParams, // Global schema params
type InferModel, // Infer model type (deprecated)
type InferInput, // Infer input type (deprecated)
type InferKeyInput, // Infer key type
type InferModelFromSchema, // Infer from full schema (preferred)
type InferInputFromSchema, // Infer input from full schema (preferred)
type TimestampFields, // createdAt/updatedAt fields type
type ArrayItem, // Extract item type from array attribute
createDynamoDBLogger, // Logger factory
type DynamoDBLogger, // Logger type
type DynamoDBLoggerConfig, // Logger config
};Builder Types
Each builder type is exported for advanced use cases:
GetBuilder,PutBuilder,UpdateBuilder,DeleteBuilderQueryBuilder,ScanBuilderBatchGetBuilder,BatchWriteBuilderTransactWriteBuilder,TransactGetBuilderCondition,OpBuilder,AttrBuilder,SizeRef
Requirements
- Node.js >= 18
- TypeScript >= 5.0 (recommended)
- AWS SDK v3 (
@aws-sdk/client-dynamodb,@aws-sdk/lib-dynamodb)
Dependencies
@aws-sdk/client-dynamodb^3.965.0@aws-sdk/lib-dynamodb^3.965.0zod^4.3.5 - Runtime validationulid^3.0.2 - ULID generationramda^0.32.0 - Functional utilities
Documentation
For complete documentation, examples, and guides, visit the main repository.
Testing
The library includes comprehensive test coverage with Jest:
npm testLicense
MIT
Contributing
Contributions are welcome! Please see the main repository for contribution guidelines.
Related Packages
- @ftschopp/dynatable-migrations - Database migration tool for schema evolution
