moondynamo

    Typed DynamoDB client for MoonBit with AWS Signature V4

    dynamodb
    aws
    database
    Download zip
    Author
    Version
    0.4.2
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    51

    #MoonDynamo

    CI License: Apache-2.0

    A typed Amazon DynamoDB client for MoonBit. MoonDynamo talks the DynamoDB JSON API directly and signs every request with AWS Signature Version 4, so it works against real AWS endpoints and DynamoDB Local alike — useful in MoonBit applications and in local integration tests.

    #Features

    • Core operations: CreateTable, DescribeTable, ListTables, PutItem, GetItem, UpdateItem, DeleteItem, Query, and Scan.
    • Batch operations: batch_get_item reads up to 100 keys in one round trip and batch_write_item applies up to 25 puts/deletes; whatever DynamoDB could not process comes back for an explicit re-drive.
    • Every DynamoDB attribute type: S, N, B, BOOL, NULL, L, M, SS, NS, and BS map to one typed AttributeValue enum. Numbers stay as decimal strings end to end, so no precision is lost through floating point.
    • Conditional writes and filtered queries: you write condition, key-condition, and filter expressions; the client sends typed expression-attribute values with them.
    • Selective reads: get_item, batch_get_item, query, and scan can return only the fields named by projection_expression, including reserved names via expression_attribute_names.
    • Secondary indexes: point query or scan at any local or global secondary index with index_name, read oldest-first with descending, and force strongly consistent base-table reads with consistent_read.
    • Reserved-word-safe expressions: pass #name placeholders through expression_attribute_names, so attributes called status, size, or timestamp are reachable from any expression.
    • Conditional updates: update_item runs SET / ADD / REMOVE expressions, can require a condition_expression before writing, and can return affected attributes via ReturnValues.
    • Pagination: Query returns LastEvaluatedKey; ListTables returns LastEvaluatedTableName. Use list_all_tables to collect every table name automatically.
    • Structured errors: service failures raise DynamoError::Service carrying the HTTP status, the error code, the message, and the x-amzn-RequestId of the failed call.
    • Automatic retries: throttling and transient server failures — 429/500/503 statuses and codes like ProvisionedThroughputExceededException — are retried with capped exponential backoff (2 retries, 100 ms doubling up to 2 s) before an error ever reaches your code.
    • Local-first configuration: endpoint, region, and credentials (including session tokens) are all explicit, so pointing at DynamoDB Local takes no extra setup.

    #Requirements

    • MoonBit with moonc 0.10.19 or newer — verified with moon 0.1.20261102 / moonc 0.10.19. MoonDynamo targets the JavaScript backend and runs on Node.js.
    • Optional: Java 8+ to run DynamoDB Local, or Node.js 18+ for the bundled mock server.

    #Installation

    From your project root:

    moon add Mr-Shi123/moondynamo

    Then import it in the moon.pkg of the package that uses it:

    import { "Mr-Shi123/moondynamo" }

    The package is available under the default alias @moondynamo.

    #Quick start

    Check the compiler version, then check, build, and test from this repository's root:

    moon version --all moon check --target js --deny-warn moon build --target js moon test --target js --deny-warn

    The bundled end-to-end demo (cmd/main/main.mbt) creates two tables and lists them across pages, writes a book, reads it back with full, projected, and batch reads, conditionally updates its page count, confirms a stale-value update is rejected, runs a filtered query and scan, deletes the item, and confirms it is gone. The tables are left in place so the demo can be re-run.

    Option A — DynamoDB Local (recommended):

    docker run --rm -p 8000:8000 amazon/dynamodb-local # in another terminal moon run cmd/main --target js

    Option B — the bundled mock — a zero-dependency fixture (scripts/mock-dynamodb.mjs) that listens on 127.0.0.1:8000 without Docker. It validates each request's content type, action, and body, and independently recomputes every SigV4 signature before responding, then shuts down once the expected seventeen-step scenario has passed:

    node scripts/mock-dynamodb.mjs # in another terminal moon run cmd/main --target js

    Both options print MoonDynamo local example passed on success.

    #Usage

    The examples below assume a service at 127.0.0.1:8000 (DynamoDB Local or the mock) with throwaway local credentials — never embed real credentials in source code. All calls are async, so the snippets live inside an async fn main.

    #Configure a client

    let config : @moondynamo.Config = {
    endpoint: "http://127.0.0.1:8000",
    region: "us-east-1",
    credentials: {
    access_key_id: "local",
    secret_access_key: "local",
    session_token: None,
    },
    }
    let db = @moondynamo.Client::new(config)

    endpoint is an http:// or https:// origin without a path (an ASCII DNS name or an IPv4 address); the client lowercases the host and drops default ports before signing. Temporary AWS credentials go in session_token.

    #Create a table

    try db.create_table("Books", "pk") |> ignore catch {
    @moondynamo.DynamoError::Service(_, "ResourceInUseException", _, _) => ()
    error => raise error
    }
    db.describe_table("Books") |> ignore

    create_table makes an on-demand (pay-per-request) table with a string partition key and, via sort_key?, an optional string sort key. Catching ResourceInUseException makes re-runs idempotent.

    #List tables

    let first = db.list_tables(limit=2)
    for name in first.table_names {
    println(name)
    }
    if first.last_evaluated_table_name is Some(cursor) {
    let next = db.list_tables(limit=2, exclusive_start_table_name=cursor)
    for name in next.table_names {
    println(name)
    }
    }

    let all_tables = db.list_all_tables(limit=2)
    for name in all_tables {
    println(name)
    }

    limit accepts 1–100 names per request. list_all_tables follows the returned marker until it is None and collects the names in memory; use list_tables when you want to process pages one at a time. The client rejects an out-of-range limit before sending a request. See the AWS ListTables API.

    #Write and read items

    let item : @moondynamo.Item = Map([
    ("pk", @moondynamo.Text("book#1")),
    ("title", @moondynamo.Text("The MoonBit Book")),
    ("pages", @moondynamo.Number("128")),
    ("tags", @moondynamo.StringSet(["moonbit", "database"])),
    ])
    db.put_item("Books", item)

    let saved = db.get_item(
    "Books",
    Map([("pk", @moondynamo.Text("book#1"))]),
    consistent_read=true,
    )
    guard saved is Some(book) else { fail("GetItem returned no item") }
    guard book.get("title") == Some(@moondynamo.Text("The MoonBit Book")) else {
    fail("GetItem returned an unexpected item")
    }

    get_item returns None when the key does not exist.

    To receive only selected attributes, pass a projection. Use a #name placeholder for reserved words such as status:

    let selected = db.get_item(
    "Books",
    Map([("pk", @moondynamo.Text("book#1"))]),
    projection_expression="title, #st",
    expression_attribute_names=Map([("#st", "status")]),
    )

    The result contains only the requested attributes. See the AWS GetItem API.

    #Conditional writes

    try db.put_item(
    "Books",
    item,
    condition_expression="attribute_not_exists(pk)",
    ) catch {
    @moondynamo.DynamoError::Service(
    _,
    "ConditionalCheckFailedException",
    message,
    _,
    ) => println("conditional write rejected: \{message}")
    error => raise error
    }

    You write the expression string; typed placeholder values travel in expression_attribute_values? — for example Map([(":max", @moondynamo.Number("500"))]).

    #Update items

    let updated = db.update_item(
    "Books",
    Map([("pk", @moondynamo.Text("book#1"))]),
    "SET pages = :pages",
    expression_attribute_values=Map([
    (":pages", @moondynamo.Number("200")),
    (":expected", @moondynamo.Number("128")),
    ]),
    condition_expression="pages = :expected",
    return_values=AllNew,
    )
    guard updated is Some(book) && book.get("pages") == Some(@moondynamo.Number("200")) else {
    fail("UpdateItem did not apply the new page count")
    }

    The update expression is a DynamoDB UpdateExpression string, so SET / ADD / REMOVE clauses and if_not_exists(...) all work as documented by AWS. condition_expression checks the current item before the update; when it is false, DynamoDB raises ConditionalCheckFailedException and leaves the item unchanged. The condition and update share expression_attribute_values and expression_attribute_names. With return_values (AllNew, AllOld, UpdatedNew, UpdatedOld, or Nothing) the returned Item? carries the affected attributes; without it you get None. See the AWS UpdateItem API.

    #Reserved words and name placeholders

    DynamoDB reserves hundreds of words (status, size, name, timestamp, …), so expressions must reach such attributes through #name placeholders. get_item, batch_get_item, put_item, update_item, delete_item, query, and scan all accept expression_attribute_names?; get_item, batch_get_item, query, and scan also take projection_expression?:

    let result = db.query(
    "Books",
    "pk = :pk",
    Map([
    (":pk", @moondynamo.Text("user#1")),
    (":st", @moondynamo.Text("active")),
    (":max", @moondynamo.Number("500")),
    ]),
    filter_expression="#st = :st AND pages <= :max",
    expression_attribute_names=Map([("#st", "status")]),
    projection_expression="title",
    )
    for book in result.items {
    // only the title of active books with at most 500 pages
    ignore(book)
    }

    projection_expression controls the returned fields; it does not reduce the read capacity used for a table query. The pagination marker remains in last_evaluated_key even when key attributes are not projected. See the AWS Query API.

    #Query a secondary index

    query accepts index_name, so the same key-condition style works against local and global secondary indexes. descending flips the read order (ScanIndexForward=false); consistent_read forces a strongly consistent read (base table only — DynamoDB does not offer it on global indexes):

    let recent = db.query(
    "Books",
    "#st = :st",
    Map([(":st", @moondynamo.Text("active"))]),
    index_name="status-index",
    descending=true,
    expression_attribute_names=Map([("#st", "status")]),
    )
    for book in recent.items {
    ignore(book) // newest active books first
    }

    #Scan a table

    scan reads every item of a table or index, with the same expression, name-placeholder, projection, and paging options as query:

    let mut exclusive_start_key : @moondynamo.Item? = None
    while true {
    let page = db.scan(
    "Books",
    filter_expression="pages <= :max",
    expression_attribute_values=Map([(":max", @moondynamo.Number("500"))]),
    projection_expression="title, #st",
    expression_attribute_names=Map([("#st", "status")]),
    exclusive_start_key?,
    limit=100,
    )
    for book in page.items {
    ignore(book) // do something with each matching item
    }
    match page.last_evaluated_key {
    Some(key) => exclusive_start_key = Some(key)
    None => break
    }
    }

    The filter applies after limit is counted, so a page can come back empty while last_evaluated_key is still set — keep following it until None.

    #Batch reads and writes

    batch_get_item fetches up to 100 keys per call; keys that do not exist are simply absent from items, while keys DynamoDB could not process (usually throttled) come back in unprocessed_keys for a follow-up call. batch_write_item applies up to 25 puts and deletes per call and returns the requests it could not process:

    let batch = db.batch_get_item("Books", [key1, key2, key3])
    for item in batch.items {
    ignore(item)
    }
    let pending = batch.unprocessed_keys // re-drive these, if any

    let selected_batch = db.batch_get_item(
    "Books",
    [key1, key2],
    projection_expression="pk, title, #st",
    expression_attribute_names=Map([("#st", "status")]),
    )
    // Include the primary key in the projection to match results to input keys.
    // Re-drive selected_batch.unprocessed_keys with the same projection options.

    let leftover = db.batch_write_item("Books", [
    Put(
    Map([
    ("pk", @moondynamo.Text("book#4")),
    ("pages", @moondynamo.Number("80")),
    ]),
    ),
    Delete(key1),
    ])
    guard leftover.is_empty() else {
    fail("re-drive the requests in leftover")
    }

    A batch write is not atomic and cannot touch the same item twice in one call. See the AWS BatchGetItem API for projection and result-order details.

    #Query with pagination

    let mut exclusive_start_key : @moondynamo.Item? = None
    let mut pages = 0
    while true {
    let page = db.query(
    "Books",
    "pk = :pk",
    Map([(":pk", @moondynamo.Text("book#1"))]),
    exclusive_start_key?,
    limit=1,
    )
    pages = pages + page.items.length()
    match page.last_evaluated_key {
    Some(key) => exclusive_start_key = Some(key)
    None => break
    }
    }
    println("query matched \{pages} item(s)")

    The small limit is just to force LastEvaluatedKey continuation in a tiny table; drop it to get full pages. Keep calling until last_evaluated_key is None.

    #Error handling

    Every operation can raise; catch and inspect what you care about. Transient failures are already retried inside the client (see Automatic retries above), so what surfaces is usually a real problem:

    try db.describe_table("Books") |> ignore catch {
    @moondynamo.DynamoError::Service(status, code, message, request_id) =>
    println("HTTP \{status} \{code}: \{message} (request \{request_id})")
    error => raise error
    }

    #Attribute value types

    Item is an alias for Map[String, AttributeValue]. The full wire-format coverage:

    MoonBitDynamoDB JSONNotes
    Text("hi"){"S": "hi"}UTF-8 string
    Number("128"){"N": "128"}decimal text — no float rounding
    Binary(b){"B": "<base64>"}Bytes encoded as base64
    BoolValue(true){"BOOL": true}
    NullValue{"NULL": true}
    ListValue([...]){"L": [...]}nested values
    ObjectValue({...}){"M": {...}}nested document
    StringSet(["a"]){"SS": ["a"]}
    NumberSet(["1"]){"NS": ["1"]}decimal text
    BinarySet([b]){"BS": ["<base64>"]}

    AttributeValue derives Eq, so results are easy to assert on in tests.

    #Error model

    pub(all) suberror DynamoError {
    InvalidConfig(String)
    InvalidResponse(String)
    Service(Int, String, String, String?)
    }

    • InvalidConfig — bad endpoint, empty region or credentials, malformed timestamp.
    • InvalidResponse — a 2xx response the client could not parse.
    • Service(status, code, message, request_id) — a non-2xx answer. The code is taken from the __type suffix (or the code field), e.g. ConditionalCheckFailedException; the message falls back from message to Message; the request ID comes from the x-amzn-RequestId header. Even a non-JSON error body surfaces as Service with the code UnknownServiceError, so callers always get a typed error.

    #How it is tested

    • 48 unit tests (moon test --target js) cover attribute round-trips, request construction and signing, expression payloads with name placeholders, projected GetItem, BatchGetItem, Query, and Scan requests, conditional UpdateItem payloads and response parsing, the ReturnValues mapping, index and scan payloads, batch payloads and limits with unprocessed-key/request parsing, ListTables paging and validation, response and error parsing, and the retry decision predicate.
    • A self-verifying HTTP fixture (scripts/mock-dynamodb.mjs) exercises real HTTP round trips and independently recomputes every SigV4 signature, so signing drift cannot pass silently. Its scenario includes two-page list_all_tables, projected GetItem, BatchGetItem, Query, and Scan calls, and both successful and rejected conditional UpdateItem calls. CI waits for the fixture to complete the entire scenario.
    • Verified against DynamoDB Local 1.25.1 end to end: CreateTable → DescribeTable → PutItem → GetItem → UpdateItem → Query → Scan → DeleteItem → GetItem, including conditional-write rejection with a real ConditionalCheckFailedException, multi-page Query walks through LastEvaluatedKey, #name placeholders for reserved-word attributes, a Query against a real global secondary index (index_name + descending) plus a filtered, paged Scan, and batch_get_item / batch_write_item runs confirming that missing keys are absent (not unprocessed) while processed writes take effect.

    #Platform status

    • JavaScript — compile, unit tests, and live HTTP calls all verified.
    • Native — moon check passes. Running natively on Windows is currently blocked by a MinGW incompatibility in moonbitlang/async's C sources (that dependency documents MSVC for its Windows event loop); native runtime testing is pending.

    #Architecture

    Two thin layers:

    • Protocol layer (protocol.mbt, attribute.mbt) — pure functions with no I/O: they normalize the endpoint, build the DynamoDB JSON payload, sign it with kaashyapan/sigv4, and parse responses into typed values or structured errors. Purity makes the whole wire format deterministic to test.
    • Client layer (client.mbt) — a small async wrapper that sends requests through moonbitlang/async's HTTP client.

    #Current limitations

    • Credentials come from the caller — no default credential chain, no automatic refresh.
    • The endpoint must be an origin without a path; no IPv6 hostnames yet.
    • Value placeholders (:name) and name placeholders (#name) are supported; programmatic expression builders are not (yet).
    • The retry policy is fixed — 2 retries, 100 ms doubling up to 2 s, no jitter or per-client tuning.
    • No Transact*, Streams, PartiQL, or schema modeling yet. The batch helpers are single-table wrappers over the multi-table AWS wire format.
    • create_table only creates on-demand tables with string keys; tables with indexes or other shapes need to be created out of band.

    #License

    Apache-2.0. Dependencies — moonbitlang/async, moonbitlang/x, kaashyapan/sigv4 — are all Apache-2.0 as well.

    Item

    type Item = Map[String, AttributeValue]

    Mutable linked hash map that maintains the order of insertion, not thread safe.

    Example

    test {
    let map = { 3: "three", 8: "eight", 1: "one" }
    @test.assert_eq(map.get(2), None)
    @test.assert_eq(map.get(3), Some("three"))
    map.set(3, "updated")
    @test.assert_eq(map.get(3), Some("updated"))
    }

    CodecError

    pub(all) suberror CodecError {
    InvalidAttribute(String)
    }

    DynamoError

    pub(all) suberror DynamoError {
    InvalidConfig(String)
    InvalidResponse(String)
    Service(Int, String, String, String?)
    }

    AttributeValue

    pub(all) enum AttributeValue {
    Text(String)
    Number(String)
    Binary(Bytes)
    BoolValue(Bool)
    NullValue
    ListValue(Array[AttributeValue])
    ObjectValue(Map[String, AttributeValue])
    StringSet(Array[String])
    NumberSet(Array[String])
    BinarySet(Array[Bytes])
    } derive(Eq,
    Debug
    )

    DynamoDB's wire attribute types. Numbers stay as decimal text to avoid losing precision when they pass through JSON.

    AttributeValue::equal

    AttributeValue::not_equal

    fn AttributeValue::not_equal(x : AttributeValue, y : AttributeValue) -> Bool

    BatchGetResult

    pub(all) struct BatchGetResult {
    items : Array[Map[String, AttributeValue]]
    unprocessed_keys : Array[Map[String, AttributeValue]]
    } derive(Eq,
    Debug
    )

    Result of a batch_get_item call: the items DynamoDB did return, plus the keys it could not process (usually throttled) for the caller to re-drive in a follow-up call.

    BatchGetResult::equal

    BatchGetResult::not_equal

    fn BatchGetResult::not_equal(x : BatchGetResult, y : BatchGetResult) -> Bool

    Client

    pub struct Client {
    config : Config
    }

    Client::batch_get_item

    async fn Client::batch_get_item(self : Client, table : String, keys : Array[Map[String, AttributeValue]], consistent_read? : Bool, projection_expression? : String, expression_attribute_names? : Map[String, String]) -> BatchGetResult

    Read up to 100 items in one round trip. Keys that do not exist are simply absent from items; keys DynamoDB could not process (usually throttled) come back in unprocessed_keys — re-drive them in a follow-up call. Consistent reads can be requested for the base table with consistent_read. Use projection_expression to select attributes and expression_attribute_names for reserved attribute names. Include key attributes in the projection if results must be matched to input keys.

    Client::batch_write_item

    async fn Client::batch_write_item(self : Client, table : String, requests : Array[WriteRequest]) -> Array[WriteRequest]

    Apply up to 25 puts and deletes in one round trip. Requests DynamoDB could not process come back as the return value — re-drive them in a follow-up call. A batch write is not atomic and cannot update the same item twice in one call; use transactions when those guarantees matter.

    Client::create_table

    async fn Client::create_table(self : Client, table : String, partition_key : String, sort_key? : String) ->
    Json

    Create an on-demand table with string partition and optional sort keys.

    Client::delete_item

    async fn Client::delete_item(self : Client, table : String, key : Map[String, AttributeValue], condition_expression? : String, expression_attribute_values? : Map[String, AttributeValue], expression_attribute_names? : Map[String, String]) -> Unit

    Client::describe_table

    async fn Client::describe_table(self : Client, table : String) ->
    Json

    Client::get_item

    async fn Client::get_item(self : Client, table : String, key : Map[String, AttributeValue], consistent_read? : Bool, projection_expression? : String, expression_attribute_names? : Map[String, String]) -> Map[String, AttributeValue]?

    Read an item by key. Use projection_expression to request only selected attributes and expression_attribute_names for reserved attribute names.

    Client::list_all_tables

    async fn Client::list_all_tables(self : Client, limit? : Int) -> Array[String]

    List every table by following LastEvaluatedTableName until DynamoDB returns no continuation. limit controls the number of names per request (1–100), not the total number returned.

    Client::list_tables

    async fn Client::list_tables(self : Client, limit? : Int, exclusive_start_table_name? : String) -> ListTablesResult

    List table names in the configured account and region. Pass the returned last_evaluated_table_name as exclusive_start_table_name to fetch the next page. DynamoDB returns at most 100 names per page.

    Client::new

    fn Client::new(config : Config) -> Client

    Client::put_item

    async fn Client::put_item(self : Client, table : String, item : Map[String, AttributeValue], condition_expression? : String, expression_attribute_values? : Map[String, AttributeValue], expression_attribute_names? : Map[String, String]) -> Unit

    Client::query

    async fn Client::query(self : Client, table : String, key_condition_expression : String, expression_attribute_values : Map[String, AttributeValue], filter_expression? : String, index_name? : String, descending? : Bool, consistent_read? : Bool, exclusive_start_key? : Map[String, AttributeValue], limit? : Int, expression_attribute_names? : Map[String, String], projection_expression? : String) -> QueryResult

    Query matching items, optionally requesting only selected attributes with projection_expression.

    Client::scan

    async fn Client::scan(self : Client, table : String, filter_expression? : String, expression_attribute_values? : Map[String, AttributeValue], expression_attribute_names? : Map[String, String], index_name? : String, consistent_read? : Bool, exclusive_start_key? : Map[String, AttributeValue], limit? : Int, projection_expression? : String) -> QueryResult

    Read the items of a table or, via index_name, of a secondary index, page by page. filter_expression drops items server-side; because the filter applies after limit is counted, a page can come back empty while last_evaluated_key is still present — keep following it until None. Use projection_expression to request only selected attributes.

    Client::update_item

    async fn Client::update_item(self : Client, table : String, key : Map[String, AttributeValue], update_expression : String, expression_attribute_values? : Map[String, AttributeValue], expression_attribute_names? : Map[String, String], condition_expression? : String, return_values? : ReturnValues) -> Map[String, AttributeValue]?

    Apply an UpdateExpression (e.g. SET pages = :p, ADD views :one, REMOVE note) to the item identified by key. Pass return_values to get the resulting attributes back; without it the reply carries none and the result is None. Pass condition_expression to update only when the current item meets a condition; a failed condition raises a service error.

    Config

    pub(all) struct Config {
    endpoint : String
    region : String
    credentials : Credentials
    }

    endpoint is an origin without a trailing slash, using an ASCII DNS name or IPv4 address, for example https://dynamodb.us-east-1.amazonaws.com or http://127.0.0.1:8000.

    Credentials

    pub(all) struct Credentials {
    access_key_id : String
    secret_access_key : String
    session_token : String?
    }

    AWS credentials supplied by the caller. Temporary credentials include a session token; the client signs that token with every request.

    ListTablesResult

    pub(all) struct ListTablesResult {
    table_names : Array[String]
    last_evaluated_table_name : String?
    }

    PreparedRequest

    pub(all) struct PreparedRequest {
    url : String
    headers : Map[String, String]
    body : String
    }

    QueryResult

    pub(all) struct QueryResult {
    items : Array[Map[String, AttributeValue]]
    last_evaluated_key : Map[String, AttributeValue]?
    }

    ReturnValues

    pub(all) enum ReturnValues {
    Nothing
    AllOld
    AllNew
    UpdatedOld
    UpdatedNew
    } derive(Eq,
    Debug
    )

    Which attributes UpdateItem should return, mirroring DynamoDB's ReturnValues parameter.

    ReturnValues::equal

    ReturnValues::not_equal

    fn ReturnValues::not_equal(x : ReturnValues, y : ReturnValues) -> Bool

    WriteRequest

    pub(all) enum WriteRequest {
    Put(Map[String, AttributeValue])
    Delete(Map[String, AttributeValue])
    } derive(Eq,
    Debug
    )

    One write operation inside a batch_write_item call: either overwrite the item under Put(item) or remove the item under Delete(key).

    WriteRequest::equal

    WriteRequest::not_equal

    fn WriteRequest::not_equal(x : WriteRequest, y : WriteRequest) -> Bool

    decode_attribute

    fn decode_attribute(json :
    Json
    ) -> AttributeValue raise CodecError

    decode_item

    fn decode_item(json :
    Json
    ) -> Map[String, AttributeValue] raise CodecError

    encode_attribute

    fn encode_attribute(value : AttributeValue) ->
    Json

    encode_item

    fn encode_item(item : Map[String, AttributeValue]) ->
    Json

    parse_response

    fn parse_response(status : Int, headers : Map[String, String], body : String) ->
    Json
    raise DynamoError

    Decode a DynamoDB response. Non-2xx responses become typed service errors, preserving the service code and request ID for callers.

    prepare_request

    fn prepare_request(config : Config, action : String, payload :
    Json
    , timestamp : String) -> PreparedRequest raise DynamoError

    Prepare a signed DynamoDB JSON request with a caller-supplied UTC timestamp (YYYYMMDDTHHMMSSZ). Supplying the time keeps this layer pure.