-
Notifications
You must be signed in to change notification settings - Fork 0
Adapter: Indirect indices
Sparse Global Secondary Indexes (GSIs) that project keys only — the toolkit transparently performs a second-hop BatchGetItem against the base table to fetch full records.
See also (AWS JS SDK v3): Global Secondary Indexes (AWS docs) · Attribute projections ·
QueryCommand· KeyConditionExpression reference. Vocabulary: Concepts.
const adapter = new Adapter({
client: docClient,
table: 'planets',
keyFields: ['name'],
indirectIndices: {'climate-index': 1}
});The index name in indirectIndices must match the GSI name declared on the DynamoDB table.
When a read method receives params.IndexName matching an entry in indirectIndices, the Adapter:
- Issues the read against the GSI, requesting only the base-table key fields.
- Wraps each returned key in
Raw<TKey>(soprepareKeyis skipped on the second hop). - Issues a second-hop
BatchGetItemagainst the base table for the full records. - Reorders results to match the GSI order via
readOrderedListByKeys. - Reapplies projection + revive on the full records.
For getByKey, this is two GetItem calls. For getByKeys / getListByParams / getList, this is a Query / Scan followed by a chunked BatchGetItem.
New to DynamoDB's secondary-index concepts? Read Concepts → Why reach for a secondary index first — it covers the four use cases (different ordering, different access predicate, compound sort keys, sparse indexes) and shows where indirect indices fit.
A sparse GSI with Projection: KEYS_ONLY (or INCLUDE with a tiny attribute list) is much cheaper to maintain than one with Projection: ALL — DynamoDB only copies the base table key, GSI key, and (for INCLUDE) a declared short list. See Projection for the three projection types and how to pick.
For sparse access patterns (e.g., "find all planets where climate = 'frozen'"), this can save substantial write capacity and storage. The indirection is transparent to the caller — same Adapter API, two hops behind the scenes.
KeyCondition is a partition-key predicate plus an optional sort-key predicate (see Concepts → KeyConditionExpression and the AWS Query API reference). Three ways to build it:
-
adapter.buildKey(values, options?, params?)— the ergonomic surface. Takes{state: 'TX', rentalId: 42}keyed bykeyFieldsnames; uses the Adapter's declaredstructuralKeyto compose the prefix.{self: true}/{partial: 'abc'}for subtree / narrowed queries. Most indirect-index call sites should reach for this first. -
buildKeyCondition(input, params?)fromdynamodb-toolkit/expressions— the primitive. Takes a fully-prepared sort-key value string. Appropriate when your structural scheme isn't the toolkit'sstructuralKeycomposition (custom separators, non-keyFieldssort keys). -
Hand-written expression string — for one-off predicates where a builder adds no value. Allocate aliases via
ExpressionAttributeNames(#c,#s, …) andExpressionAttributeValues(:c,:s, …) — the same placeholder syntax every other expression uses. Pass the resultingparamstogetListByParams, or build it inside aprepareListInputhook sogetListcan drive it.
Pass {ignoreIndirection: true} to read the GSI projection as-is, skipping the second hop:
const partial = await adapter.getByKey(
{climate: 'frozen'},
undefined,
{params: {IndexName: 'climate-index'}, ignoreIndirection: true}
);
// → {name: 'Hoth'} (just the base-table key that the GSI stored)This is useful when you only need to know which items match (not their full content) — one hop, much faster. Common uses: existence checks, "give me the keys so I can delete them", cardinality counts where you'd also pass {needTotal: false, fields: keyFields}.
The ignoreIndirection option is available on every read method that honors indirectIndices: getByKey, getByKeys, getList, getListByParams, and the clone/move variants that take options.
// Schema:
// Base table 'planets' — partition key 'name'
// GSI 'climate-index' — partition key 'climate', sort key 'name', Projection KEYS_ONLY
const adapter = new Adapter({
client: docClient,
table: 'planets',
keyFields: ['name'],
indirectIndices: {'climate-index': 1}
});
// One call — toolkit does two hops behind the scenes
const allFrozen = await adapter.getListByParams(
{
IndexName: 'climate-index',
KeyConditionExpression: '#c = :c',
ExpressionAttributeNames: {'#c': 'climate'},
ExpressionAttributeValues: {':c': 'frozen'}
},
{limit: 50}
);
// → {data: [{name: 'Hoth', diameter: 7200, ...}, ...], offset, limit, total?}Note that the wiki convention of naming the index 'climate-index' (or elsewhere in the wiki, '-t-name-index') is the author's own convention, not a toolkit requirement. The toolkit only treats a name as "indirect" if it appears in the indirectIndices map — the name itself can be anything the table's GSI is declared as.
-
Two round-trips per read. For dense access patterns where the GSI would be hit constantly, projecting
ALL(no indirection) is usually faster overall. -
BatchGetItemper-call limit is 100 items. The toolkit chunks automatically and retriesUnprocessedKeyswith exponential backoff — see Batch and transactions. -
Order preservation across the second hop relies on
readOrderedListByKeys— the GSI ordering (sort key direction) is honored forgetList/getListByParams.getByKeysdoes not reorder (DynamoDB doesn't promise caller-order forBatchGetItem).
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