Skip to content

Release notes

Eugene Lazutkin edited this page Apr 22, 2026 · 7 revisions

Release notes

Reverse-chronological. Links to the npm version history and this repo's git tags.

3.6.0 — 2026-04-22

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.

3.6.0 — T1 / T2 provisioning

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 are warn (non-blocking); declared-but-missing are error. {throwOnMismatch: true} throws TableVerificationFailed carrying the same diffs.
  • Opt-in descriptor record via options.descriptorKey on the Adapter (typical value '__adapter__'). ensureTable writes a JSON snapshot under __toolkit_descriptor__ on the reserved-key record; verifyTable reads it back and flags drift that DescribeTable can'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.
  • CLIdynamodb-toolkit ensure-table <adapter-module> / verify-table <adapter-module>. Loads the module via dynamic ESM import and accepts default or named adapter export. 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).

3.5.0 — Cascade + A6' relationship declaration

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 (requires keyFields.length > 1 + structuralKey declared). 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 + ifExists self-delete (absent → skipped).
    • cloneAllUnder(srcKey, dstKey, options) — root-first prefix-swap subtree clone. options.mapFn composes after the swap via mergeMapFn.
    • 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 _subtreeRename with rename).
  • Throws CascadeNotDeclared when cascade primitives are called without relationships.structural.
  • Default REST handler unchanged. DELETE /key stays single-row; developers wire cascade endpoints themselves by calling adapter.deleteAllUnder(key) from their handler.
  • Resumability: maxItems / resumeToken flow through the descendants phase; self-op runs only when pagination completes (and is skipped on resumeToken for cloneAllUnder* to prevent re-processing).

3.4.0 — Concurrency + Marshalling

Optimistic-concurrency pair (versionField + asOf) and the new /marshalling subpath.

  • options.versionField — opt-in OC. Requires technicalPrefix. Universal write condition attribute_not_exists(<pk>) OR <versionField> = :observed; auto-increment on every write; delete accepts {expectedVersion}; mass editListByParams routes ConditionalCheckFailed to a dedicated conflicts: [{key, reason: 'VersionConflict', sdkError}] bucket. Preserved across revive so 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> <= :asOf into FilterExpression. Requires the adapter to opt in — throws CreatedAtFieldNotDeclared otherwise. No auto-write; the user's prepare hook owns the format.
  • dynamodb-toolkit/marshalling — standalone helpers in the SDK marshall/unmarshall naming family:
    • marshallDateISO / marshallDateEpoch — no generic marshallDate alias (forces ISO-vs-epoch intent at the call site).
    • marshallMap(map, valueTransform?) — optional nested-value transform.
    • marshallURLURL instance round-trip.
    • Marshaller<TRuntime, TStored> type pair with concrete dateISO, dateEpoch, url pairs.
  • New errors: CreatedAtFieldNotDeclared.

3.3.0 — Mass-op resumability, edit, subtree macros

Cursor-based resumability for list mass ops, plus the edit primitive and two subtree macros.

  • encodeCursor / decodeCursor in dynamodb-toolkit/mass (base64url; Node Buffer fast path + TextEncoder/btoa fallback for Bun/Deno).
  • MassOpOptions (maxItems, resumeToken, ifNotExists, ifExists) + MassOpResult ({processed, skipped, failed, conflicts, cursor?}).
  • Resumable list opsdeleteListByParams / cloneListByParams / moveListByParams return MassOpResult; page-boundary cursor emission; ifNotExists/ifExists on clone switch from BatchWriteItem to per-item PutItem with ConditionExpression.
  • adapter.edit(key, mapFn, {readFields?}) — read → shallow-diff → UpdateCommand. Throws KeyFieldChanged on 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 buildersswapPrefix(src, dst), mergeMapFn(...fns) for composing transforms.
  • GIGO cleanup: removed AmbiguousDestination, removed typeof mapFn !== 'function' guards, removed ifNotExists + ifExists mutual-exclusion check. TS types are the contract.
  • New errors: KeyFieldChanged.

3.2.0 — Foundation: declarative schema, A1', filter grammar, naming cleanup

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.

