npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ddbflow

v1.0.0

Published

Fluent DynamoDB params builder — works with AWS SDK v2 and v3

Downloads

32

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 DocumentClient or v3 @aws-sdk/lib-dynamodb)
  • All six operationsput, get, del, update, query, scan
  • All operators=, <>, <, >, <=, >=, BETWEEN, begins_with, contains, attribute_exists, attribute_not_exists, attribute_type, size
  • All UpdateExpression actionsSET, 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 ddbflow

Quick 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.json should 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 = :role2

All 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 field

Update 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 fields

Key fields are always excluded from UpdateExpression automatically.

Flags

builder.forceCreate()        // PutItem — disables attribute_not_exists guard (allow overwrite)
builder.forceRead()          // ConsistentRead: true (get, query, scan)
builder.setLimit(100)        // Limit for query / scan

Build 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 params

After 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 guard

UpdateItem

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 -d

Wait for the container to be healthy (a few seconds), then:

pnpm test:e2e

The test suite will:

  • Create a DynamicBuilderTest table with a GSI automatically.
  • Seed a small set of items.
  • Run put, get, del, update, query, and scan operations using the real DynamoDB API.
  • Drop the table on teardown.

2 — Stop DynamoDB Local

pnpm db:down

Override the endpoint

DYNAMO_ENDPOINT=http://localhost:9000 pnpm test:e2e

Helper 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 tests

Unit tests and E2E tests use separate Jest configs (jest.config.js vs jest.e2e.config.js). Regular pnpm test only runs unit tests — safe to use without Docker.


License

ISC © David Estevez