Skip to content

Adapter: CRUD methods

Eugene Lazutkin edited this page Jul 17, 2026 · 2 revisions

Adapter: CRUD methods

All methods are async. Single-write paths can auto-upgrade to TransactWriteItems — see Adapter: Transaction auto-upgrade.

See also (AWS JS SDK v3): GetCommand · PutCommand · UpdateCommand · DeleteCommand · QueryCommand / ScanCommand. Vocabulary: Concepts.

Reads

getByKey(key, fields?, options?)

getByKey(
  key: TKey | Raw<TKey>,
  fields?: string | string[] | null,
  options?: {consistent?: boolean; reviveItems?: boolean; ignoreIndirection?: boolean; params?: object}
): Promise<TItem | undefined>;

Returns undefined on miss. With {reviveItems: false} returns a Raw<TItem> (see Adapter: Raw marker). With an indirect index in params.IndexName, automatically does a second-hop BatchGet against the base table — see Adapter: Indirect indices.

const item = await adapter.getByKey({name: 'Hoth'}, ['name', 'climate']);

getByKeys(keys, fields?, options?)

getByKeys(keys: (TKey | Raw<TKey>)[], fields?, options?): Promise<TItem[]>;

Uses BatchGetItem with UnprocessedKeys retry. Missing keys are silently dropped; order is not preserved.

getList(options?, example?, index?)

getList(options?: ListOptions, example?: Partial<TItem>, index?: string): Promise<PaginatedResult<TItem>>;

