ddbflow
v1.0.0
Published
Fluent DynamoDB params builder — works with AWS SDK v2 and v3
Downloads
32
Maintainers
Readme
ddbflow
Stop writing DynamoDB boilerplate. Build params with a fluent chain — AWS SDK v2 and v3 ready.
Building DynamoDB params by hand looks like this:
// A simple update: two SETs, an ADD, a REMOVE, and one condition. Buckle up.
const params = {
TableName: 'UsersTable',
Key: { userId: 'u-1' },
ConditionExpression: '#version = :version',
UpdateExpression: 'SET #name = :name, #status = :status ADD #views :views REMOVE #tempToken',
ExpressionAttributeNames: {
'#userId': 'userId',
'#version': 'version',
'#name': 'name',
'#status': 'status',
'#views': 'views',
'#tempToken': 'tempToken',
},
ExpressionAttributeValues: {
':version': 5,
':name': 'Alice Smith',
':status': 'active',
':views': 1,
},
};Every. Single. Time. Each field repeated in three places. A typo anywhere silently breaks the expression. The SDK gives you zero help.
With ddbflow:
const params = builder
.key.hash.name('userId').key.hash.condition.eq('u-1')
.condition('version').eq(5)
.update('name').set('Alice Smith')
.update('status').set('active')
.update('views').add(1)
.update('tempToken').remove()
.build('update');Same params. No repeated strings. No manual attribute maps. No ceremony.
Why ddbflow
- One source of truth — field names live in the chain, never repeated across three separate objects
- Drop-in compatible — returns a plain object you pass directly to any DynamoDB client (v2
DocumentClientor v3@aws-sdk/lib-dynamodb) - All six operations —
put,get,del,update,query,scan - All operators —
=,<>,<,>,<=,>=,BETWEEN,begins_with,contains,attribute_exists,attribute_not_exists,attribute_type,size - All
UpdateExpressionactions —SET,ADD,REMOVE,DELETE - AND / OR filter logic, projection whitelist/blacklist, update field allowlist/blocklist
- Auto-reset after
build()— same builder instance, clean state on every call - TypeScript-first — strict-mode compatible, full declarations, all sub-builder types exported
- Zero runtime dependencies
Installation
pnpm add ddbflow
# or
npm install ddbflow
# or
yarn add ddbflowQuick Start
import { DDBBuilder } from 'ddbflow';
const builder = new DDBBuilder('UsersTable');Query with GSI + date range + filter
const params = builder
.index('byTenantCreatedAt')
.key.hash.name('tenantId').key.hash.condition.eq('t-123')
.key.sort.name('createdAt').key.sort.condition.between('2024-01-01', '2024-12-31')
.condition('status').eq('active')
.condition('deletedAt').attributeNotExists()
.project(['userId', 'email', 'createdAt'])
.setLimit(50)
.build('query');
await docClient.send(new QueryCommand(params));UpdateItem with optimistic lock
const params = builder
.key.hash.name('userId').key.hash.condition.eq('u-1')
.condition('version').eq(5) // fails if version doesn't match
.update('name').set('Alice Smith')
.update('views').add(1)
.update('tempToken').remove()
.build('update');
await docClient.send(new UpdateCommand(params));PutItem with safe-create guard
// attribute_not_exists guard is added automatically — throws if the item already exists
const params = builder
.key.hash.name('userId').key.sort.name('createdAt')
.item({ userId: 'u-2', createdAt: '2024-06-01', name: 'Bob' })
.build('put');
// Need to overwrite? Chain .forceCreate() to skip the guard.Scan with OR condition
const params = builder
.condition('status').eq('suspended')
.orCondition('role').eq('banned')
.setLimit(200)
.build('scan');
// FilterExpression: "#status = :status OR #role = :role"Module Systems
The package ships CommonJS and ES Module builds plus full TypeScript declarations.
The correct format is picked automatically via the exports field — no extra config needed.
TypeScript / ESM
import { DDBBuilder } from 'ddbflow';
import type { DynamoMethod, DynamoParams, ConditionBuilder } from 'ddbflow';
tsconfig.jsonshould use"moduleResolution": "bundler","node16", or"nodenext".
Classic"node"resolution also works.
CommonJS
const { DDBBuilder } = require('ddbflow');API Reference
Constructor
new DDBBuilder(tableName: string)Key Configuration
Configure the hash and/or sort key field names and their conditions.
builder.key.hash.name('userId') // define hash key field name
builder.key.sort.name('createdAt') // define sort key field name
builder.key.hash.condition.eq('u-123') // hash key only supports eq
builder.key.sort.condition.eq('2024-01-01') // sort key conditions:
builder.key.sort.condition.gt('2024-01-01') // gt, ge, lt, le
builder.key.sort.condition.between('2024-01-01', '2024-12-31')
builder.key.sort.condition.beginsWith('2024')Index
builder.index('myGSIName') // GSI or LSI name (required for composite key operations)Filter Conditions
Use .condition(field) for AND logic (default) and .orCondition(field) for OR logic.
// AND conditions (default)
builder.condition('status').eq('active')
builder.condition('age').gt(18)
builder.condition('age').lt(65)
builder.condition('name').beginsWith('Al')
builder.condition('tags').contains('premium')
builder.condition('email').attributeExists()
builder.condition('deletedAt').attributeNotExists()
builder.condition('score').attributeType('N') // S|N|B|BOOL|NULL|L|M|SS|NS|BS
builder.condition('price').between(10, 100)
// OR conditions
builder.condition('role').eq('admin')
builder.orCondition('role').eq('superuser')
// → #role = :role AND ... OR #role = :role2All condition operators
| Method | DynamoDB Expression |
|-------------------------|-------------------------------|
| .eq(val) | field = val |
| .nq(val) | field <> val |
| .gt(val) | field > val |
| .ge(val) | field >= val |
| .lt(val) | field < val |
| .le(val) | field <= val |
| .between(lo, hi) | field BETWEEN lo AND hi |
| .beginsWith(str) | begins_with(field, str) |
| .contains(val) | contains(field, val) |
| .attributeExists() | attribute_exists(field) |
| .attributeNotExists() | attribute_not_exists(field) |
| .attributeType(type) | attribute_type(field, type) |
Bulk conditions (must*)
builder.mustEQ({ status: 'active', type: 'user' })
builder.mustGT({ age: 18 })
builder.mustLE({ credit: 1000 })
builder.mustBeginsWith({ prefix: 'usr-' })
// Generic: supports eq, nq, gt, ge, lt, le
builder.must({
eq: { status: 'active' },
gt: { age: 18 },
lt: { age: 65 },
})Projection
builder.project('userId') // single field
builder.project(['userId', 'email', 'name']) // multiple fields
builder.notProject('password') // exclude fieldUpdate Actions
// Individual field actions
builder.update('name').set('Alice') // SET name = :name
builder.update('views').add(1) // ADD views :views (number increment)
builder.update('tempToken').remove() // REMOVE tempToken (delete attribute)
builder.update('tags').delete(['oldTag']) // DELETE tags :tags (remove from Set)
// Bulk SET / ADD
builder.set({ name: 'Alice', status: 'active' })
builder.add({ views: 1, score: 5 })Update field filtering
builder.enableUpdate(['name', 'status']) // only these fields can be updated
builder.disableUpdate(['id', 'createdAt']) // never update these fieldsKey fields are always excluded from
UpdateExpressionautomatically.
Flags
builder.forceCreate() // PutItem — disables attribute_not_exists guard (allow overwrite)
builder.forceRead() // ConsistentRead: true (get, query, scan)
builder.setLimit(100) // Limit for query / scanBuild Methods
builder.build('put') // → PutItem params
builder.build('get') // → GetItem params
builder.build('del') // → DeleteItem params
builder.build('update') // → UpdateItem params
builder.build('query') // → Query params
builder.build('scan') // → Scan paramsAfter build() the builder is automatically reset (same table name, clean state).
Usage Examples
PutItem
const params = builder
.key.hash.name('userId')
.key.sort.name('createdAt')
.item({ userId: 'u-1', createdAt: '2024-01-15', name: 'Alice' })
.build('put');
// ConditionExpression: "attribute_not_exists(#userId) AND attribute_not_exists(#createdAt)"
// Overwrite mode (no condition guard):
const overwrite = builder
.key.hash.name('userId')
.item({ userId: 'u-1', name: 'Alice Updated' })
.forceCreate()
.build('put');GetItem
const params = builder
.key.hash.name('userId')
.key.hash.condition.eq('u-1')
.project(['userId', 'email', 'name'])
.build('get');DeleteItem
const params = builder
.key.hash.name('userId')
.key.hash.condition.eq('u-1')
.build('del');
// ConditionExpression: "attribute_exists(#userId)" — safe-delete guardUpdateItem
const params = builder
.key.hash.name('userId')
.key.hash.condition.eq('u-1')
.condition('version').eq(3) // optimistic lock
.update('name').set('Alice Smith')
.update('views').add(1)
.update('tempField').remove()
.update('oldTags').delete(['tag1'])
.build('update');Query
const params = builder
.index('byTenantCreatedAt')
.key.hash.name('tenantId')
.key.sort.name('createdAt')
.key.hash.condition.eq('t-1')
.key.sort.condition.between('2024-01-01', '2024-12-31')
.condition('status').eq('active')
.condition('deletedAt').attributeNotExists()
.project(['id', 'name', 'status', 'createdAt'])
.setLimit(100)
.build('query');Scan with AND + OR
const params = builder
.condition('status').eq('active')
.condition('type').eq('premium')
.orCondition('role').eq('admin') // OR admin users regardless of status/type
.setLimit(200)
.build('scan');
// FilterExpression: "#status = :status AND #type = :type OR #role = :role"TypeScript Types
All public types are exported from the package entry point and can be imported with import type.
import type {
// Operation methods accepted by .build()
DynamoMethod, // 'put' | 'get' | 'del' | 'update' | 'query' | 'scan'
// Returned by .build() — a plain object ready for the DynamoDB SDK
DynamoParams, // Record<string, unknown>
// DynamoDB attribute types for .attributeType()
AttributeType, // 'S' | 'N' | 'B' | 'BOOL' | 'NULL' | 'L' | 'M' | 'SS' | 'NS' | 'BS'
// All condition operators
ConditionOperator, // 'eq' | 'nq' | 'gt' | 'ge' | 'lt' | 'le' | 'between' | ...
ComparisonOperator, // 'eq' | 'nq' | 'gt' | 'ge' | 'lt' | 'le'
RangeOperator, // 'between'
FunctionOperator, // 'attribute_exists' | 'begins_with' | 'contains' | ...
// Update actions
UpdateAction, // 'set' | 'add' | 'remove' | 'delete'
// AND / OR joiner for filter expressions
LogicJoiner, // 'AND' | 'OR'
// Bulk conditions shape accepted by .must()
BulkConditions,
// Fluent sub-builders returned by .condition() / .orCondition() / .update() / .key
ConditionBuilder,
UpdateBuilder,
KeyAPI,
HashKeyConditionBuilder,
SortKeyConditionBuilder,
} from 'ddbflow';Typing the params object
DynamoParams is Record<string, unknown>. When passing to the AWS SDK, cast or access fields
with a type assertion since SDK input types differ slightly between v2 and v3:
import { QueryCommand } from '@aws-sdk/lib-dynamodb';
import type { DynamoParams } from 'ddbflow';
const params: DynamoParams = builder
.key.hash.name('pk')
.key.hash.condition.eq('u-1')
.build('query');
// AWS SDK v3 — params shape is directly compatible
await docClient.send(new QueryCommand(params as Parameters<typeof QueryCommand>[0]));Extending the builder with a typed wrapper
import { DDBBuilder } from 'ddbflow';
import type { DynamoParams } from 'ddbflow';
class UserRepository {
private readonly builder = new DDBBuilder('UsersTable');
findActiveByTenant(tenantId: string, limit = 50): DynamoParams {
return this.builder
.index('byTenantStatus')
.key.hash.name('tenantId')
.key.sort.name('status')
.key.hash.condition.eq(tenantId)
.key.sort.condition.eq('active')
.setLimit(limit)
.build('query');
}
}Build Output
The package ships:
| File | Format |
|-------------------|----------------------------|
| dist/index.js | CommonJS (for require()) |
| dist/index.mjs | ES Module (for import) |
| dist/index.d.ts | TypeScript declarations |
End-to-End Tests
E2E tests run queries against a real DynamoDB Local instance via Docker.
1 — Start DynamoDB Local
pnpm db:up
# or directly:
docker compose -f docker-compose.test.yml up -dWait for the container to be healthy (a few seconds), then:
pnpm test:e2eThe test suite will:
- Create a
DynamicBuilderTesttable with a GSI automatically. - Seed a small set of items.
- Run
put,get,del,update,query, andscanoperations using the real DynamoDB API. - Drop the table on teardown.
2 — Stop DynamoDB Local
pnpm db:downOverride the endpoint
DYNAMO_ENDPOINT=http://localhost:9000 pnpm test:e2eHelper scripts
| Script | Description |
|-----------------------|----------------------------------------|
| pnpm db:up | Start DynamoDB Local in the background |
| pnpm db:down | Stop and remove the container |
| pnpm db:logs | Follow container logs |
| pnpm test:e2e | Run E2E tests (needs db:up) |
| pnpm test:e2e:watch | Watch mode for E2E tests |
Development
pnpm build # compile TypeScript → dist/
pnpm build:watch # watch mode
pnpm test # run unit tests (no Docker needed)
pnpm test:verbose # verbose output
pnpm test:watch # watch mode for unit testsUnit tests and E2E tests use separate Jest configs (
jest.config.jsvsjest.e2e.config.js). Regularpnpm testonly runs unit tests — safe to use without Docker.
License
ISC © David Estevez
