Typed DynamoDB client for MoonBit with AWS Signature V4
moon add Mr-Shi123/moondynamoimport {
"Mr-Shi123/moondynamo"
}moon version --all
moon check --target js --deny-warn
moon build --target js
moon test --target js --deny-warndocker run --rm -p 8000:8000 amazon/dynamodb-local
# in another terminal
moon run cmd/main --target jsnode scripts/mock-dynamodb.mjs
# in another terminal
moon run cmd/main --target jslet 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)try db.create_table("Books", "pk") |> ignore catch {
@moondynamo.DynamoError::Service(_, "ResourceInUseException", _, _) => ()
error => raise error
}
db.describe_table("Books") |> ignorelet 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)
}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")
}let selected = db.get_item(
"Books",
Map([("pk", @moondynamo.Text("book#1"))]),
projection_expression="title, #st",
expression_attribute_names=Map([("#st", "status")]),
)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
}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")
}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)
}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
}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
}
}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")
}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)")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
}| MoonBit | DynamoDB JSON | Notes |
|---|---|---|
| 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>"]} |
pub(all) suberror DynamoError {
InvalidConfig(String)
InvalidResponse(String)
Service(Int, String, String, String?)
}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"))
}pub(all) suberror DynamoError {
InvalidConfig(String)
InvalidResponse(String)
Service(Int, String, String, String?)
}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)pub(all) struct BatchGetResult {
items : Array[Map[String, AttributeValue]]
unprocessed_keys : Array[Map[String, AttributeValue]]
} derive(Eq, Debug)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]) -> BatchGetResultasync fn Client::batch_write_item(self : Client, table : String, requests : Array[WriteRequest]) -> Array[WriteRequest]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]) -> Unitasync 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]?async fn Client::list_tables(self : Client, limit? : Int, exclusive_start_table_name? : String) -> ListTablesResultasync 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]) -> Unitasync 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) -> QueryResultasync 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) -> QueryResultasync 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]?pub(all) struct Credentials {
access_key_id : String
secret_access_key : String
session_token : String?
}pub(all) struct QueryResult {
items : Array[Map[String, AttributeValue]]
last_evaluated_key : Map[String, AttributeValue]?
}pub(all) enum WriteRequest {
Put(Map[String, AttributeValue])
Delete(Map[String, AttributeValue])
} derive(Eq, Debug)fn parse_response(status : Int, headers : Map[String, String], body : String) -> Json raise DynamoErrorfn prepare_request(config : Config, action : String, payload : Json, timestamp : String) -> PreparedRequest raise DynamoError