Delegates list-input construction to the prepareListInput(example, index) hook, then forwards to getListByParams. The default hook returns {} (you'll typically override it to set IndexName/KeyConditionExpression for a sortable GSI — see KeyConditionExpression and Adapter: Hooks).

getListByParams(params, options?)

getListByParams(params: object, options?: ListOptions): Promise<PaginatedResult<TItem>>;

params is the prepared input (KeyConditionExpression, IndexName, etc.). options drives offset/limit/fields/filter/search/asOf/needTotal/reviveItems/ignoreIndirection/includeDescriptor.

interface ListOptions {
  offset?: number;                        // offset paging only — ignored by getPage*
  limit?: number;
  cursor?: string;                        // getPage* only — opaque token from the prior page
  descending?: boolean;
  sort?: string;                          // signed field → auto-picked index
  consistent?: boolean;
  fields?: string | string[];
  search?: string;                        // free-form text search over `searchable` mirrors
  filter?: Array<                         // structured clauses (Option D shape)
    | {field: string; op: 'eq'|'ne'|'lt'|'le'|'gt'|'ge'|'beg'|'ct'; value: string}
    | {field: string; op: 'in'|'btw'; value: string[]}
    | {field: string; op: 'ex'|'nx'}
  >;
  asOf?: Date | string | number;          // scope-freeze upper bound (requires createdAtField)
  includeDescriptor?: boolean;            // show the reserved descriptor row (otherwise hidden)
  caseSensitive?: boolean;                // search match
  needTotal?: boolean;                    // default true
  reviveItems?: boolean;
  ignoreIndirection?: boolean;
  retry?: RetryOptions;                   // indirect-hop BatchGetItem retry; defaults to adapter.retry
}

Returns {data: TItem[], offset, limit, total?}. With {needTotal: false}, total is omitted (no count round-trip).

getListUnder(partialKey, options?)

getListUnder(partialKey: Partial<TItem>, options?: ListOptions): Promise<PaginatedResult<TItem>>;

Shorthand for getListByParams(buildKey(partialKey), options) — the common "list descendants under a key" pattern. Equivalent to the children-default of buildKey. For {self: true} / {partial} shapes, compose buildKey + getListByParams directly.

const txVehicles = await adapter.getListUnder({state: 'TX'}, {limit: 50});

getPage(options?, example?, index?) / getPageByParams(params, options?)

getPage(options?: ListOptions, example?: Partial<TItem>, index?: string): Promise<CursorPage<TItem>>;
getPageByParams(params: object, options?: ListOptions): Promise<CursorPage<TItem>>;

Cursor-paged siblings of getList / getListByParams — DynamoDB's native LastEvaluatedKey paging. Constant cost per page regardless of depth (offset paging walks COUNT pages toward the offset) and never computes a total. Returns {data, limit, cursor?}; pass the opaque cursor back via options.cursor for the next page. With a filter, a page may come back short (even empty) and still carry a cursor — keep paging while cursor is present. Shares the whole list pipeline (filter / search / asOf / descriptor hiding / indirect second hop / revive).

let page = await adapter.getPage({limit: 50});
while (page.cursor) {
  page = await adapter.getPage({limit: 50, cursor: page.cursor});
}

buildKey(values, options?, params?) — compose a KeyConditionExpression

buildKey(
  values: Partial<TItem>,
  options?: {self?: boolean; partial?: string; indexName?: string},
  params?: object
): object;

Composes the KeyConditionExpression / ExpressionAttributeNames / ExpressionAttributeValues for a Query against the base table using the adapter's declared keyFields + structuralKey. Always list-oriented — defaults to descendants.

  • Defaultbegins_with(structuralKey, "TX|Dallas|") (children only; the trailing separator excludes the parent row).
  • {partial: 'Bui'}begins_with(structuralKey, "TX|Dallas|Bui"); narrows to items whose next-tier value starts with Bui.
  • {self: true}begins_with(structuralKey, "TX|Dallas") (no trailing separator); includes the row at the supplied key plus descendants. Requires that sibling values at the last supplied tier not be prefixes of each other (true by construction for zero-padded numeric keyFields; caller's responsibility for string values).
  • {partial} takes precedence over {self} when both are set.

Single-field keyFields (no structural key) emits pk equality; {self} / {partial} throw — there's no sort key to narrow against. Use getByKey for single-record reads instead.

Writes — single

post(item, options?) — create-only

post(item: TItem | Raw<TItem>, options?: {returnFailedItem?: boolean}): Promise<unknown>;

Adds an attribute_not_exists(<partition_key>) condition. Throws ConditionalCheckFailedException if the key already exists.

put(item, options?)

put(item: TItem | Raw<TItem>, options?: {force?: boolean; conditions?: ConditionClause[]; params?: object; returnFailedItem?: boolean}): Promise<unknown>;

Default adds an attribute_exists condition (write fails if missing). {force: true} skips the existence check (create-or-replace). Extra options.conditions are composed on top via buildCondition.

patch(key, patch, options?)

patch(
  key: TKey | Raw<TKey>,
  patch: Partial<TItem> | Raw<Partial<TItem>>,
  options?: {delete?: string[]; separator?: string; arrayOps?: ArrayOp[]; conditions?: ConditionClause[]; params?: object; returnFailedItem?: boolean}
): Promise<unknown>;

Builds an UpdateExpression. Key fields are stripped from patch automatically. options.delete lists paths to REMOVE. options.arrayOps adds atomic array operations — see Expressions: Update builder.

await adapter.patch(
  {name: 'Hoth'},
  {gravity: '1.5g'},
  {delete: ['old_field'], arrayOps: [{op: 'append', path: 'tags', values: ['frozen']}]}
);

delete(key, options?)

delete(key: TKey | Raw<TKey>, options?: {conditions?: ConditionClause[]; params?: object; returnFailedItem?: boolean}): Promise<unknown>;

DynamoDB's Delete is idempotent — it succeeds whether or not the item exists. Supply options.conditions to gate the delete on a predicate; the toolkit composes them into ConditionExpression via buildCondition.

Guarded delete example — refuse to delete unless a matching state flag is still set:

await adapter.delete(
  {name: 'Hoth'},
  {
    conditions: [
      {path: 'status', op: '=', value: 'archived'}
    ]
  }
);
// Throws ConditionalCheckFailedException if `status !== 'archived'`

options.params is merged in unchanged (e.g., set ReturnValues: 'ALL_OLD' to get the deleted item back as .Attributes).

returnFailedItem — introspect the item that lost the check

post, put, patch, and delete all accept {returnFailedItem: true}. When set, the toolkit adds ReturnValuesOnConditionCheckFailure: 'ALL_OLD' to the request — so when the existence check (or any options.conditions) fails, the thrown ConditionalCheckFailedException carries the current state of the item on its Item field. Useful for "tell me what I collided with" messages.

try {
  await adapter.post({name: 'Hoth', climate: 'frozen'}, {returnFailedItem: true});
} catch (err) {
  if (err.name === 'ConditionalCheckFailedException') {
    console.error('Collided with existing item:', err.Item);
    // → {name: 'Hoth', climate: 'temperate', diameter: 7200, ...}
  }
}

The same flag also works inside transaction descriptors (makePost / makePut / makePatch / makeDelete) — when the transaction aborts, the failing CancellationReason on the thrown TransactionCanceledException carries the item.

options.params escape hatch: setting ReturnValuesOnConditionCheckFailure: 'ALL_OLD' by hand through options.params is equivalent; the typed option just saves the import and is easier to discover.

clone(key, mapFn?, options?) and move(key, mapFn?, options?)

clone(key, mapFn?: (item: TItem) => TItem, options?: {force?: boolean; params?: object}): Promise<TItem | undefined>;
move(key, mapFn?, options?): Promise<TItem | undefined>;

clone reads the source, applies mapFn, writes the result (post if force is unset, put({force: true}) otherwise). move does the same plus deletes the original — bundled into a single TransactWriteItems call for atomicity.

mapFn is optional; the default is identity — the item is cloned/moved as-is. You almost always want to pass one, because cloning a key-addressable item back to the same key with post (the default) throws ConditionalCheckFailedException. The common patterns are:

  • Rewrite the key (e.g. item => ({...item, name: item.name + '-copy'})).
  • Pass {force: true} if you actually want to overwrite the destination.

Returns the cloned/moved item (post-mapFn), or undefined when the source is missing.

// Rename + duplicate:
const copy = await adapter.clone(
  {name: 'Tatooine'},
  item => ({...item, name: 'Tatooine-copy'})
);

// Move with key rewrite — atomic put + delete:
await adapter.move(
  {name: 'Tatooine'},
  item => ({...item, name: 'Jakku'})
);

// Overwrite destination (use with care — `force` bypasses the existence check):
await adapter.clone({name: 'Hoth'}, item => ({...item, name: 'Hoth-mirror'}), {force: true});

edit(key, mapFn, options?) — read-modify-update

edit(
  key: TKey | Raw<TKey>,
  mapFn: (item: TItem) => TItem | undefined,
  options?: {readFields?: string[]; allowKeyChange?: boolean; expectedVersion?: number; params?: object}
): Promise<TItem | undefined>;

Reads the item, runs mapFn, diffs the result against the original, and dispatches an UpdateCommand with the computed SET / REMOVE ops. Returns the revived post-edit item. mapFn returning undefined short-circuits with no write. If mapFn returns an unchanged shape (identity / no diff), the write is skipped entirely — no WCU charged.

  • Key-field mutations — by default throw KeyFieldChanged. Pass {allowKeyChange: true} to auto-promote the edit into a move (atomic put-to-new-key + delete-from-old-key).
  • versionField interaction — the OC condition is auto-applied using the version observed on the read; stale state rejects with a CCF.
  • readFields — optional projection for the initial read (saves RCU when the mapFn only touches a few fields).

Good for "update only the fields that actually changed" flows where computing the diff beats rewriting the whole item.

editListByParams(params, mapFn, options?)

editListByParams(
  params: object,
  mapFn: (item: TItem) => TItem | undefined,
  options?: MassOptions
): Promise<MassOpResult>;

Resumable batch edit across every item matching params (Query or Scan). Same per-item semantics as edit. Honors options.asOf / options.filter / options.maxItems / options.resumeToken for scope-freeze and incremental runs. See Adapter: Mass methods for the mass-op envelope.

Type tag methods

typeOf(item)

typeOf(item: Partial<TItem> | undefined | null): string | number | undefined;

Classifies an item. Priority:

  1. typeDiscriminator.name value when present on the item — returned as-is (coerced to string).
  2. typeLabels[depth - 1] where depth is the count of contiguous-from-start defined keyFields on the item, when typeLabels is declared.
  3. Raw depth number when no typeLabels is declared.
  4. undefined when the item carries no recognised type-signalling fields.

With typeField also declared, the built-in prepare step stamps this label on write — so typeOf reads back what it wrote. Combined pattern: typeField: 'kind', typeDiscriminator: 'kind'.

applyFilter(params, clauses)

applyFilter(params: object, clauses: FilterClause[]): object;

Compiles parsed ?<op>-<field>=<value> clauses (from the REST layer's parseFilter) into FilterExpression / KeyConditionExpression on params. Validates against the adapter's filterable allowlist; coerces values to the declared type; auto-promotes index-compatible clauses into KeyConditionExpression when the target (base table or params.IndexName) has matching pk/sk.

Throws BadFilterField / BadFilterOp when a clause names an unlisted field or uses an unallowed op. Typically called internally by getListByParams (via options.filter); call it directly only when building custom list paths outside the adapter's standard list flow.

Clone this wiki locally