-
Notifications
You must be signed in to change notification settings - Fork 0
Release notes
Reverse-chronological. Links to the npm version history and this repo's git tags.
Bug fixes surfaced by the post-implementation ergonomics review against a realistic hierarchical use case (runnable example at examples/car-rental/). All three fixes restore documented intent; no API-shape changes.
-
adapter.buildKeynow emits the partition-key equality on composite keys. Previously the helper returned only the sort-key condition (begins_with(#sk, :v)or#sk = :v), so every resultingQueryfailed withValidationException: Query condition missed key schema element. Now emits#pk = :pkv AND (…sk condition…)via the primitive's existingpkName/pkValueknobs. Existing buildKey tests updated to assert the corrected shape. -
adapter.patch+versionFieldnow conditions on existence by default. Callingpatch(key, partial)without{expectedVersion}previously emittedattribute_not_exists(<pk>)— the wrong semantic for patch (fails on every existing record). Now emitsattribute_exists(<pk>)unlessexpectedVersionis supplied, matching patch's intended "update-if-exists" contract. The version still ADDs +1 via UpdateExpression regardless. Version-conditioned patches ({expectedVersion}) are unchanged. -
getListByParamsnow honorsoptions.fFilter/options.filter/options.asOf. Callers who hand-built query params (e.g., viabuildKey) and passed them togetListByParams(params, options)found these three options silently dropped — onlygetList(options, example, index)honored them via_buildListParams. Moved option-to-params translation intogetListByParamsso both entry points funnel through one application point.getListremains a thin wrapper.
-
examples/car-rental/— runnable end-to-end walkthrough against DynamoDB Local exercising the full 3.6.0 surface (typed declaration,buildKey,f-filter grammar, mass ops with cursor resume, cascade primitives, optimistic concurrency, scope-freeze,typeOfdispatch for multi-type-same-tier,ensureTable/verifyTable, descriptor round-trip). Serves as integration test + documentation-adjacent template. See the Ergonomics review note.
535 Node · 529 Bun · 529 Deno · npm audit clean. Typed + JS checks pass.
Consolidated publish of the hierarchical-use-case workstream (3.2 → 3.6). Tags 3.2.0, 3.3.0, 3.4.0, 3.5.0 exist locally for history; 3.6.0 is the first npm publish since 3.1.2. No load-bearing breaking changes — renamed bulk-individual helpers ship alongside deprecated aliases. The sections below cover what landed in each tag, newest first.
New subpath dynamodb-toolkit/provisioning and CLI bin/dynamodb-toolkit. Opt-in; IaC-managed deployments ignore it entirely.
-
ensureTable(adapter, {yes?, dryRun?})— ADD-only planner. Returns a plan by default ({tableName, steps, summary[]});{yes: true}executes. Plan steps:create(CreateTable),add-gsi(UpdateTable with a single{Create}),skip-extra-gsi/skip-extra-lsi(reported, never dropped),skip-missing-lsi(surfaced with a note — LSIs can only be declared at CreateTable). Never emits destructive operations. Delegates declaration legality to DynamoDB. -
verifyTable(adapter, {throwOnMismatch?, requireDescriptor?})— structured diff. Compares base KeySchema, AttributeDefinitions, GSI/LSI key schemas + projections. Billing mode / stream config compared only when declared on the adapter. Extras in the live table arewarn(non-blocking); declared-but-missing areerror.{throwOnMismatch: true}throwsTableVerificationFailedcarrying the same diffs. -
Opt-in descriptor record via
options.descriptorKeyon the Adapter (typical value'__adapter__').ensureTablewrites a JSON snapshot under__toolkit_descriptor__on the reserved-key record;verifyTablereads it back and flags drift thatDescribeTablecan't see (marshalling fields, searchable mirrors, filterable allowlist, versionField/createdAtField names). Absent descriptor is neutral by default;{requireDescriptor: true}opts into "this table must have been toolkit-provisioned" strictness. -
CLI —
dynamodb-toolkit ensure-table <adapter-module>/verify-table <adapter-module>. Loads the module via dynamic ESMimportand acceptsdefaultor namedadapterexport. Flags--yes,--strict,--require-descriptor,--json,-h/--help. -
New error:
TableVerificationFailed(root re-export).
Tests: unit against a mock client + e2e against DynamoDB Local (Docker, skipped when unavailable).
Developer-primitive cascade for hierarchical deletes / clones / moves. Relationship gate is explicit — the toolkit does not infer cascade scope from composite keyFields alone.
-
options.relationships: {structural: true}on Adapter construction. Validated (requireskeyFields.length > 1+structuralKeydeclared). Forward-compatible shape for future cross-adapter / via-GSI kinds. -
Five cascade primitives — two intentional styles per op (no options-bag overload):
-
deleteAllUnder(srcKey, options)— leaf-first descendant delete +ifExistsself-delete (absent →skipped). -
cloneAllUnder(srcKey, dstKey, options)— root-first prefix-swap subtree clone.options.mapFncomposes after the swap viamergeMapFn. -
cloneAllUnderBy(srcKey, mapFn, options)— mapFn-driven subtree clone; destinations as mapFn dictates (fan-out across different subtrees). -
moveAllUnder(srcKey, dstKey, options)/moveAllUnderBy(srcKey, mapFn, options)— two-phase idempotent copy+delete (shared_subtreeRenamewithrename).
-
-
Throws
CascadeNotDeclaredwhen cascade primitives are called withoutrelationships.structural. -
Default REST handler unchanged.
DELETE /keystays single-row; developers wire cascade endpoints themselves by callingadapter.deleteAllUnder(key)from their handler. -
Resumability:
maxItems/resumeTokenflow through the descendants phase; self-op runs only when pagination completes (and is skipped onresumeTokenforcloneAllUnder*to prevent re-processing).
Optimistic-concurrency pair (versionField + asOf) and the new /marshalling subpath.
-
options.versionField— opt-in OC. RequirestechnicalPrefix. Universal write conditionattribute_not_exists(<pk>) OR <versionField> = :observed; auto-increment on every write;deleteaccepts{expectedVersion}; masseditListByParamsroutesConditionalCheckFailedto a dedicatedconflicts: [{key, reason: 'VersionConflict', sdkError}]bucket. Preserved acrossreviveso callers round-trip versions naturally. -
options.createdAtField+{asOf}mass-op option — scope-freeze. Mass ops accept{asOf: Date | string | number}; the toolkit AND-merges<createdAtField> <= :asOfintoFilterExpression. Requires the adapter to opt in — throwsCreatedAtFieldNotDeclaredotherwise. No auto-write; the user'spreparehook owns the format. -
dynamodb-toolkit/marshalling— standalone helpers in the SDKmarshall/unmarshallnaming family:-
marshallDateISO/marshallDateEpoch— no genericmarshallDatealias (forces ISO-vs-epoch intent at the call site). -
marshallMap(map, valueTransform?)— optional nested-value transform. -
marshallURL—URLinstance round-trip. -
Marshaller<TRuntime, TStored>type pair with concretedateISO,dateEpoch,urlpairs.
-
-
New errors:
CreatedAtFieldNotDeclared.
Cursor-based resumability for list mass ops, plus the edit primitive and two subtree macros.
-
encodeCursor/decodeCursorindynamodb-toolkit/mass(base64url; NodeBufferfast path +TextEncoder/btoafallback for Bun/Deno). -
MassOpOptions(maxItems,resumeToken,ifNotExists,ifExists) +MassOpResult({processed, skipped, failed, conflicts, cursor?}). -
Resumable list ops —
deleteListByParams/cloneListByParams/moveListByParamsreturnMassOpResult; page-boundary cursor emission;ifNotExists/ifExistson clone switch fromBatchWriteItemto per-itemPutItemwithConditionExpression. -
adapter.edit(key, mapFn, {readFields?})— read → shallow-diff →UpdateCommand. ThrowsKeyFieldChangedon keyField mutation;{allowKeyChange: true}opts into auto-promotion to a move. No-op short-circuit suppresses WCU on empty diffs. -
adapter.editListByParams— resumable per-item edit across a scan. -
adapter.rename(from, to, options)— put-if-not-exists (dst) then delete (src); two idempotent phases. -
adapter.cloneWithOverwrite(from, to, options)— delete (dst) then put (dst); source stays intact. -
Canned mapFn builders —
swapPrefix(src, dst),mergeMapFn(...fns)for composing transforms. -
GIGO cleanup: removed
AmbiguousDestination, removedtypeof mapFn !== 'function'guards, removedifNotExists+ifExistsmutual-exclusion check. TS types are the contract. -
New errors:
KeyFieldChanged.
The largest release of the workstream. Makes hierarchical adapters declarative, ships the read-side key-condition helpers, lands the filter grammar, cleans up misleading "List" names.
-
options.technicalPrefix(opt-in) — namespaces adapter-managed fields. Prepare rejects incoming user items touching the prefix; revive strips prefixed fields before the user's hook sees them. -
options.keyFieldsaccepts full descriptors:{name, type?: 'string' | 'number' | 'binary', width?: number}.widthrequired on{type: 'number'}in composite keys (zero-padding). -
options.structuralKey— required for compositekeyFields. Shorthand'_sk'or{name, separator?}(defaults'|'). -
options.indices— single discriminated map{<name>: {type: 'gsi' | 'lsi', pk?, sk?, projection?, sparse?, indirect?}}. LegacyindirectIndices: {name: 1}entries synthesise into minimal{type: 'gsi', indirect: true, projection: 'keys-only'}. -
options.typeLabels(paired 1:1 withkeyFields) +options.typeDiscriminator— poweradapter.typeOf(item)(type detection by discriminator field → structural depth → raw depth). - Built-in prepare / revive steps — structural-key formation on prepare (contiguous-from-start + zero-padding), stripping on revive. User hooks still compose after built-ins.
-
buildKeyCondition(input, params)primitive indynamodb-toolkit/expressions. Follows thebuild<Target>convention + counter-based placeholder naming. -
adapter.buildKey(values, options, params)— ergonomic Adapter method.valuesis an object keyed bykeyFieldsnames (validated contiguous-from-start).options.kind: 'exact' | 'children' | 'partial';options.partialappends after the separator. Mergesparamsexactly likebuildCondition.
-
f-<field>-<op>=<value>grammar replaces the v2prefix/pair conventions. Single prefixf-, noflt-. -
Op vocabulary:
eq,ne,lt,le,gt,ge,in,btw,beg,ct,ex,nx. SQL-standardge/le(not Django'sgte/lte). -
options.filterable: {<field>: [ops]}— allowlist on the Adapter. Unlisted field →BadFilterField; disallowed op →BadFilterOp. -
Multi-value via first-character delimiter with
,fallback (f-cost-in=,1,2,3orf-cost-in=|1|2|3). -
?prefix=absorbed intof-<sk>-beg=…and removed. One grammar.
Triggered by the design-principle audit that distinguished bulk-individual helpers (plural of single-op; caller supplies the set) from list operations (toolkit produces the list).
-
readListByKeys+readOrderedListByKeys→readByKeys(always ordered, length-preserving —undefinedat missing positions per the D2 fix). -
deleteListByKeys→deleteByKeys. -
writeList→writeItems. - Old names remain as deprecated aliases with a one-time
console.warn; removed in a future minor. - File renames:
read-ordered-list-by-keys.js→read-by-keys.js(callers switch);delete-list-by-keys.js→delete-by-keys.js.
getByKeys and the wire -by-names endpoint no longer silently drop missing items. Result array is length-aligned with the input keys: result[i] === undefined at missing positions. Callers who want a compact array call .filter(Boolean) themselves.
NoIndexForSortField, ConsistentReadOnGSIRejected, BadFilterField, BadFilterOp (root re-exports). ToolkitError base class for instanceof discrimination.
- Thin SDK-helper — consumes/produces SDK types, same semantics; introduces toolkit concepts only when strictly needed.
-
No client-side list manipulation — don't sort/filter/reshape lists post-read. Refuse with a clear error when the DB can't answer natively.
?sort=<field>without a matching index now throwsNoIndexForSortFieldinstead of in-memory sorting. -
User callbacks throw; toolkit does not wrap — every caller-supplied extension point (hooks,
mapFn,sparse.onlyWhenpredicate,exampleFromContext,valueTransform) propagates errors unchanged. Caller's error class / message / stack surfaces intact. -
GIGO: no runtime type-checking of args — TS types are the contract; no
typeof !== 'function'guards on mapFn args, etc.
Rest-core extractions + streaming body reader. Purely additive; no breaking changes. Framework adapters (koa / express / fetch / lambda) adopted these extractions at their 0.2.0 release the same day.
Pulled out of the dynamodb-toolkit/handler + dynamodb-toolkit/rest-core internals so framework adapters can share them:
-
buildListOptions(query)— constructs theListOptionsbag from parsed query state (offset/limit/sort/filter/fields). -
resolveSort(sortField, options)— maps?sort=<field>to the resolved index /descendingflag. -
stripMount(path, mountPath)— trailing-slash-safe mount-point stripping. -
coerceStringQuery(raw)— null-prototype accumulator coercing multi-value query shapes to a flat{k: string | string[]}map. -
validateWriteBody(body)— gates POST// PUT/:key/ PATCH/:keyon object-shaped bodies. Returns400 BadBodyonnull, arrays, or primitives. Previously those bodies were silently spread ({...null, ...key}→ key-only row).
-
readJsonBody(req, {maxBodyBytes, destroy?})— streamingTextDecoderper chunk; peak memory ≈ 1× body size (was 3× with the Buffer-concat path). Partial UTF-8 codepoints across chunk boundaries handled by the streaming decoder. -
matchRouteHEAD → GET auto-promote —HEAD /keynow matches theGET /keyroute (unless a dedicated HEAD handler is registered). -
Body-always-parsed invariant —
PUT /-clone/PUT /-movealways receive the parsed body inexampleFromContext, even when the body is empty. -
Byte-accurate
maxBodyBytes— limit applied to raw bytes, not character count.
Three CodeQL alerts cleared (workflow permissions, two ReDoS candidates).
Security + correctness hardening sweep. Full module-by-module audit — 19 bug fixes + 9 defensive improvements. All changes are either bug fixes restoring the documented / intended contract, or additive opt-in options with sensible defaults — so this is a patch release.
-
Prototype pollution guards (4 vectors closed):
-
paths/getPathno longer reads through the prototype chain.getPath({}, 'toString')now returnsdefaultValueinstead ofFunction.prototype.toString. -
paths/setPath/paths/deletePathrefuse to walk or write through__proto__/constructor/prototypesegments. -
rest-core/parsePatchuses a null-prototype accumulator — a PATCH body containing"__proto__"no longer reassigns the patch's prototype. -
mass/readOrderedListByKeysusesObject.create(null)accumulators — a record with partition-key value'__proto__'no longer pollutesObject.prototypeglobally.
-
-
DoS-on-self vectors closed (3):
-
batchWrite/batchGetcap at 8 retry attempts (was infinite loop on persistent throttling). -
handlerrejects bodies larger than 1 MB with HTTP 413PayloadTooLarge(was unbounded memory accumulation). -
parsePagingcaps?offset=at 100 000 by default (was unbounded —?offset=1e15could DoS via astronomical skip-page calls).
-
-
AWS-aligned retry backoff:
backoff()default per-retry cap is now 20 s (was undocumented 60 s with infinite retries). Matches AWS SDK v3 default and AWS DynamoDB retry guidance ("stop around one minute"). WithMAX_ATTEMPTS = 8, worst-case total wait is ~43 s.
-
Adapter.moveByKeysandmass/moveListnow pairput+deleteper item. Previously, a falsymapFnreturn could drop the put but leave the delete in flight — silent data loss. Now both legs drop together. -
Adapterconstructor rejects non-arraykeyFields.new Adapter({..., keyFields: 'name'})previously silently produced broken queries (attribute alias'n'); now throws. -
applyBatch/applyTransactionthrow on unknownactionvalues (were silently dropped — caused phantom "processed" counts and missing writes). -
buildUpdatethrows on unknownarrayOp.opvalues (typos like'increment'were silently dropped, producing partial UpdateExpressions). -
buildUpdateguardsoptions.deleteandoptions.arrayOpswithArray.isArray(string input no longer iterates characters). -
applyBatchtotalcounts only items actually added to the batch (was inflating the count with dropped items). -
normalizeFields(',,,')now returnsnullinstead of[](restores the "no fields requested → project everything" contract). -
applyPatch(o, null)returnsounchanged instead of crashing onObject.keys(null). -
applyPatchoptions.deletenow gated withArray.isArray. -
cloneParams(null)returns a fresh clone instead of crashing. -
handlerstrips leading//fromreq.urlsoGET //evil.com/pathcannot pivot the URL's origin; sanitizes theHostheader to prevent crashes on malformed headers. -
handler405 error body no longer echoesurl.pathname(minor information disclosure removed).
-
createHandler(adapter, {maxBodyBytes})— HTTP body-size cap. Default 1 MB. -
parseFields(input, {maxItems})— output-length cap. Default 1000. -
parseNames(input, {maxItems})— output-length cap. Default 1000. -
parseFilter(input, {maxLength})— query-string length cap. Default 1024. -
parsePaging(input, {maxOffset})— offset ceiling. Default 100 000. -
defaultPolicy.maxOffsetadded (same default).
| Change | Before | After |
|---|---|---|
backoff() default cap |
undocumented 60 s | 20 s |
batchWrite / batchGet attempts |
infinite retry | 8 (throws after) |
Unknown action in applyBatch / applyTransaction
|
silent drop | throws |
Unknown arrayOp.op in buildUpdate
|
silent drop | throws |
Adapter with non-array keyFields
|
silently accepted (broken) | throws |
| Handler request body > 1 MB | unbounded | 413 PayloadTooLarge
|
?offset= on list routes |
unbounded | capped at 100 000 |
getPath({}, 'toString') |
Function.prototype.toString |
defaultValue |
See the per-page wiki updates (REST core, HTTP handler, Batch and transactions, Mass operations) for the detailed surface.
Additive features. No breaking changes.
-
returnFailedItemoption onpost/put/patch/deleteand theirmake*batch builders. SetsReturnValuesOnConditionCheckFailure: 'ALL_OLD'; the thrownConditionalCheckFailedExceptioncarries the colliding item on itsItemfield (or the matchingCancellationReason.Iteminside a transaction). -
{options: ...}sentinel forapplyTransaction— position-independent descriptor carrying transaction-level knobs:clientRequestToken(idempotency),returnConsumedCapacity,returnItemCollectionMetrics. -
explainTransactionCancellation(err, ...descriptors)in/batch. MapsTransactionCanceledException.CancellationReasonsback to the input descriptors and returns{failures, message} | null. Paired withreturnFailedItemto surface the colliding item in one call. -
Cross-runtime test matrix — Node, Deno, Bun (
npm test,npm run test:deno,npm run test:bun). - CJS smoke test + TypeScript smoke test wired into the suite.
-
LICENSE file shipped (BSD-3-Clause).
llms.txt+llms-full.txtincluded in the npm tarball. - CI lint step re-enabled (
.github/dependabot.ymlin.prettierignore). - Three CodeQL alerts cleared (workflow permissions, two ReDoS candidates).
Typed .d.ts sidecars updated throughout. See Adapter: CRUD methods, Batch and transactions, and Compatibility for the detailed coverage.
Green-field rewrite on AWS SDK v3. Not a drop-in upgrade from v2. v2 consumers stay on dynamodb-toolkit@2.3.0. The v3 line is the actively-developed branch.
Highlights:
-
AWS SDK v3 (
@aws-sdk/client-dynamodb+@aws-sdk/lib-dynamodb) peer deps, replacing v2'saws-sdk. -
ESM-only,
"type": "module", hand-written.d.tssidecars next to every.js. No build step. -
One data format — plain JS via
lib-dynamodbmiddleware. The v2Raw/DbRawpair collapsed into a singleRaw<T>bypass marker. -
Options bags everywhere:
put(item, {force: true})instead ofput(item, true);patch(key, patch, {delete, separator, arrayOps, conditions}). -
Hooks renamed:
prepareListParams→prepareListInput,updateParams→updateInput. Hooks bag (options.hooks) is the canonical extension point. -
Patch wire format:
_delete/_separator(single underscore); configurable viapolicy.metaPrefix. -
REST split:
dynamodb-toolkit/rest-coreis framework-agnostic;dynamodb-toolkit/handleris thenode:httpadapter. Koa / Express / Hono / Fetch / Lambda adapters live in separate packages. -
Transaction auto-upgrade: single-op CRUD automatically upgrades to
TransactWriteItemswhencheckConsistencyreturns extra descriptors. - Indirect-index second-hop for sparse GSIs with key-only projection.
- Dropped:
makeClient,getProfileName,specialTypes,converter,converterOptions— use@aws-sdk/credential-providersdirectly.
See Migration: v2 to v3 and SDK v2 to v3 cheat sheet.
The v2 documentation snapshot lives at the v2.3-docs git tag of this wiki repo. The v2 source code remains on npm as dynamodb-toolkit@2.3.0 and on GitHub at the matching git tag.
Start here
- Getting started
- Concepts
- Key and field design
- Compatibility
- Migration: v2 to v3
- SDK v2 to v3 cheat sheet
Guides
- Hierarchical data walkthrough
- Key expression patterns
- Multi-type tables
- Pagination
- Mass operation semantics
- URL schema design
Adapter
- Adapter
- Constructor options
- CRUD methods
- Mass methods
- Batch builders
- Hooks
- Raw marker
- Indirect indices
- Transaction auto-upgrade
Expression builders
Batch / transactions / mass / paths
REST surface
Framework adapters
Recipes
- Recipes index
- List records of a tier
- Per-tier sparse GSI markers
- Tier within a partition
- Reservation with auto-release
- Keys-only GSI, runtime projection
- Cascade subtree operations
- Querying subtrees with buildKey
- Filter URL grammar
- Text search
- Provisioning workflow
- Resumable mass operations
History