Expand description
Utilities for working with DynamoDB, particularly focusing on an opinionated approach to modeling data using single-table design.
In order to effectively use this library, you should first have completed a single-table design, identifying your entities and the access patterns that you will need to implement.
§Tables
Tables are defined via a trait implementation. To do this, we’ll need to expose
a means for modyne to access the table’s name, a properly configured client,
and the relevant keys for the table.
Below, we define a database that has one global secondary index in addition to the default primary key.
use modyne::{keys, Table};
struct Database {
table_name: String,
client: aws_sdk_dynamodb::Client,
}
impl Table for Database {
type PrimaryKey = keys::Primary;
type IndexKeys = keys::Gsi1;
fn table_name(&self) -> &str {
&self.table_name
}
fn client(&self) -> &aws_sdk_dynamodb::Client {
&self.client
}
}§Primary keys and indexes
While a default primary key and generic indexes are provided, it is possible to define your own primary key or index if desired. These types must be serde-serializable.
use modyne::keys::{
GlobalSecondaryIndexDefinition, IndexKey, Key, KeyDefinition,
PrimaryKey, PrimaryKeyDefinition, SecondaryIndexDefinition
};
#[derive(Debug, serde::Serialize)]
struct SessionToken {
session_token: String,
}
impl PrimaryKey for SessionToken {
const PRIMARY_KEY_DEFINITION: PrimaryKeyDefinition = PrimaryKeyDefinition {
hash_key: "session_token",
range_key: None,
};
}
impl Key for SessionToken {
const DEFINITION: KeyDefinition =
<Self as PrimaryKey>::PRIMARY_KEY_DEFINITION.into_key_definition();
}
#[derive(Debug, serde::Serialize)]
struct UserIndex {
user_id: String,
}
impl IndexKey for UserIndex {
const INDEX_DEFINITION: SecondaryIndexDefinition = GlobalSecondaryIndexDefinition {
index_name: "user_index",
hash_key: "user_id",
range_key: None,
}.into_index();
}§Entities
Entities are the heart of the data model. An instance of an entity represents a single item in a DynamoDB table. An entity will always have the same primary key as the assoicated table, but may also participate in zero or more secondary indexes.
For more information on setting up an entity, see EntityDef and
Entity.
use modyne::{keys, Entity, EntityDef};
#[derive(Debug, EntityDef, serde::Serialize, serde::Deserialize)]
struct Session {
user_id: String,
session_token: String,
}
impl Entity for Session {
type KeyInput<'a> = &'a str;
type Table = Database;
type IndexKeys = keys::Gsi1;
fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
keys::Primary {
hash: format!("SESSION#{}", input),
range: format!("SESSION#{}", input),
}
}
fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
keys::FullKey {
primary: Self::primary_key(&self.session_token),
indexes: keys::Gsi1 {
hash: format!("USER#{}", self.user_id),
range: format!("SESSION#{}", self.session_token),
},
}
}
}Entities can be interacted with through utility methods on the EntityExt
trait.
use modyne::EntityExt;
let mk_session = || Session {
session_token: String::from("session-1"),
user_id: String::from("user-1"),
};
let upsert = mk_session().put();
let create = mk_session().create();
let replace = mk_session().replace();
let delete = Session::delete("session-1");
let get = Session::get("session-1");
let update = Session::update("session-1");§Projections
A projection is a read-only view of some subset of an entity’s attributes. Every
entity is trivially its own projection. Projections can be defined manually or
by using the Projection derive macro.
use modyne::{EntityDef, Projection};
#[derive(Debug, EntityDef, serde::Serialize, serde::Deserialize)]
struct Session {
user_id: String,
session_token: String,
}
#[derive(Debug, Projection, serde::Deserialize)]
#[entity(Session)]
struct SessionTokenOnly {
session_token: String,
}The derive macro for projections includes a minimal amount of verification to
ensure that the field names match names know about from the projected entity.
Note that if the entity or the projection use the flatten attribute, then this
detection algorithm will not be able to identify misnamed fields. As an example,
the following will fail to compile.
#[derive(Debug, Projection, serde::Deserialize)]
#[entity(Session)]
struct SessionTokenOnly {
session: String,
}However, serde field attributes can be used to rename fields so that they will
appropriately match up if a different field name in the struct is desired.
#[derive(Debug, Projection, serde::Deserialize)]
#[entity(Session)]
struct SessionTokenOnly {
#[serde(rename = "session_token")]
session: String,
}§Aggregates and queries
The most efficient way to pull data out of a DynamoDB table is using range queries that can extract many entities in one operation. To support this, we provide means of ergonomically processing the variety of entities that might be returned in a single query through an aggregate.
use modyne::{
expr, keys, projections, read_projection,
Aggregate, Error, Item, QueryInput, QueryInputExt,
};
struct UserInfoQuery<'a> {
user_id: &'a str,
}
impl QueryInput for UserInfoQuery<'_> {
type Index = keys::Gsi1;
type Aggregate = UserInfo;
fn key_condition(&self) -> expr::KeyCondition<Self::Index> {
let partition = format!("USER#{}", self.user_id);
expr::KeyCondition::in_partition(partition)
}
}
#[derive(Debug, Default)]
struct UserInfo {
session_tokens: Vec<String>,
metadata: Option<UserMetadata>,
}
projections! {
enum UserInfoEntities {
Session,
UserMetadata,
}
}
impl Aggregate for UserInfo {
type Projections = UserInfoEntities;
fn merge(&mut self, item: Item) -> Result<(), Error> {
match read_projection!(item)? {
Self::Projections::Session(session) => {
self.session_tokens.push(session.session_token)
}
Self::Projections::UserMetadata(user) => {
self.metadata = Some(user)
}
}
Ok(())
}
}
impl Database {
async fn get_user_info_page(&self, user_id: &str) -> Result<UserInfo, Error> {
let result = UserInfoQuery { user_id: "test" }
.query()
.execute(self)
.await?;
let mut info = UserInfo::default();
info.reduce(result.items.unwrap_or_default())?;
Ok(info)
}
}§Non-standard tables
Modyne is generally opinionated about how to manage entities in a single-table
design. However, certain users may want to manage that information in a slightly
different way for interoperability with already existing ecosystems migrating
into Modyne. For these cases, the Table trait has a few items that can be
overriden.
§Using a non-standard entity type attribute name
If the attribute that holds the entity type name is not entity_type, then you
can specify the appropriate attribute name by overriding the
ENTITY_TYPE_ATTRIBUTE constant.
use modyne::{keys, Table};
impl Table for Database {
const ENTITY_TYPE_ATTRIBUTE: &'static str = "et";
type PrimaryKey = keys::Primary;
type IndexKeys = keys::Gsi1;
fn table_name(&self) -> &str {
&self.table_name
}
fn client(&self) -> &aws_sdk_dynamodb::Client {
&self.client
}
}This table will now store and retreive the entity type name from the et
attribute on an item, which it will expect to be a DynamoDB string value.
§Using a non-standard entity type attribute value
If the attribute that holds the entity type name does not use a DynamoDB string value, then you can override the behavior for embedding and extracting the entity type name by overriding the serialization functions for the table.
use modyne::{keys, AttributeValue, EntityTypeNameRef, MalformedEntityTypeError, Table};
impl Table for Database {
type PrimaryKey = keys::Primary;
type IndexKeys = keys::Gsi1;
fn table_name(&self) -> &str {
&self.table_name
}
fn client(&self) -> &aws_sdk_dynamodb::Client {
&self.client
}
fn deserialize_entity_type(
attr: &AttributeValue,
) -> Result<&EntityTypeNameRef, MalformedEntityTypeError> {
let values = attr
.as_ss()
.map_err(|_| MalformedEntityTypeError::Custom("expected a string set".into()))?;
let value = values
.first()
.expect("a DynamoDB string set always has at least one element");
Ok(EntityTypeNameRef::from_str(value.as_str()))
}
fn serialize_entity_type(entity_type: &EntityTypeNameRef) -> AttributeValue {
AttributeValue::Ss(vec![entity_type.to_string()])
}
}The above example uses the standard entity_type attribute, but stores and
retrieves the name as a single-element string set. You can combine both of these
overrides to use a non-standard value codec with a non-standard attribute name.
§Features
derive: Re-exports the derive macros provided by themodyne-derivecrate.once_cell: Uses the lazy initialization primitives provided by theonce_cellcrate.
§Minimum supported Rust version (MSRV)
The minimum supported Rust version for this crate is 1.70.0. The MSRV can be
reduced to 1.68.0 by enabling the once_cell feature to use that crate’s lazy
initialization primitives if desired.
Modules§
- expr
- Expression builders
- keys
- Types representing DynamoDB keys in a single-table design
- model
- Models for interacting with DynamoDB
- types
- Types useful as attributes in DynamoDB items
Macros§
- once_
projection_ expression - Generate a static projection expression that is computed exactly once in the lifetime of the program
- projections
- Utility macro for defining an
ProjectionSetused when querying items into anAggregate - read_
projection - Utility macro for reading an entity from a DynamoDB item
Structs§
- Entity
Type Name - The name for a DynamoDB entity type
- Entity
Type Name Ref - The name for a DynamoDB entity type
- Error
- An error that occurred while interacting with DynamoDB
Enums§
- Attribute
Value Represents the data for an attribute.
Each attribute value is described as a name-value pair. The name is the data type, and the value is the data itself.
For more information, see Data Types in the Amazon DynamoDB Developer Guide.
- Malformed
Entity Type Error - The entity type attribute was found, but was malformed and could not be extracted
Traits§
- Aggregate
- An aggregate of multiple entity types, often used when querying multiple items from a single partition key.
- Entity
- An entity in a DynamoDB table
- Entity
Def - The name and attribute definition for an
Entity - Entity
Ext - Extension trait for
Entitytypes - Projection
- A projection of an entity that may not contain all of the entity’s attributes
- Projection
Ext - Extension trait for
Projectiontypes - Projection
Set - A description of the set of entity types that constitute an
Aggregate - Query
Input - A value that can be used to query an aggregate
- Query
Input Ext - Extensions to an aggregate query
- Scan
Input - A value that can be used to query an aggregate
- Scan
Input Ext - Extensions to an aggregate scan
- Table
- A description of a DynamoDB table
- Test
Table Ext - Extension trait for
Tableto provide convenience methods for testing operations
Type Aliases§
- Item
- An alias for a DynamoDB item
Derive Macros§
- Entity
Def - Derive macro for the
EntityDeftrait - Projection
- Derive macro for the
Projectiontrait