dynamodb-provider
v3.1.3
Published
A dynamodb Provider that simplifies the native api. It packs with a Single Table adaptor/pseudo ORM for single table design
Readme
Dynamo DB Provider
Fast Develop for DynamoDB with this type-safe & single-table awareness library!
Min Node version: 16
New documentation website here
Introduction
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.
Architecture
The library has three parts:
- DynamoDB Provider - Type-safe wrappers around DynamoDB operations (get, update, query, transaction, etc.). Use this for table-per-entity designs.
- SingleTable - Table configuration layer that removes repetition when all operations target the same table with fixed keys and indexes.
- 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.
1. DynamoDB Provider
The provider wraps AWS SDK clients (v2 or v3) with type-safe methods. Only DocumentClient instances are supported.
Using v2
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...
}),
},
});Using v3
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.
Configuration
interface DynamoDbProviderParams {
dynamoDB: DynamoDBConfig;
logCallParams?: boolean;
}logCallParams - Logs the parameters sent to DynamoDB before each operation. Useful for debugging.
Provider Methods
Here you'll find each method exposed on the provider.
Quick Access
get
Retrieves a single item by primary key.
get<Entity = AnyObject, PKs extends StringKey<Entity> | unknown = unknown>(
params: GetItemParams<Entity, PKs>,
): Promise<Entity | undefined>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:
interface User {
userId: string;
name: string;
email: string;
age: number;
}
const user = await provider.get<User>({
table: 'UsersTable',
key: { userId: '12345' },
consistentRead: true,
propertiesToRetrieve: ['name', 'email'],
});create
Creates an item in the table. DynamoDB's PutItem overwrites existing items—use conditions to prevent this.
create<Entity>(params: CreateParams<Entity>): Promise<Entity>Parameters:
table- Table nameitem- Item to create (must include primary key)conditions- Optional conditions that must be met before creating
Example:
const user = await provider.create({
table: 'Users',
item: {
userId: '12345',
name: 'John Doe',
email: '[email protected]',
age: 30,
},
conditions: [
{ operation: 'not_exists', property: 'userId' }
],
});For composite keys, as dynamodb doc, check both:
conditions: [
{ operation: 'not_exists', property: 'partitionKey' },
{ operation: 'not_exists', property: 'sortKey' }
]Condition Operations:
equalnot_equallower_thanlower_or_equal_thanbigger_thanbigger_or_equal_thanbegins_withcontainsnot_containsbetweeninnot_inexistsnot_exists
Condition Structure:
{
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:
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))delete
Deletes an item by primary key.
delete<Entity>(params: DeleteParams<Entity>): Promise<void>Parameters:
table- Table namekey- Primary key of the item to deleteconditions- Optional conditions that must be met before deleting
Example:
await provider.delete({
table: 'Users',
key: { id: '12345' },
conditions: [
{ operation: 'exists', property: 'id' }
]
});update
Updates an item with support for value updates, property removal, and atomic operations.
update<Entity>(params: UpdateParams<Entity>): Promise<Partial<Entity> | undefined>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:
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:
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 exist- Optional
refProperty- Check different property for existence
- Optional
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:
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' }
});batchGet
Retrieves multiple items by primary keys. Automatically handles batches >100 items and retries unprocessed items.
batchGet<Entity>(options: BatchListItemsArgs<Entity>): Promise<Entity[]>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:
const products = await provider.batchGet({
table: 'Products',
keys: [
{ productId: '123' },
{ productId: '456' }
],
consistentRead: true,
propertiesToRetrieve: ['name', 'price'],
});list
Scans a table with optional filters, limits, and pagination.
list<Entity>(table: string, options?: ListOptions<Entity>): Promise<ListTableResult<Entity>>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:
const result = await provider.list('Products', {
filters: {
category: 'electronics',
price: { operation: 'bigger_than', value: 100 }
},
limit: 100
});listAll
Scans entire table, automatically handling pagination. Same options as list except no limit or paginationToken.
listAll<Entity>(table: string, options?: ListAllOptions<Entity>): Promise<Entity[]>Example:
const products = await provider.listAll('Products', {
filters: { category: 'electronics' },
propertiesToRetrieve: ['productId', 'name', 'price']
});query
Queries items by partition key with optional range key conditions.
query<Entity>(params: QueryParams<Entity>): Promise<QueryResult<Entity>>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-ASCorDESC(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:
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']
});queryOne
Queries for the first item matching the criteria. Returns the item directly or undefined if no match found.
queryOne<Entity>(params: QueryOneParams<Entity>): Promise<Entity | undefined>Parameters:
Same as query, except:
- No
limit- always queries for 1 item - No
paginationToken- returns first match only - No
fullRetrieval- automatically set to false
Example:
const user = await provider.queryOne({
table: 'Users',
partitionKey: { name: 'email', value: '[email protected]' }
});
if (user) {
console.log(`Found user: ${user.name}`);
}queryAll
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.
queryAll<Entity>(params: QueryAllParams<Entity>): Promise<Entity[]>Parameters:
Same as query, except:
- No
paginationToken- automatically handles pagination - No
fullRetrieval- always set to true internally limit(optional) - maximum total items to return (stops pagination when limit reached)
Example:
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`);transaction
Executes multiple operations atomically. All operations succeed or all fail. Wraps TransactWrite (max 100 items or 4MB).
transaction(configs: (TransactionParams | null)[]): Promise<void>Transaction types:
{ create: CreateParams }- Put item{ update: UpdateParams }- Update item{ erase: DeleteParams }- Delete item{ validate: ValidateTransactParams }- Condition check
Example:
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' }]
}
}
]);Helper Methods
createSet - Normalizes DynamoDB Set creation across v2 and v3:
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:
toTransactionParams<Item>(
items: Item[],
generator: (item: Item) => (TransactionParams | null)[]
): TransactionParams[]If you're not using single-table design, the provider is all you need
Single table
SingleTable provides table configuration and reduces boilerplate for single-table designs. Create one instance per table.
Requires a DynamoDbProvider instance.
Configuration Parameters
dynamodbProvider
- Type:
IDynamodbProvider - Required: Yes
- An instance of
DynamodbProvider.
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:
# - Separator used to join key paths. If item key is
['USER', id], the DynamoDB key becomesUSER#id.
typeIndex
- Type:
object - Optional
- Index configuration for entity type identification. Required for
listType,listAllFromType,findTableItem, andfilterTableItensmethods.partitionKey(string): Column name for the type identifier. Its value is the entitytype.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 tonew Date().toISOString(). Returnundefinedto 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:
Record<string, { partitionKey: string; rangeKey: string; numeric?: boolean; }> - Optional
- Secondary index configuration.
- Key: Index name as defined in DynamoDB.
- Value: Object with
partitionKeyandrangeKeycolumn names, optionalnumericto 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: trueflag. This in turn makes it a requirement to only accept numbers/single numbers arrays are possible valid values for it.
autoRemoveTableProperties
- Type:
boolean - Default:
true - 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:
boolean - Default:
false - Retains the
typeIndexpartition key column in returned items. Applies only whenautoRemoveTablePropertiesis true.
propertyCleanup
- Type:
(item: AnyObject) => AnyObject - Optional
- Custom cleanup function for returned items. Overrides
autoRemoveTablePropertiesandkeepTypePropertywhen provided.
blockInternalPropUpdate
- Type:
boolean - Default:
true - Blocks updates to internal properties (keys, index keys, type key, TTL). Default behavior throws error if attempted. Set to
falseto disable or usebadUpdateValidationfor custom validation.
badUpdateValidation
- Type:
(propertiesInUpdate: Set<string>) => boolean | string - Optional
- Custom validation for update operations. Receives all properties referenced in
values,remove, oratomicOperations. - Return values:
true: 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:
Record<string, () => any> - Optional
- Define custom value generators that can be referenced in entity
autoGenconfigurations.
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:
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<UserType>().as({
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
},
},
});Usage
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',
}
}
})Single Table Methods
Available methods:
- get
- batchGet
- create
- delete
- update
- query
- transaction
- ejectTransactParams
- toTransactionParams
- createSet
- listType
- listAllFromType
- findTableItem
- filterTableItens
single table get
Retrieves a single item by partition and range keys.
get<Entity>(params: SingleTableGetParams<Entity>): Promise<Entity | undefined>Parameters:
partitionKey- Partition key value. Type:null | string | Array<string | number | null>rangeKey- Range key value. Type:null | string | Array<string | number | null>consistentRead(optional) - Use strongly consistent reads. Default:falsepropertiesToRetrieve(optional) - Root-level attributes to return
Returns the item or undefined if not found.
Key Types:
type KeyValue = null | string | Array<string | number | null>;Array keys are joined with the configured keySeparator. null values are primarily for index updates where parameters may be incomplete.
Example:
const user = await table.get({
partitionKey: ['USER', id],
rangeKey: '#DATA',
consistentRead: true,
})single table batch get
Retrieves multiple items by keys. Handles batches >100 items and retries unprocessed items automatically.
batchGet<Entity>(params: SingleTableBatchGetParams<Entity>): Promise<Entity[]>Parameters:
keys- Array of key objects, each containing:partitionKey- Partition key valuerangeKey- Range key value
consistentRead(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:
const items = await table.batchGet({
keys: [
{ partitionKey: 'USER#123', rangeKey: 'INFO#456' },
{ partitionKey: 'USER#789', rangeKey: 'INFO#012' },
],
propertiesToRetrieve: ['name', 'email'],
throwOnUnprocessed: true,
});single table create
Creates an item in the table.
create<Entity>(params: SingleTableCreateParams<Entity>): Promise<Entity>Parameters:
item- The item to createkey- Object containing:partitionKey- Partition key valuerangeKey- Range key value
indexes(optional) - Index key values. Structure:Record<IndexName, { partitionKey, rangeKey }>. Only available if table hasindexesconfigured.expiresAt(optional) - UNIX timestamp for TTL. Only available if table hasexpiresAtconfigured.type(optional) - Entity type identifier. Only available if table hastypeIndexconfigured.
Example:
const user = await table.create({
key: {
partitionKey: 'USER#123',
rangeKey: '#DATA',
},
item: {
id: '123',
name: 'John Doe',
email: '[email protected]',
},
type: 'USER',
expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days
});single table delete
Deletes an item by partition and range keys.
delete<Entity>(params: SingleTableDeleteParams<Entity>): Promise<void>Parameters:
partitionKey- Partition key valuerangeKey- Range key valueconditions(optional) - Conditions that must be met before deletion
Example:
await table.delete({
partitionKey: 'USER#123',
rangeKey: '#DATA',
});single table update
Updates an item with support for value updates, property removal, and atomic operations.
update<Entity>(params: SingleTableUpdateParams<Entity>): Promise<Partial<Entity> | undefined>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 withnumeric: true.conditions(optional) - Conditions that must be metreturnUpdatedProperties(optional) - Return updated valuesindexes(optional) - Update index keys. Structure:Record<IndexName, Partial<{ partitionKey, rangeKey }>>. Only available if table hasindexesconfigured.expiresAt(optional) - UNIX timestamp for TTL. Only available if table hasexpiresAtconfigured.type(optional) - Entity type value. Updates thetypeIndex.partitionKeyproperty. Only available if table hastypeIndexconfigured.
Example:
const result = await table.update({
partitionKey: ['USER', 'some-id'],
rangeKey: '#DATA',
values: {
email: '[email protected]',
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.
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)
single table query
Queries items by partition key with optional range key conditions.
query<Entity>(params: SingleTableQueryParams<Entity>): Promise<QueryResult<Entity>>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 hasindexesconfigured.retrieveOrder(optional) -ASCorDESC. 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:
const { items, paginationToken } = await table.query({
partition: ['USER', 'your-id'],
range: {
value: 'LOG#',
operation: 'begins_with',
},
retrieveOrder: 'DESC',
limit: 10,
propertiesToRetrieve: ['id', 'timestamp', 'message']
});single table queryOne
Queries for the first item matching the criteria. Returns the item directly or undefined if no match found.
queryOne<Entity>(params: SingleTableQueryOneParams<Entity>): Promise<Entity | undefined>Parameters:
Same as query, except:
- No
limit- always queries for 1 item - No
paginationToken- returns first match only - No
fullRetrieval- automatically set to false
Example:
const user = await table.queryOne({
partition: ['USER', '[email protected]']
});
if (user) {
console.log(`Found user: ${user.name}`);
}single table queryAll
Queries for all items matching the criteria. Auto-paginates through all results and returns items directly as an array.
queryAll<Entity>(params: SingleTableQueryAllParams<Entity>): Promise<Entity[]>Parameters:
Same as query, except:
- No
paginationToken- automatically handles pagination - No
fullRetrieval- always set to true internally limit(optional) - maximum total items to return (stops pagination when limit reached)
Example:
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`);single table transaction
Executes multiple operations atomically. All operations succeed or all fail.
transaction(configs: (SingleTableTransactionParams | null)[]): Promise<void>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:
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' }]
}
}
]);single table eject transact params
Converts single table transaction configs to provider transaction configs for merging with transactions from other tables.
ejectTransactParams(configs: (SingleTableTransactionParams | null)[]): TransactionParams[]Parameters:
configs- Array of single table transaction configurations
Returns array of provider-compatible transaction configurations.
Example:
const singleTableTransacts = table.ejectTransactParams([
{ create: { key: { partitionKey: 'A', rangeKey: 'B' }, item: { name: 'test' } } }
]);
await otherProvider.transaction([
{ create: { table: 'OtherTable', item: { id: '1' } } },
...singleTableTransacts,
]);single table generate transaction config list
Maps items to transaction configurations.
toTransactionParams<Item>(
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:
const configs = table.toTransactionParams(users, (user) => ({
update: {
partitionKey: ['USER', user.id],
rangeKey: '#DATA',
values: { lastSync: new Date().toISOString() }
}
}));
await table.transaction(configs);single table create set
Creates a DynamoDB Set. Normalizes Set creation across SDK v2 and v3.
createSet<T>(items: T[]): DBSet<T[number]>Parameters:
items- Array of strings or numbers
Example:
await table.create({
key: { partitionKey: 'ITEM#1', rangeKey: '#DATA' },
item: {
id: '1',
tags: table.createSet(['tag1', 'tag2', 'tag3']),
counts: table.createSet([1, 2, 3])
}
});single table list all from type
Retrieves all items of a specified type. Requires typeIndex with an existing DynamoDB index.
listAllFromType<Entity>(type: string): Promise<Entity[]>Parameters:
type- Entity type value matching thetypeIndexpartition key
Automatically paginates until all items are retrieved.
Example:
const users = await table.listAllFromType('USER');single table list type
Retrieves items of a specific type with pagination support. Requires typeIndex with an existing DynamoDB index.
listType<Entity>(params: ListItemTypeParams): Promise<ListItemTypeResult<Entity>>Parameters:
type- Entity type value matching thetypeIndexpartition 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) -ASCorDESCfullRetrieval(optional) - Auto-paginate until all items retrieved. Default:falsefilters(optional) - Filter expressions
Returns { items, paginationToken? }
Example:
const { items, paginationToken } = await table.listType({
type: 'USER',
range: {
operation: 'begins_with',
value: 'john',
},
limit: 10,
});single table find table item
Finds the first item matching a type. Requires typeIndex configured.
findTableItem<Entity>(items: AnyObject[], type: string): Entity | undefinedParameters:
items- Array of items to searchtype- Entity type value
Returns first matching item or undefined.
Example:
const items = await table.query({ partition: ['USER', id] });
const userData = table.findTableItem<User>(items.items, 'USER');single table filter table itens
Filters items by type. Requires typeIndex configured.
filterTableItens<Entity>(items: AnyObject[], type: string): Entity[]Parameters:
items- Array of items to filtertype- Entity type value
Returns array of matching items.
Example:
const items = await table.query({ partition: ['USER', id] });
const logs = table.filterTableItens<Log>(items.items, 'USER_LOG');Single Table Schema
The schema system provides entity definitions, partition management, and collection joins. Access via table.schema.
Single Table Schema: Entity
Entities represent data types within the table.
Syntax:
type tUser = {
id: string;
name: string;
createdAt: string;
}
const User = table.schema.createEntity<tUser>().as({
// entity parameters
})The two-step invocation (createEntity<Type>().as()) 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: Partial<Entity>) => KeyValue - Array: Supports dot notation for property references (see below)
- Type:
KeyValue = null | string | Array<string | number | null> - Parameters restricted to entity properties only
- Function:
getRangeKey(function or array, required) - Generates range key from entity properties. Same structure asgetPartitionKey.
Key Resolver Example:
type User = {
id: string;
name: string;
createdAt: string;
}
const User = table.schema.createEntity<User>().as({
type: 'USER',
getPartitionKey: ({ id }: Pick<User, 'id'>) => ['USER', id],
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:
type Event = {
id: string;
timestamp: string;
userId: string;
}
const Event = table.schema.createEntity<Event>().as({
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'becomesid) - IDE provides autocomplete for property names
- Typos cause
undefinedvalues 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.type AutoGenParams<Entity> = { 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'- Assigns0'timestamp'- Generates ISO timestamp vianew Date().toISOString()() => any- Inline custom generator function- Custom generator keys from table's
autoGeneratorsconfig (see autoGenerators)
Example:
// With custom table generators const table = new SingleTable({ // ...config autoGenerators: { tenantId: () => getCurrentTenant(), } }); const Entity = table.schema.createEntity<EntityType>().as({ 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
autoGenconfigured 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
operationandgetValues
type Log = { timestamp: string; } const Logs = table.schema.createEntity<Log>().as({ type: 'APP_LOGS', getPartitionKey: () => ['APP_LOG'], getRangeKey: ({ timestamp }: Pick<Log, 'timestamp'>) => timestamp, 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 hasindexesconfigured.- Key: Custom index identifier
- Value: Index configuration
getPartitionKey- Partition key resolver (function or array)getRangeKey- Range key resolver (function or array)index- Table index name matchingindexesconfigurationrangeQueries(optional) - Predefined queries for this index
type Log = { type: string; timestamp: string; } const Logs = table.schema.createEntity<Log>().as({ type: 'APP_LOGS', getPartitionKey: () => ['APP_LOG'], getRangeKey: ({ timestamp }: Pick<Log, 'timestamp'>) => timestamp, indexes: { byType: { getPartitionKey: ({ type }: Pick<Log, 'type'>) => ['APP_LOG_BY_TYPE', type], getRangeKey: ({ timestamp }: Pick<Log, 'timestamp'>) => timestamp, index: 'DynamoIndex1' } } })extend(function, optional) - Adds or modifies properties on retrieved items.- Signature:
(item: Entity) => AnyObject - Applied automatically to all retrieval operations via
from()
type User = { id: string; name: string; dob: string; } const User = table.schema.createEntity<User>().as({ type: 'USER', getPartitionKey: ({ id }: Pick<User, 'id'>) => ['USER', id], getRangeKey: () => '#DATA', extend: ({ dob }) => ({ age: calculateAge(dob) }) })Retrieved items include the extended properties automatically.
- Signature:
includeTypeOnEveryUpdate(boolean, optional) - Automatically includes the entity'stypevalue on every update operation. Only available if table hastypeIndexconfigured. Default:false.This is useful when using update operations as upserts or when you need to ensure the type is always set on items.
const User = table.schema.createEntity<User>().as({ type: 'USER', getPartitionKey: ({ id }: Pick<User, 'id'>) => ['USER', id], 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.partitionKeycolumn. ThetypeIndex.rangeKeyis not affected.
Using Entities
Entities expose helper methods and integration with schema.from().
Entity Helper Methods:
getKey(params)- Generates key reference from parameters required bygetPartitionKeyandgetRangeKey. Returns{ partitionKey: KeyValue, rangeKey: KeyValue }.getCreationParams(item, options?)- Generates parameters for single table create. OptionalexpiresAtparameter 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:
type User = {
id: string;
name: string;
createdAt: string;
}
const User = table.schema.createEntity<User>().as({
type: 'USER',
getPartitionKey: ({ id }: Pick<User, 'id'>) => ['USER', id],
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:
getbatchGetcreateupdatedeletelistAll- RequirestypeIndexlist- RequirestypeIndexqueryqueryIndex- Requires entityindexesdefinition
Query Methods:
query and queryIndex expose custom method plus any defined rangeQueries. Each range query method supports three invocation styles:
- Default call - Returns
QueryResult<Entity>with pagination support .one()- Returns first matching item orundefined.all()- Returns all matching items as array
type Log = {
type: string;
timestamp: string;
}
const Logs = table.schema.createEntity<Log>().as({
type: 'APP_LOGS',
getPartitionKey: () => ['APP_LOG'],
getRangeKey: ({ timestamp }: Pick<Log, 'timestamp'>) => timestamp,
rangeQueries: {
recent: {
operation: 'bigger_than',
getValues: ({ since }: { since: string }) => ({ value: since })
}
},
indexes: {
byType: {
getPartitionKey: ({ type }: Pick<Log, 'type'>) => ['APP_LOG_BY_TYPE', type],
getRangeKey: ({ timestamp }: Pick<Log, 'timestamp'>) => timestamp,
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 exceptlimitandpaginationToken.all(params?)- Accepts all params exceptpaginationToken(limit sets max total items)
All retrieval methods apply extend function if defined.
Single Table Schema: Partition
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:
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<Type>().entity() for main table or .index() for index partitions:
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<User>().entity({
type: 'USER',
paramMatch: {
userId: 'id' // Maps partition param 'userId' to entity property 'id'
},
// Other entity parameters...
})
const UserLoginAttempt = userPartition.use('loginAttempt').create<UserLoginAttempt>().entity({
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:
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<User>().as({
type: 'USER',
getPartitionKey: () => ['APP_USERS'],
getRangeKey: ({ id }: Pick<User, 'id'>) => ['USER', id],
indexes: {
userData: userIndexPartition.use('mainData').create<User>().index({
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:
Single Table Schema: Collection
Collections define joined entity structures for retrieval. Data in single-table designs often spans multiple entries that need to be retrieved and joined together.
Collection Parameters
ref(entity or null, required) - Root entity of the collection. Usenullfor 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 withpartition.index(string, optional) - Table index name. Only valid withgetPartitionKey.partition(Partition, optional) - Existing partition (entity or index partition). Mutually exclusive withgetPartitionKeyandindex. 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. Requiresrefto be an entity.(params?: AnyObject) => RangeQueryConfig- Custom range query function.
join(object, required) - Entity join configuration. Structure:Record<string, JoinConfig>. Each key becomes a property in the result type.
Join Configuration:
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<string, JoinConfig>;
}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 tabletypeIndexwith existing DynamoDB index.'TYPE'- Join by entity type property. RequirestypeIndex.partitionKeydefined (index need not exist in DynamoDB).(parent, child) => boolean- Custom join function. Returnstrueto join.
join(optional) - Nested join configuration. Same structure as rootjoin. Enables multi-level joins.
Returns a collection object for type extraction and query execution.
Collection Type Extraction
Use GetCollectionType to infer the collection's TypeScript type:
type YourType = GetCollectionType<typeof yourCollection>Collection Examples
Collection with Root Entity:
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<User>().entity({
type: 'USER',
paramMatch: { userId: 'id' },
})
const UserLoginAttempt = userPartition.use('loginAttempt').create<UserLoginAttempt>().entity({
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<typeof userWithLogins>Collection without Root Entity:
type UserPermission = {
permissionId: string;
timestamp: string;
addedBy: string;
}
const UserPermission = userPartition.use('permissions').create<UserPermission>().entity({
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<typeof userDataCollection>You can also call table.schema.createCollection if you need to pass in partitions/partition-getters inline
Using Collections
Collections expose a get method via schema.from():
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.