Declarative schema

  • 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.keyFields accepts full descriptors: {name, type?: 'string' | 'number' | 'binary', width?: number}. width required on {type: 'number'} in composite keys (zero-padding).
  • options.structuralKey — required for composite keyFields. Shorthand '_sk' or {name, separator?} (defaults '|').
  • options.indices — single discriminated map {<name>: {type: 'gsi' | 'lsi', pk?, sk?, projection?, sparse?, indirect?}}. Legacy indirectIndices: {name: 1} entries synthesise into minimal {type: 'gsi', indirect: true, projection: 'keys-only'}.
  • options.typeLabels (paired 1:1 with keyFields) + options.typeDiscriminator — power adapter.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.

A1' — read-side key-condition helpers

  • buildKeyCondition(input, params) primitive in dynamodb-toolkit/expressions. Follows the build<Target> convention + counter-based placeholder naming.
  • adapter.buildKey(values, options, params) — ergonomic Adapter method. values is an object keyed by keyFields names (validated contiguous-from-start). options.kind: 'exact' | 'children' | 'partial'; options.partial appends after the separator. Merges params exactly like buildCondition.

Filter grammar

  • f-<field>-<op>=<value> grammar replaces the v2 prefix/pair conventions. Single prefix f-, no flt-.
  • Op vocabulary: eq, ne, lt, le, gt, ge, in, btw, beg, ct, ex, nx. SQL-standard ge/le (not Django's gte/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,3 or f-cost-in=|1|2|3).
  • ?prefix= absorbed into f-<sk>-beg=… and removed. One grammar.

Naming cleanup (with deprecated aliases)

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 + readOrderedListByKeysreadByKeys (always ordered, length-preserving — undefined at missing positions per the D2 fix).
  • deleteListByKeysdeleteByKeys.
  • writeListwriteItems.
  • Old names remain as deprecated aliases with a one-time console.warn; removed in a future minor.
  • File renames: read-ordered-list-by-keys.jsread-by-keys.js (callers switch); delete-list-by-keys.jsdelete-by-keys.js.

D2 — length-preserving bulk reads

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.

New errors

NoIndexForSortField, ConsistentReadOnGSIRejected, BadFilterField, BadFilterOp (root re-exports). ToolkitError base class for instanceof discrimination.

Standing rules pinned this release

  • 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 throws NoIndexForSortField instead of in-memory sorting.
  • User callbacks throw; toolkit does not wrap — every caller-supplied extension point (hooks, mapFn, sparse.onlyWhen predicate, 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.

3.1.2 — 2026-04-20

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.

Rest-core extractions (Tier-A)

Pulled out of the dynamodb-toolkit/handler + dynamodb-toolkit/rest-core internals so framework adapters can share them:

  • buildListOptions(query) — constructs the ListOptions bag from parsed query state (offset/limit/sort/filter/fields).
  • resolveSort(sortField, options) — maps ?sort=<field> to the resolved index / descending flag.
  • 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 /:key on object-shaped bodies. Returns 400 BadBody on null, arrays, or primitives. Previously those bodies were silently spread ({...null, ...key} → key-only row).

Handler

  • readJsonBody(req, {maxBodyBytes, destroy?}) — streaming TextDecoder per 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.
  • matchRoute HEAD → GET auto-promoteHEAD /key now matches the GET /key route (unless a dedicated HEAD handler is registered).
  • Body-always-parsed invariantPUT /-clone / PUT /-move always receive the parsed body in exampleFromContext, even when the body is empty.
  • Byte-accurate maxBodyBytes — limit applied to raw bytes, not character count.

Security (continued from 3.1.1)

Three CodeQL alerts cleared (workflow permissions, two ReDoS candidates).

3.1.1 — 2026-04-19

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.

Security

  • Prototype pollution guards (4 vectors closed):
    • paths/getPath no longer reads through the prototype chain. getPath({}, 'toString') now returns defaultValue instead of Function.prototype.toString.
    • paths/setPath / paths/deletePath refuse to walk or write through __proto__ / constructor / prototype segments.
    • rest-core/parsePatch uses a null-prototype accumulator — a PATCH body containing "__proto__" no longer reassigns the patch's prototype.
    • mass/readOrderedListByKeys uses Object.create(null) accumulators — a record with partition-key value '__proto__' no longer pollutes Object.prototype globally.
  • DoS-on-self vectors closed (3):
    • batchWrite / batchGet cap at 8 retry attempts (was infinite loop on persistent throttling).
    • handler rejects bodies larger than 1 MB with HTTP 413 PayloadTooLarge (was unbounded memory accumulation).
    • parsePaging caps ?offset= at 100 000 by default (was unbounded — ?offset=1e15 could 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"). With MAX_ATTEMPTS = 8, worst-case total wait is ~43 s.

Correctness

  • Adapter.moveByKeys and mass/moveList now pair put + delete per item. Previously, a falsy mapFn return could drop the put but leave the delete in flight — silent data loss. Now both legs drop together.
  • Adapter constructor rejects non-array keyFields. new Adapter({..., keyFields: 'name'}) previously silently produced broken queries (attribute alias 'n'); now throws.
  • applyBatch / applyTransaction throw on unknown action values (were silently dropped — caused phantom "processed" counts and missing writes).
  • buildUpdate throws on unknown arrayOp.op values (typos like 'increment' were silently dropped, producing partial UpdateExpressions).
  • buildUpdate guards options.delete and options.arrayOps with Array.isArray (string input no longer iterates characters).
  • applyBatch total counts only items actually added to the batch (was inflating the count with dropped items).
  • normalizeFields(',,,') now returns null instead of [] (restores the "no fields requested → project everything" contract).
  • applyPatch(o, null) returns o unchanged instead of crashing on Object.keys(null).
  • applyPatch options.delete now gated with Array.isArray.
  • cloneParams(null) returns a fresh clone instead of crashing.
  • handler strips leading // from req.url so GET //evil.com/path cannot pivot the URL's origin; sanitizes the Host header to prevent crashes on malformed headers.
  • handler 405 error body no longer echoes url.pathname (minor information disclosure removed).

Additive options (opt-in, sensible defaults)

  • 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.maxOffset added (same default).

Behavior changes to be aware of (all fix footguns)

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.

3.1.0 — 2026-04-18

Additive features. No breaking changes.

  • returnFailedItem option on post / put / patch / delete and their make* batch builders. Sets ReturnValuesOnConditionCheckFailure: 'ALL_OLD'; the thrown ConditionalCheckFailedException carries the colliding item on its Item field (or the matching CancellationReason.Item inside a transaction).
  • {options: ...} sentinel for applyTransaction — position-independent descriptor carrying transaction-level knobs: clientRequestToken (idempotency), returnConsumedCapacity, returnItemCollectionMetrics.
  • explainTransactionCancellation(err, ...descriptors) in /batch. Maps TransactionCanceledException.CancellationReasons back to the input descriptors and returns {failures, message} | null. Paired with returnFailedItem to 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.txt included in the npm tarball.
  • CI lint step re-enabled (.github/dependabot.yml in .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.

3.0.0 — 2026-04-16

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's aws-sdk.
  • ESM-only, "type": "module", hand-written .d.ts sidecars next to every .js. No build step.
  • One data format — plain JS via lib-dynamodb middleware. The v2 Raw / DbRaw pair collapsed into a single Raw<T> bypass marker.
  • Options bags everywhere: put(item, {force: true}) instead of put(item, true); patch(key, patch, {delete, separator, arrayOps, conditions}).
  • Hooks renamed: prepareListParamsprepareListInput, updateParamsupdateInput. Hooks bag (options.hooks) is the canonical extension point.
  • Patch wire format: _delete / _separator (single underscore); configurable via policy.metaPrefix.
  • REST split: dynamodb-toolkit/rest-core is framework-agnostic; dynamodb-toolkit/handler is the node:http adapter. Koa / Express / Hono / Fetch / Lambda adapters live in separate packages.
  • Transaction auto-upgrade: single-op CRUD automatically upgrades to TransactWriteItems when checkConsistency returns extra descriptors.
  • Indirect-index second-hop for sparse GSIs with key-only projection.
  • Dropped: makeClient, getProfileName, specialTypes, converter, converterOptions — use @aws-sdk/credential-providers directly.

See Migration: v2 to v3 and SDK v2 to v3 cheat sheet.

v2.x and earlier

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.

Clone this wiki locally