A dynamodb Provider that simplifies the native api. It packs with a Single Table adaptor/pseudo ORM for single table design
npm install dynamodb-provider


Fast Develop for DynamoDB with this type-safe & single-table awareness library!
> Min Node version: 16
New documentation website here
The DynamoDB SDK (both v2 and v3) lacks type safety and requires significant boilerplate. Building expressions, avoiding attribute name collisions, and managing code repetition typically results in verbose, hard-to-maintain abstractions.
This library wraps DynamoDB operations with type-safe methods that work for both table-per-entity and single-table designs. Apart from the ksuid for ID generation, it has zero dependencies.
The library has three parts:
1. DynamoDB Provider - Type-safe wrappers around DynamoDB operations (get, update, query, transaction, etc.). Use this for table-per-entity designs.
2. SingleTable - Table configuration layer that removes repetition when all operations target the same table with fixed keys and indexes.
3. Schema - Entity and collection definitions for single-table designs, with partition and access pattern management.
Each part builds on the previous. Use only what you need—the provider works standalone, and SingleTable works without schemas.
If you want to instruct your agents/LLMs, take a look at the ai folder.
The provider wraps AWS SDK clients (v2 or v3) with type-safe methods. Only DocumentClient instances are supported.
``ts
import { DynamoDB } from 'aws-sdk';
import { DynamodbProvider } from 'dynamodb-provider'
const provider = new DynamodbProvider({
dynamoDB: {
target: 'v2',
instance: new DynamoDB.DocumentClient({
// any config you may need. region, credentials...
}),
},
});
`
`ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
BatchGetCommand,
GetCommand,
DeleteCommand,
PutCommand,
UpdateCommand,
ScanCommand,
QueryCommand,
TransactWriteCommand,
} from "@aws-sdk/lib-dynamodb";
import { DynamodbProvider } from 'dynamodb-provider'
const ddbClient = new DynamoDBClient({
// any config you may need. region, credentials...
});
const documentClient = DynamoDBDocumentClient.from(ddbClient);
const provider = new DynamodbProvider({
dynamoDB: {
target: 'v3',
instance: documentClient,
commands: {
BatchGetCommand,
GetCommand,
DeleteCommand,
PutCommand,
UpdateCommand,
ScanCommand,
QueryCommand,
TransactWriteCommand,
};
},
});
`
The library doesn't bundle AWS SDK packages—install the version you need.
`ts`
interface DynamoDbProviderParams {
dynamoDB: DynamoDBConfig;
logCallParams?: boolean;
}
logCallParams - Logs the parameters sent to DynamoDB before each operation. Useful for debugging.
Here you'll find each method exposed on the provider.
Quick Access
- get
- create
- delete
- update
- batchGet
- list
- listAll
- query
Retrieves a single item by primary key.
`ts`
get
params: GetItemParams
): Promise
Parameters:
- table - Table namekey
- - Primary key (partition key and optionally sort key)consistentRead
- - Use strongly consistent reads (default: false)propertiesToRetrieve
- - Specific attributes to return (root-level only)
Returns the item or undefined if not found.
Example:
`ts
interface User {
userId: string;
name: string;
email: string;
age: number;
}
const user = await provider.get
table: 'UsersTable',
key: { userId: '12345' },
consistentRead: true,
propertiesToRetrieve: ['name', 'email'],
});
`
Creates an item in the table. DynamoDB's PutItem overwrites existing items—use conditions to prevent this.
`ts`
create
Parameters:
- table - Table nameitem
- - Item to create (must include primary key)conditions
- - Optional conditions that must be met before creating
Example:
`ts`
const user = await provider.create({
table: 'Users',
item: {
userId: '12345',
name: 'John Doe',
email: 'john@example.com',
age: 30,
},
conditions: [
{ operation: 'not_exists', property: 'userId' }
],
});
For composite keys, as dynamodb doc, check both:
`ts`
conditions: [
{ operation: 'not_exists', property: 'partitionKey' },
{ operation: 'not_exists', property: 'sortKey' }
]
Condition Operations:
- equalnot_equal
- lower_than
- lower_or_equal_than
- bigger_than
- bigger_or_equal_than
- begins_with
- contains
- not_contains
- between
- in
- not_in
- exists
- not_exists
-
Condition Structure:
`ts`
{
property: string;
operation: ExpressionOperation;
value?: string | number; // for basic operations
values?: (string | number)[]; // for 'in', 'not_in'
start?: string | number; // for 'between'
end?: string | number; // for 'between'
joinAs?: 'and' | 'or'; // default: 'and'
nested?: ItemExpression[]; // for parenthesized expressions
}
Nested Conditions:
Use nested for complex parenthesized conditions:
`ts`
conditions: [
{
property: 'status',
operation: 'equal',
value: 'active',
nested: [
{ property: 'price', operation: 'lower_than', value: 100, joinAs: 'or' },
{ property: 'featured', operation: 'equal', value: true }
]
}
]
// Generates: (status = 'active' AND (price < 100 OR featured = true))
Deletes an item by primary key.
`ts`
delete
Parameters:
- table - Table namekey
- - Primary key of the item to deleteconditions
- - Optional conditions that must be met before deleting
Example:
`ts`
await provider.delete({
table: 'Users',
key: { id: '12345' },
conditions: [
{ operation: 'exists', property: 'id' }
]
});
Updates an item with support for value updates, property removal, and atomic operations.
`ts`
update
Parameters:
- table - Table namekey
- - Primary keyvalues
- - Properties to updateremove
- - Properties to remove (root-level only)atomicOperations
- - Atomic operations (see details below)conditions
- - Conditions that must be metreturnUpdatedProperties
- - Return updated values (useful for counters)
Example:
`ts`
const updated = await provider.update({
table: 'Users',
key: { userId: '12345' },
values: { name: 'John Doe' },
atomicOperations: [
{ operation: 'add', property: 'loginCount', value: 1 }
],
conditions: [
{ operation: 'exists', property: 'userId' }
],
returnUpdatedProperties: true
});
// returns { name: 'John Doe', loginCount: 43 }
Atomic operations can include inline conditions:
`ts`
await provider.update({
table: 'Items',
key: { id: '12' },
atomicOperations: [
{
operation: 'subtract',
property: 'count',
value: 1,
if: { operation: 'bigger_than', value: 0 } // prevents negative
}
],
})
Atomic Operations:
- Math Operations:
- sum - Add to existing value (fails if property doesn't exist)subtract
- - Subtract from existing value (fails if property doesn't exist)add
- - Add to value, auto-initializes to 0 if missing
- Set Operations:
- add_to_set - Add values to a DynamoDB Setremove_from_set
- - Remove values from a Set
- Conditional:
- set_if_not_exists - Set value only if property doesn't existrefProperty
- Optional - Check different property for existence
`ts`
atomicOperations: [
{ type: 'add', property: 'count', value: 1 }, // safe, auto-init to 0
{ type: 'sum', property: 'total', value: 50 }, // requires existing value
{
type: 'set_if_not_exists',
property: 'status',
value: 'pending',
refProperty: 'createdAt' // set status if createdAt missing
}
]
Counter pattern for sequential IDs:
`ts
const { count } = await provider.update({
table: 'Counters',
key: { name: 'USER_ID' },
atomicOperations: [{ operation: 'add', property: 'count', value: 1 }],
returnUpdatedProperties: true
});
await provider.create({
table: 'Users',
item: { id: count, name: 'John' }
});
`
Retrieves multiple items by primary keys. Automatically handles batches >100 items and retries unprocessed items.
`ts`
batchGet
Parameters:
- table - Table namekeys
- - Array of primary keysconsistentRead
- - Use strongly consistent reads (default: false)propertiesToRetrieve
- - Specific attributes to returnthrowOnUnprocessed
- - Throw if items remain unprocessed after retries (default: false)maxRetries
- - Max retry attempts for unprocessed items (default: 8)
Example:
`ts`
const products = await provider.batchGet({
table: 'Products',
keys: [
{ productId: '123' },
{ productId: '456' }
],
consistentRead: true,
propertiesToRetrieve: ['name', 'price'],
});
Scans a table with optional filters, limits, and pagination.
`ts`
list
Parameters:
- table - Table namepropertiesToRetrieve
- - Attributes to returnfilters
- - Filter conditions (value, array, or filter config)limit
- - Max items to returnconsistentRead
- - Use strongly consistent reads (default: false)parallelRetrieval
- - Parallel scan config: { segment, total }index
- - Index name to scanpaginationToken
- - Continue from previous scan
Returns { items, paginationToken? }
Filter syntaxes:
- { status: 'active' } - Equality{ status: ['active', 'pending'] }
- - IN operation{ price: { operation: 'bigger_than', value: 100 } }
- - Complex filter
Example:
`ts`
const result = await provider.list('Products', {
filters: {
category: 'electronics',
price: { operation: 'bigger_than', value: 100 }
},
limit: 100
});
Scans entire table, automatically handling pagination. Same options as list except no limit or paginationToken.
`ts`
listAll
Example:
`ts`
const products = await provider.listAll('Products', {
filters: { category: 'electronics' },
propertiesToRetrieve: ['productId', 'name', 'price']
});
Queries items by partition key with optional range key conditions.
`ts`
query
Parameters:
- table - Table namepartitionKey
- - { name, value } for partition keyrangeKey
- - Range key condition with operations: equal, lower_than, lower_or_equal_than, bigger_than, bigger_or_equal_than, begins_with, betweenindex
- - Index name to queryretrieveOrder
- - ASC or DESC (default: ASC)limit
- - Max items to returnfullRetrieval
- - Auto-paginate until all items retrieved (default: false)paginationToken
- - Continue from previous queryfilters
- - Additional filter expressionspropertiesToRetrieve
- - Specific attributes to return (root-level only)
Returns { items, paginationToken? }
Example:
`ts`
const { items } = await provider.query({
table: 'Orders',
partitionKey: { name: 'customerId', value: '12345' },
rangeKey: {
name: 'orderId',
operation: 'bigger_or_equal_than',
value: 'A100'
},
retrieveOrder: 'DESC',
limit: 10,
filters: { status: 'shipped' },
propertiesToRetrieve: ['orderId', 'totalAmount', 'createdAt']
});
Queries for the first item matching the criteria. Returns the item directly or undefined if no match found.
`ts`
queryOne
Parameters:
Same as query, except:limit
- No - always queries for 1 itempaginationToken
- No - returns first match onlyfullRetrieval
- No - automatically set to false
Example:
`ts
const user = await provider.queryOne({
table: 'Users',
partitionKey: { name: 'email', value: 'user@example.com' }
});
if (user) {
console.log(Found user: ${user.name});`
}
Queries for all items matching the criteria. Auto-paginates through all results and returns items directly as an array. Same behavior as query with fullRetrieval: true. Can't be paginated over like that case, however.
`ts`
queryAll
Parameters:
Same as query, except:paginationToken
- No - automatically handles paginationfullRetrieval
- No - always set to true internallylimit
- (optional) - maximum total items to return (stops pagination when limit reached)
Example:
`ts
const allOrders = await provider.queryAll({
table: 'Orders',
partitionKey: { name: 'customerId', value: '12345' },
rangeKey: {
name: 'createdAt',
operation: 'bigger_or_equal_than',
value: '2024-01-01'
},
filters: { status: 'completed' },
limit: 100 // Optional: max total items
});
console.log(Found ${allOrders.length} completed orders);`
Executes multiple operations atomically. All operations succeed or all fail. Wraps TransactWrite (max 100 items or 4MB).
`ts`
transaction(configs: (TransactionParams | null)[]): Promise
Transaction types:
- { create: CreateParams } - Put item{ update: UpdateParams }
- - Update item{ erase: DeleteParams }
- - Delete item{ validate: ValidateTransactParams }
- - Condition check
Example:
`ts`
await provider.transaction([
{
update: {
table: 'Orders',
key: { orderId: 'A100' },
values: { status: 'completed' },
conditions: [{ property: 'status', operation: 'equal', value: 'pending' }]
}
},
{
erase: {
table: 'Carts',
key: { cartId: 'C100' }
}
},
{
create: {
table: 'CompletedOrders',
item: { orderId: 'A100', customerId: '12345', totalAmount: 100 }
}
},
{
validate: {
table: 'Customers',
key: { id: '12345' },
conditions: [{ operation: 'exists', property: 'id' }]
}
}
]);
createSet - Normalizes DynamoDB Set creation across v2 and v3:
`ts`
await provider.create({
table: 'Items',
item: {
id: '111',
tags: provider.createSet([1, 2, 10, 40]),
statuses: provider.createSet(['active', 'pending'])
}
});
toTransactionParams - Maps items to transaction configs:
`ts`
toTransactionParams
items: Item[],
generator: (item: Item) => (TransactionParams | null)[]
): TransactionParams[]
---
If you're not using single-table design, the provider is all you need
SingleTable provides table configuration and reduces boilerplate for single-table designs. Create one instance per table.
Requires a DynamoDbProvider instance.
#### dynamodbProvider
- Type: IDynamodbProviderDynamodbProvider
- Required: Yes
- An instance of .
#### table
- Type: string
- Required: Yes
- The DynamoDB table name.
#### partitionKey
- Type: string
- Required: Yes
- The partition key column name.
#### rangeKey
- Type: string
- Required: Yes
- The range key column name.
#### keySeparator
- Type: string#
- Default: ['USER', id]
- Separator used to join key paths. If item key is , the DynamoDB key becomes USER#id.
#### typeIndex
- Type: objectlistType
- Optional
- Index configuration for entity type identification. Required for , listAllFromType, findTableItem, and filterTableItens methods.partitionKey
- (string): Column name for the type identifier. Its value is the entity type.rangeKey
- (string): Column name for the sort key.name
- (string): Index name in DynamoDB.rangeKeyGenerator
- (function, optional): (item, type) => string | undefined - Generates range key value. Defaults to new Date().toISOString(). Return undefined to skip range key creation.
The index does not need to exist in DynamoDB if only using the type property for filtering. The index must exist for query-based methods like listType and listAllFromType.
#### expiresAt
- Type: string
- Optional
- TTL column name if configured in DynamoDB.
#### indexes
- Type: RecordpartitionKey
- Optional
- Secondary index configuration.
- Key: Index name as defined in DynamoDB.
- Value: Object with and rangeKey column names, optional numeric to indicate its range value should be a number (unlocks atomic updates for it too)
> Important! SingleTable patterns rely on string keys almost exclusively, with the exception of rank-type range keys. That's why we auto convert any valid key/index-key to string, unless marked here with the numeric: true flag. This in turn makes it a requirement to only accept numbers/single numbers arrays are possible valid values for it.
#### autoRemoveTableProperties
- Type: booleantrue
- Default:
- Removes internal properties from returned items:
- Main table partition and range keys
- Type index partition and range keys
- All secondary index partition and range keys
- TTL attribute
Items should contain all relevant properties independently without relying on key extraction. For example, if PK is USER#id, include an id property in the item.
#### keepTypeProperty
- Type: booleanfalse
- Default: typeIndex
- Retains the partition key column in returned items. Applies only when autoRemoveTableProperties is true.
#### propertyCleanup
- Type: (item: AnyObject) => AnyObjectautoRemoveTableProperties
- Optional
- Custom cleanup function for returned items. Overrides and keepTypeProperty when provided.
#### blockInternalPropUpdate
- Type: booleantrue
- Default: false
- Blocks updates to internal properties (keys, index keys, type key, TTL). Default behavior throws error if attempted. Set to to disable or use badUpdateValidation for custom validation.
#### badUpdateValidation
- Type: (propertiesInUpdate: Setvalues
- Optional
- Custom validation for update operations. Receives all properties referenced in , remove, or atomicOperations.true
- Return values:
- : Update is invalid (throws error).false
- : Update is valid.string
- : Custom error message to throw.
The partition key check always runs as it violates DynamoDB rules.
#### autoGenerators
- Type: RecordautoGen
- Optional
- Define custom value generators that can be referenced in entity configurations.
Extends or overrides the built-in auto-generation types (UUID, KSUID, timestamp, count). Custom generators defined here become available throughout all entities in the table.
Example:
`ts
const table = new SingleTable({
dynamodbProvider: provider,
table: 'YOUR_TABLE_NAME',
partitionKey: 'pk',
rangeKey: 'sk',
autoGenerators: {
// Add custom generators
tenantId: () => getTenantFromContext(),
organizationId: () => getOrgFromContext(),
// Override built-in generators
UUID: () => customUUIDImplementation(),
timestamp: () => customTimestamp(),
},
});
// Use in entity definitions
const User = table.schema.createEntity
type: 'USER',
getPartitionKey: ({ id }) => ['USER', id],
getRangeKey: () => '#DATA',
autoGen: {
onCreate: {
versionId: 'KSUID' // Uses builtin implementation
id: 'UUID', // Uses custom UUID implementation
tenantId: 'tenantId', // Uses custom tenantId generator
createdAt: 'timestamp', // Uses custom timestamp implementation
},
onUpdate: {
organizationId: 'organizationId', // Uses custom generator
updatedAt: 'timestamp', // Uses custom timestamp
},
},
});
`
`ts
import { SingleTable, DynamodbProvider } from 'dynamodb-provider'
const provider = new DynamodbProvider({
// provider params
});
const table = new SingleTable({
dynamodbProvider: provider,
table: 'YOUR_TABLE_NAME',
partitionKey: 'pk',
rangeKey: 'sk',
keySeparator: '#',
typeIndex: {
name: 'TypeIndexName',
partitionKey: '_type',
rangeKey: '_timestamp',
},
expiresAt: 'ttl',
indexes: {
SomeIndex: {
partitionKey: 'gsipk1',
rangeKey: 'gsisk1',
}
}
})
`
Available methods:
- get
- batchGet
- create
- delete
- update
- query
- transaction
- ejectTransactParams
- toTransactionParams
- createSet
- listType
- listAllFromType
- findTableItem
- filterTableItens
Retrieves a single item by partition and range keys.
`ts`
get
Parameters:
- partitionKey - Partition key value. Type: null | string | ArrayrangeKey
- - Range key value. Type: null | string | ArrayconsistentRead
- (optional) - Use strongly consistent reads. Default: falsepropertiesToRetrieve
- (optional) - Root-level attributes to return
Returns the item or undefined if not found.
Key Types:
`ts`
type KeyValue = null | string | Array
Array keys are joined with the configured keySeparator. null values are primarily for index updates where parameters may be incomplete.
Example:
`ts`
const user = await table.get({
partitionKey: ['USER', id],
rangeKey: '#DATA',
consistentRead: true,
})
Retrieves multiple items by keys. Handles batches >100 items and retries unprocessed items automatically.
`ts`
batchGet
Parameters:
- keys - Array of key objects, each containing:partitionKey
- - Partition key valuerangeKey
- - Range key valueconsistentRead
- (optional) - Use strongly consistent reads. Default: falsepropertiesToRetrieve
- (optional) - Root-level attributes to returnthrowOnUnprocessed
- (optional) - Throw error if items remain unprocessed after retries. Default: falsemaxRetries
- (optional) - Maximum retry attempts for unprocessed items. Default: 8
Example:
`ts`
const items = await table.batchGet({
keys: [
{ partitionKey: 'USER#123', rangeKey: 'INFO#456' },
{ partitionKey: 'USER#789', rangeKey: 'INFO#012' },
],
propertiesToRetrieve: ['name', 'email'],
throwOnUnprocessed: true,
});
Creates an item in the table.
`ts`
create
Parameters:
- item - The item to createkey
- - Object containing:partitionKey
- - Partition key valuerangeKey
- - Range key valueindexes
- (optional) - Index key values. Structure: Record. Only available if table has indexes configured.expiresAt
- (optional) - UNIX timestamp for TTL. Only available if table has expiresAt configured.type
- (optional) - Entity type identifier. Only available if table has typeIndex configured.
Example:
`ts`
const user = await table.create({
key: {
partitionKey: 'USER#123',
rangeKey: '#DATA',
},
item: {
id: '123',
name: 'John Doe',
email: 'john.doe@example.com',
},
type: 'USER',
expiresAt: Math.floor(Date.now() / 1000) + 60 60 24 * 30, // 30 days
});
Deletes an item by partition and range keys.
`ts`
delete
Parameters:
- partitionKey - Partition key valuerangeKey
- - Range key valueconditions
- (optional) - Conditions that must be met before deletion
Example:
`ts`
await table.delete({
partitionKey: 'USER#123',
rangeKey: '#DATA',
});
Updates an item with support for value updates, property removal, and atomic operations.
`ts`
update
Parameters:
- partitionKey - Partition key valuerangeKey
- - Range key valuevalues
- (optional) - Properties to updateremove
- (optional) - Root-level properties to removeatomicOperations
- (optional) - Atomic operations (see update for operations)atomicIndexes
- (optional) - Atomic operations on numeric index range keys. Only available for indexes configured with numeric: true.conditions
- (optional) - Conditions that must be metreturnUpdatedProperties
- (optional) - Return updated valuesindexes
- (optional) - Update index keys. Structure: Record. Only available if table has indexes configured.expiresAt
- (optional) - UNIX timestamp for TTL. Only available if table has expiresAt configured.type
- (optional) - Entity type value. Updates the typeIndex.partitionKey property. Only available if table has typeIndex configured.
Example:
`ts`
const result = await table.update({
partitionKey: ['USER', 'some-id'],
rangeKey: '#DATA',
values: {
email: 'newemail@example.com',
status: 'active'
},
remove: ['someProperty'],
atomicOperations: [{ operation: 'sum', property: 'loginCount', value: 1 }],
expiresAt: Math.floor(Date.now() / 1000) + 60 60 24 * 30,
indexes: {
SomeIndex: { partitionKey: 'NEW_PARTITION' },
},
conditions: [{ property: 'status', operation: 'equal', value: 'pending' }],
returnUpdatedProperties: true,
});
Atomic Numeric Index Updates:
Perform atomic operations on numeric index range keys without worrying about blockInternalPropUpdate restrictions or referencing internal column names.
`ts
const table = new SingleTable({
// ...config
indexes: {
LeaderboardIndex: {
partitionKey: 'lbPK',
rangeKey: 'score',
numeric: true, // Enable atomic operations
},
RankIndex: {
partitionKey: 'rankPK',
rangeKey: 'rank',
numeric: true,
},
},
});
await table.update({
partitionKey: ['PLAYER', 'player-123'],
rangeKey: '#DATA',
values: { name: 'Updated Name' },
atomicIndexes: [
{
index: 'LeaderboardIndex',
type: 'add',
value: 500,
},
{
index: 'RankIndex',
type: 'subtract',
value: 1,
if: {
operation: 'bigger_than',
value: 0,
},
},
],
});
`
Atomic Index Operation Types:
- add - Add to value, auto-initializes to 0 if missingsubtract
- - Subtract from value (fails if property doesn't exist)sum
- - Add to value (fails if property doesn't exist)
Optional if Condition:
- operation - Condition operation (bigger_than, lower_than, etc.)value
- - Comparison valueproperty
- (optional) - Different property to check (defaults to the index range key being updated)
Queries items by partition key with optional range key conditions.
`ts`
query
Parameters:
- partition - Partition key value. Type: KeyValuerange
- (optional) - Range key condition with operations: equal, lower_than, lower_or_equal_than, bigger_than, bigger_or_equal_than, begins_with, betweenindex
- (optional) - Index name to query. Only available if table has indexes configured.retrieveOrder
- (optional) - ASC or DESC. Default: ASClimit
- (optional) - Maximum items to returnfullRetrieval
- (optional) - Auto-paginate until all items retrieved. Default: falsepaginationToken
- (optional) - Continue from previous queryfilters
- (optional) - Filter expressionspropertiesToRetrieve
- (optional) - Specific attributes to return (root-level only)
Returns { items, paginationToken? }
Example:
`ts`
const { items, paginationToken } = await table.query({
partition: ['USER', 'your-id'],
range: {
value: 'LOG#',
operation: 'begins_with',
},
retrieveOrder: 'DESC',
limit: 10,
propertiesToRetrieve: ['id', 'timestamp', 'message']
});
Queries for the first item matching the criteria. Returns the item directly or undefined if no match found.
`ts`
queryOne
Parameters:
Same as query, except:limit
- No - always queries for 1 itempaginationToken
- No - returns first match onlyfullRetrieval
- No - automatically set to false
Example:
`ts
const user = await table.queryOne({
partition: ['USER', 'user@example.com']
});
if (user) {
console.log(Found user: ${user.name});`
}
Queries for all items matching the criteria. Auto-paginates through all results and returns items directly as an array.
`ts`
queryAll
Parameters:
Same as query, except:paginationToken
- No - automatically handles paginationfullRetrieval
- No - always set to true internallylimit
- (optional) - maximum total items to return (stops pagination when limit reached)
Example:
`ts
const allLogs = await table.queryAll({
partition: ['USER', 'your-id'],
range: {
value: 'LOG#',
operation: 'begins_with',
},
retrieveOrder: 'DESC',
filters: { level: 'ERROR' },
limit: 50 // Optional: max total items
});
console.log(Found ${allLogs.length} error logs);`
Executes multiple operations atomically. All operations succeed or all fail.
`ts`
transaction(configs: (SingleTableTransactionParams | null)[]): Promise
Parameters:
- configs - Array of transaction configurations (max 100 items or 4MB total):{ create: SingleTableCreateParams }
- - Put item{ update: SingleTableUpdateParams }
- - Update item{ erase: SingleTableDeleteParams }
- - Delete item{ validate: SingleTableValidateTransactParams }
- - Condition check
Transaction parameters match the corresponding single table method parameters. null values in the array are filtered out.
Example:
`ts`
await table.transaction([
{
update: {
partitionKey: 'ORDER#A100',
rangeKey: '#DATA',
values: { status: 'completed' },
conditions: [{ property: 'status', operation: 'equal', value: 'pending' }]
}
},
{
erase: {
partitionKey: 'CART#C100',
rangeKey: '#DATA'
}
},
{
create: {
key: { partitionKey: 'COMPLETED#A100', rangeKey: '#DATA' },
item: { orderId: 'A100', customerId: '12345', totalAmount: 100 }
}
},
{
validate: {
partitionKey: 'CUSTOMER#12345',
rangeKey: '#DATA',
conditions: [{ operation: 'exists', property: 'id' }]
}
}
]);
Converts single table transaction configs to provider transaction configs for merging with transactions from other tables.
`ts`
ejectTransactParams(configs: (SingleTableTransactionParams | null)[]): TransactionParams[]
Parameters:
- configs - Array of single table transaction configurations
Returns array of provider-compatible transaction configurations.
Example:
`ts
const singleTableTransacts = table.ejectTransactParams([
{ create: { key: { partitionKey: 'A', rangeKey: 'B' }, item: { name: 'test' } } }
]);
await otherProvider.transaction([
{ create: { table: 'OtherTable', item: { id: '1' } } },
...singleTableTransacts,
]);
`
Maps items to transaction configurations.
`ts`
toTransactionParams
items: Item[],
generator: (item: Item) => SingleTableTransactionParams | (SingleTableTransactionParams | null)[] | null
): SingleTableTransactionParams[]
Parameters:
- items - Array of items to processgenerator
- - Function that returns transaction config(s) for each item
Example:
`ts
const configs = table.toTransactionParams(users, (user) => ({
update: {
partitionKey: ['USER', user.id],
rangeKey: '#DATA',
values: { lastSync: new Date().toISOString() }
}
}));
await table.transaction(configs);
`
Creates a DynamoDB Set. Normalizes Set creation across SDK v2 and v3.
`ts`
createSet
Parameters:
- items - Array of strings or numbers
Example:
`ts`
await table.create({
key: { partitionKey: 'ITEM#1', rangeKey: '#DATA' },
item: {
id: '1',
tags: table.createSet(['tag1', 'tag2', 'tag3']),
counts: table.createSet([1, 2, 3])
}
});
Retrieves all items of a specified type. Requires typeIndex with an existing DynamoDB index.
`ts`
listAllFromType
Parameters:
- type - Entity type value matching the typeIndex partition key
Automatically paginates until all items are retrieved.
Example:
`ts`
const users = await table.listAllFromType('USER');
Retrieves items of a specific type with pagination support. Requires typeIndex with an existing DynamoDB index.
`ts`
listType
Parameters:
- type - Entity type value matching the typeIndex partition keyrange
- (optional) - Range key filter with operations: equal, lower_than, lower_or_equal_than, bigger_than, bigger_or_equal_than, begins_with, betweenlimit
- (optional) - Maximum items to returnpaginationToken
- (optional) - Continue from previous queryretrieveOrder
- (optional) - ASC or DESCfullRetrieval
- (optional) - Auto-paginate until all items retrieved. Default: falsefilters
- (optional) - Filter expressions
Returns { items, paginationToken? }
Example:
`ts`
const { items, paginationToken } = await table.listType({
type: 'USER',
range: {
operation: 'begins_with',
value: 'john',
},
limit: 10,
});
Finds the first item matching a type. Requires typeIndex configured.
`ts`
findTableItem
Parameters:
- items - Array of items to searchtype
- - Entity type value
Returns first matching item or undefined.
Example:
`ts`
const items = await table.query({ partition: ['USER', id] });
const userData = table.findTableItem
Filters items by type. Requires typeIndex configured.
`ts`
filterTableItens
Parameters:
- items - Array of items to filtertype
- - Entity type value
Returns array of matching items.
Example:
`ts`
const items = await table.query({ partition: ['USER', id] });
const logs = table.filterTableItens
The schema system provides entity definitions, partition management, and collection joins. Access via table.schema.
Entities represent data types within the table.
Syntax:
`ts
type tUser = {
id: string;
name: string;
createdAt: string;
}
const User = table.schema.createEntity
// entity parameters
})
`
The two-step invocation (createEntity) enables proper type inference. Entity types rely on TypeScript types without runtime schema validation.
Entity Parameters:
- type (string, required) - Unique identifier for the entity type within the table. Throws error if duplicate types are registered.
- getPartitionKey (function or array, required) - Generates partition key from entity properties.
- Function: (params: PartialKeyValue = null | string | Array
- Array: Supports dot notation for property references (see below)
- Type:
- Parameters restricted to entity properties only
- getRangeKey (function or array, required) - Generates range key from entity properties. Same structure as getPartitionKey.
Key Resolver Example:
`ts
type User = {
id: string;
name: string;
createdAt: string;
}
const User = table.schema.createEntity
type: 'USER',
getPartitionKey: ({ id }: Pick
getRangeKey: () => '#DATA',
})
`
Parameters must exist in the entity type. Using non-existent properties causes type errors.
Dot Notation Shorthand:
Key resolvers can use array syntax with dot notation for property references:
`ts
type Event = {
id: string;
timestamp: string;
userId: string;
}
const Event = table.schema.createEntity
type: 'USER_EVENT',
getPartitionKey: ['USER_EVENT'],
getRangeKey: ['.id'], // References Event.id
indexes: {
byUser: {
getPartitionKey: ['EVENT_BY_USER', '.userId'],
getRangeKey: ['.timestamp'],
index: 'IndexOne',
},
},
});
`
Dot Notation Behavior:
- Strings starting with . reference entity properties.
- Leading is removed before property lookup ('.id' becomes id)undefined
- IDE provides autocomplete for property names
- Typos cause values in keys.
- Constants without remain unchanged ('USER_EVENT' stays 'USER_EVENT')
Use functions for complex key generation logic. Dot notation handles simple property references only.
- autoGen (object, optional) - Auto-generate property values on create or update.
`ts`
type AutoGenParams
onCreate?: { [Key in keyof Entity]?: AutoGenOption };
onUpdate?: { [Key in keyof Entity]?: AutoGenOption };
};
Built-in Generator Types:
- 'UUID' - Generates v4 UUID'KSUID'
- - Generates K-Sortable Unique ID'count'
- - Assigns 0'timestamp'
- - Generates ISO timestamp via new Date().toISOString()() => any
- - Inline custom generator functionautoGenerators
- Custom generator keys from table's config (see autoGenerators)
Example:
`ts
// With custom table generators
const table = new SingleTable({
// ...config
autoGenerators: {
tenantId: () => getCurrentTenant(),
}
});
const Entity = table.schema.createEntity
type: 'ENTITY',
getPartitionKey: ({ id }) => ['ENTITY', id],
getRangeKey: () => '#DATA',
autoGen: {
onCreate: {
id: 'UUID', // Built-in generator
tenantId: 'tenantId', // Custom generator from table config
createdAt: 'timestamp', // Built-in generator
status: () => 'active', // Inline function
},
onUpdate: {
updatedAt: 'timestamp',
},
},
});
`
Properties with autoGen configured become optional in creation parameters. User-provided values always override generated ones.
- rangeQueries (object, optional) - Predefined range key queries for the entity.
- Key: Query method name
- Value: Query configuration with operation and getValues
`ts
type Log = {
timestamp: string;
}
const Logs = table.schema.createEntity
type: 'APP_LOGS',
getPartitionKey: () => ['APP_LOG'],
getRangeKey: ({ timestamp }: Pick
rangeQueries: {
dateSlice: {
operation: 'between',
getValues: ({ start, end }: { start: string, end: string }) => ({ start, end })
}
}
})
`
Generates typed query methods accessible via table.schema.from(Logs).query.dateSlice({ start, end }).
- indexes (object, optional) - Secondary index definitions. Only available if table has indexes configured.getPartitionKey
- Key: Custom index identifier
- Value: Index configuration
- - Partition key resolver (function or array)getRangeKey
- - Range key resolver (function or array)index
- - Table index name matching indexes configurationrangeQueries
- (optional) - Predefined queries for this index
`ts
type Log = {
type: string;
timestamp: string;
}
const Logs = table.schema.createEntity
type: 'APP_LOGS',
getPartitionKey: () => ['APP_LOG'],
getRangeKey: ({ timestamp }: Pick
indexes: {
byType: {
getPartitionKey: ({ type }: Pick
getRangeKey: ({ timestamp }: Pick
index: 'DynamoIndex1'
}
}
})
`
- extend (function, optional) - Adds or modifies properties on retrieved items.
- Signature: (item: Entity) => AnyObjectfrom()
- Applied automatically to all retrieval operations via
`ts
type User = {
id: string;
name: string;
dob: string;
}
const User = table.schema.createEntity
type: 'USER',
getPartitionKey: ({ id }: Pick
getRangeKey: () => '#DATA',
extend: ({ dob }) => ({ age: calculateAge(dob) })
})
`
Retrieved items include the extended properties automatically.
- includeTypeOnEveryUpdate (boolean, optional) - Automatically includes the entity's type value on every update operation. Only available if table has typeIndex configured. Default: false.
This is useful when using update operations as upserts or when you need to ensure the type is always set on items.
`ts
const User = table.schema.createEntity
type: 'USER',
getPartitionKey: ({ id }: Pick
getRangeKey: () => '#DATA',
})
// Pass explicitly on each update call
await table.schema.from(User).update({
id: 'user-123',
values: { name: 'John' },
includeTypeOnEveryUpdate: true, // Type will be included
});
// Or use getUpdateParams to generate params with type
const params = User.getUpdateParams({
id: 'user-123',
values: { name: 'John' },
includeTypeOnEveryUpdate: true,
});
// params.type === 'USER'
`
Note: This only populates the typeIndex.partitionKey column. The typeIndex.rangeKey is not affected.
Entities expose helper methods and integration with schema.from().
Entity Helper Methods:
- getKey(params) - Generates key reference from parameters required by getPartitionKey and getRangeKey. Returns { partitionKey: KeyValue, rangeKey: KeyValue }.
- getCreationParams(item, options?) - Generates parameters for single table create. Optional expiresAt parameter available if table has TTL configured.
- getUpdateParams(params) - Generates parameters for single table update. Requires key parameters plus update operations (values, atomicOperations, etc.).
- Transaction Builders:
- transactCreateParams - Returns { create: {...} }transactUpdateParams
- - Returns { update: {...} }transactDeleteParams
- - Returns { erase: {...} }transactValidateParams
- - Returns { validate: {...} }
Using schema.from():
Creates a repository interface for entity operations:
`ts
type User = {
id: string;
name: string;
createdAt: string;
}
const User = table.schema.createEntity
type: 'USER',
getPartitionKey: ({ id }: Pick
getRangeKey: () => ['#DATA'],
})
const userRepo = table.schema.from(User)
await userRepo.create({
id: 'user-id',
name: 'John',
createdAt: new Date().toISOString()
})
await userRepo.update({
id: 'user-id',
values: { name: 'Jane' }
})
`
Available Methods:
- getbatchGet
- create
- update
- delete
- listAll
- - Requires typeIndexlist
- - Requires typeIndexquery
- queryIndex
- - Requires entity indexes definition
Query Methods:
query and queryIndex expose custom method plus any defined rangeQueries. Each range query method supports three invocation styles:
- Default call - Returns QueryResult with pagination support.one()
- - Returns first matching item or undefined.all()
- - Returns all matching items as array
`ts
type Log = {
type: string;
timestamp: string;
}
const Logs = table.schema.createEntity
type: 'APP_LOGS',
getPartitionKey: () => ['APP_LOG'],
getRangeKey: ({ timestamp }: Pick
rangeQueries: {
recent: {
operation: 'bigger_than',
getValues: ({ since }: { since: string }) => ({ value: since })
}
},
indexes: {
byType: {
getPartitionKey: ({ type }: Pick
getRangeKey: ({ timestamp }: Pick
index: 'DynamoIndex1',
rangeQueries: {
dateSlice: {
operation: 'between',
getValues: ({ start, end }: { start: string, end: string }) => ({ start, end })
}
}
}
}
})
// Query main partition
const { items, paginationToken } = await table.schema.from(Logs).query.custom()
const { items: page } = await table.schema.from(Logs).query.custom({ limit: 10, retrieveOrder: 'DESC' })
// Get first matching log
const firstLog = await table.schema.from(Logs).query.one()
// Returns: Log | undefined
// Get all logs (auto-paginated)
const allLogs = await table.schema.from(Logs).query.all()
// Returns: Log[]
const recentLog = await table.schema.from(Logs).query.recent({ since: '2025-01-01' })
// Range queries with one/all
const recentLog = await table.schema.from(Logs).query.recent.one({ since: '2024-01-01' })
// Returns: Log | undefined
const allRecent = await table.schema.from(Logs).query.recent.all({ since: '2024-01-01' })
// Returns: Log[]
// Query index with one/all
const errorLog = await table.schema.from(Logs).queryIndex.byType.one({ type: 'ERROR' })
// Returns: Log | undefined
const allErrors = await table.schema.from(Logs).queryIndex.byType.all({ type: 'ERROR' })
// Returns: Log[]
// Index range queries with one/all
const firstError = await table.schema.from(Logs).queryIndex.byType.dateSlice.one({
type: 'ERROR',
start: '2024-01-01',
end: '2024-01-31'
})
const allJanErrors = await table.schema.from(Logs).queryIndex.byType.dateSlice.all({
type: 'ERROR',
start: '2024-01-01',
end: '2024-01-31',
limit: 100 // Optional: max total items to return
})
`
Query Method Parameters:
- Default call - Accepts limit, paginationToken, retrieveOrder, filters, propertiesToRetrieve, range.one(params?)
- - Accepts all params except limit and paginationToken.all(params?)
- - Accepts all params except paginationToken (limit sets max total items)
All retrieval methods apply extend function if defined.
Partitions group entities sharing the same partition key. Centralizes key generation for related entities.
Parameters:
- name (string, required) - Unique partition identifier. Throws error if duplicated.
- getPartitionKey (function, required) - Partition key generator. Function form only (dot notation not supported).
- index (string, optional) - Table index name. Creates index partition when specified.
- entries (object, required) - Range key generators mapped by name. Each entry can be used once to create an entity or index definition.
Example:
`ts`
const userPartition = table.schema.createPartition({
name: 'USER_PARTITION',
getPartitionKey: ({ userId }: { userId: string }) => ['USER', userId],
entries: {
mainData: () => ['#DATA'],
permissions: ({ permissionId }: { permissionId: string }) => ['PERMISSION', permissionId],
loginAttempt: ({ timestamp }: { timestamp: string }) => ['LOGIN_ATTEMPT', timestamp],
orders: ({ orderId }: { orderId: string }) => ['ORDER', orderId],
},
})
Use descriptive parameter names (userId instead of id) for clarity.
Creating Entities from Partitions:
Use partition.use(entry).create for main table or .index() for index partitions:
`ts
type User = {
id: string;
name: string;
createdAt: string;
email: string;
}
type UserLoginAttempt = {
userId: string;
timestamp: string;
success: boolean;
ip: string;
}
const User = userPartition.use('mainData').create
type: 'USER',
paramMatch: {
userId: 'id' // Maps partition param 'userId' to entity property 'id'
},
// Other entity parameters...
})
const UserLoginAttempt = userPartition.use('loginAttempt').create
type: 'USER_LOGIN_ATTEMPT',
// No paramMatch needed - all partition params exist in entity type
})
`
Parameter Matching:
- paramMatch - Maps partition parameters to entity properties when names differ. Required when partition parameters are not present in entity type. Optional when all parameters exist in entity.
Each partition entry can be used only once.
Index Partitions:
Partitions can target secondary indexes by specifying the index parameter:
`ts
type User = {
id: string;
name: string;
}
const userIndexPartition = table.schema.createPartition({
name: 'USER_INDEX_PARTITION',
index: 'DynamoIndex1', // Must match table indexes configuration
getPartitionKey: ({ userId }: { userId: string }) => ['USER', userId],
entries: {
mainData: () => ['#DATA'],
loginAttempt: ({ timestamp }: { timestamp: string }) => ['LOGIN_ATTEMPT', timestamp],
},
})
// Use index partition in entity definition
const User = table.schema.createEntity
type: 'USER',
getPartitionKey: () => ['APP_USERS'],
getRangeKey: ({ id }: Pick
indexes: {
userData: userIndexPartition.use('mainData').create
paramMatch: { userId: 'id' },
// rangeQueries and other index parameters
}),
}
})
`
Index partitions return .index() method instead of .entity() when used.
Another great usage of partition is to facilitate your collections creation. Let's explore what a collection is:
Collections define joined entity structures for retrieval. Data in single-table designs often spans multiple entries that need to be retrieved and joined together.
- ref (entity or null, required) - Root entity of the collection. Use null for collections with only joined entities.
- type ('SINGLE' or 'MULTIPLE', required) - Collection cardinality. 'SINGLE' returns one result, 'MULTIPLE' returns an array.
- getPartitionKey (function, optional) - Partition key generator for the collection. Mutually exclusive with partition.
- index (string, optional) - Table index name. Only valid with getPartitionKey.
- partition (Partition, optional) - Existing partition (entity or index partition). Mutually exclusive with getPartitionKey and index. The collection infers index usage automatically.
- narrowBy (optional) - Range key filter for collection query:
- 'RANGE_KEY' - Uses ref entity's range key as query prefix. Requires ref to be an entity.(params?: AnyObject) => RangeQueryConfig
- - Custom range query function.
- join (object, required) - Entity join configuration. Structure: Record. Each key becomes a property in the result type.
Join Configuration:
`ts`
type JoinConfig = {
entity: RefEntity;
type: 'SINGLE' | 'MULTIPLE';
extractor?: (item: AnyObject) => any;
sorter?: (a: any, b: any) => number;
joinBy?: 'POSITION' | 'TYPE' | ((parent: any, child: any) => boolean);
join?: Record
}
Join Parameters:
- entity (required) - Entity to join.
- type (required) - 'SINGLE' for single item, 'MULTIPLE' for array.
- extractor (optional) - Transforms joined entity before inclusion. Signature: (item) => any.
- sorter (optional) - Sorts 'MULTIPLE' type joins. Ignored for 'SINGLE'. Signature: (a, b) => number.
- joinBy (optional) - Join strategy. Default: 'POSITION'.'POSITION'
- - Sequential join based on query order. Requires table typeIndex with existing DynamoDB index.'TYPE'
- - Join by entity type property. Requires typeIndex.partitionKey defined (index need not exist in DynamoDB).(parent, child) => boolean
- - Custom join function. Returns true to join.
- join (optional) - Nested join configuration. Same structure as root join. Enables multi-level joins.
Returns a collection object for type extraction and query execution.
Use GetCollectionType to infer the collection's TypeScript type:
`ts`
type YourType = GetCollectionType
Collection with Root Entity:
`ts
const userPartition = table.schema.createPartition({
name: 'USER_PARTITION',
getPartitionKey: ({ userId }: { userId: string }) => ['USER', userId],
entries: {
mainData: () => ['#DATA'],
loginAttempt: ({ timestamp }: { timestamp: string }) => ['LOGIN_ATTEMPT', timestamp],
},
})
type User = {
id: string;
name: string;
email: string;
}
type UserLoginAttempt = {
userId: string;
timestamp: string;
success: boolean;
ip: string;
}
const User = userPartition.use('mainData').create
type: 'USER',
paramMatch: { userId: 'id' },
})
const UserLoginAttempt = userPartition.use('loginAttempt').create
type: 'USER_LOGIN_ATTEMPT',
})
// This will correctly need the userId param to retrieve when doing schema.from(userWithLogins).get
const userWithLogins = userPartition.collection({
ref: User,
type: 'SINGLE',
join: {
logins: {
entity: UserLoginAttempt,
type: 'MULTIPLE',
joinBy: 'TYPE',
},
},
});
// Type: User & { logins: UserLoginAttempt[] }
type UserWithLogins = GetCollectionType
`
Collection without Root Entity:
`ts
type UserPermission = {
permissionId: string;
timestamp: string;
addedBy: string;
}
const UserPermission = userPartition.use('permissions').create
type: 'USER_PERMISSION',
})
const userDataCollection = userPartition.collection({
ref: null,
type: 'SINGLE',
join: {
logins: {
entity: UserLoginAttempt,
type: 'MULTIPLE',
joinBy: 'TYPE',
},
permissions: {
entity: UserPermission,
type: 'MULTIPLE',
joinBy: 'TYPE',
extractor: ({ permissionId }: UserPermission) => permissionId,
}
},
});
// Type: { logins: UserLoginAttempt[], permissions: string[] }
type UserData = GetCollectionType
`
You can also call table.schema.createCollection if you need to pass in partitions/partition-getters inline
Collections expose a get method via schema.from():
`ts`
const result = await table.schema.from(userWithLogins).get({
userId: 'user-id-12',
})
Returns the collection type for 'SINGLE' collections or undefined if not found. Returns array for 'MULTIPLE'` collections.