Skip to content

Commit

Permalink
adde single table example (#514)
Browse files Browse the repository at this point in the history
  • Loading branch information
googley42 authored Oct 24, 2024
1 parent 5f67c83 commit 3ab1bcc
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 35 deletions.
13 changes: 13 additions & 0 deletions dynamodb/src/it/scala/zio/dynamodb/DynamoDBLocalSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ abstract class DynamoDBLocalSpec extends ZIOSpec[DynamoDBExecutor] {
AttributeDefinition.attrDefnString("year")
)

def idAndSelectorKeyTable(tableName: String): DynamoDBQuery.CreateTable =
DynamoDBQuery.createTable(tableName, KeySchema("id", "selector"), BillingMode.PayPerRequest)(
AttributeDefinition.attrDefnString("id"),
AttributeDefinition.attrDefnString("selector")
)

def idAndAccountIdGsiTable(tableName: String): DynamoDBQuery[Any, Unit] =
DynamoDBQuery
.createTable(tableName, KeySchema("id"), BillingMode.PayPerRequest)(
Expand All @@ -85,6 +91,13 @@ abstract class DynamoDBLocalSpec extends ZIOSpec[DynamoDBExecutor] {
managedTable(idAndYearKeyTable).flatMap(t => f(t.value))
}

def withIdAndSelectorKeyTable(
f: String => ZIO[DynamoDBExecutor, Throwable, TestResult]
): ZIO[DynamoDBExecutor with Scope, Throwable, TestResult] =
ZIO.scoped {
managedTable(idAndSelectorKeyTable).flatMap(t => f(t.value))
}

def withTwoSingleIdKeyTables(
f: (String, String) => ZIO[DynamoDBExecutor, Throwable, TestResult]
): ZIO[DynamoDBExecutor with Scope, Throwable, TestResult] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ object TypeSafeApiAlternateModeling extends DynamoDBLocalSpec {
implicit val schema: Schema.CaseClass2[String, Int, Abc] = DeriveSchema.gen[Abc]
val (sku, amount) = ProjectionExpression.accessors[Abc]
}
final case class Xyz(sku: String, otp: String) extends GroupBody
final case class Xyz(sku: String, orderId: String) extends GroupBody
object Xyz {
implicit val schema: Schema.CaseClass2[String, String, Xyz] = DeriveSchema.gen[Xyz]
val (sku, otp) = ProjectionExpression.accessors[Xyz]
val (sku, orderId) = ProjectionExpression.accessors[Xyz]
}
implicit val schema: Schema.Enum2[Abc, Xyz, GroupBody] = DeriveSchema.gen[GroupBody]
val (fixed, otp) = ProjectionExpression.accessors[GroupBody]
val (abc, xyz) = ProjectionExpression.accessors[GroupBody]
}

@discriminatorName("contractType")
Expand Down Expand Up @@ -66,7 +66,7 @@ object TypeSafeApiAlternateModeling extends DynamoDBLocalSpec {
object Contract {
implicit val schema: Schema.CaseClass5[String, Option[String], Option[String], Boolean, ContractBody, Contract] =
DeriveSchema.gen[Contract]
val (id, identityId, accountId, isTest, body) = ProjectionExpression.accessors[Contract]
val (id, alternateId, accountId, isTest, body) = ProjectionExpression.accessors[Contract]
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package zio.dynamodb

import zio.test.assertTrue
import zio.test.TestAspect
import zio.test.Spec
import zio.test.TestEnvironment
import zio.Scope
import zio.schema.Schema
import zio.schema.DeriveSchema
import java.time.Instant
import zio.schema.annotation.discriminatorName
import zio.ZIO

/**
* Demonstrates how to implement the single table pattern (also known as the adjacency lists pattern) using the type-safe API
* using sum and product types.
* see https://www.alexdebrie.com/posts/dynamodb-single-table/
* see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-adjacency-graphs.html
*/
object TypeSafeApiSingleTableSpec extends DynamoDBLocalSpec {

@discriminatorName("userBodyType")
sealed trait UserBody
object UserBody {

final case class Profile(username: String, fullName: String, email: String, createdAt: Instant) extends UserBody
object Profile {
implicit val schema: Schema.CaseClass4[String, String, String, Instant, Profile] = DeriveSchema.gen[Profile]
val (username, fullName, email, createdAt) = ProjectionExpression.accessors[Profile]
}

final case class Order(username: String, orderId: String, status: String, createdAt: Instant) extends UserBody
object Order {
implicit val schema: Schema.CaseClass4[String, String, String, Instant, Order] = DeriveSchema.gen[Order]
val (userName, orderId, status, createdAt) = ProjectionExpression.accessors[Order]
}

implicit val schema: Schema.Enum2[Profile, Order, UserBody] = DeriveSchema.gen[UserBody]
val (profile, order) = ProjectionExpression.accessors[UserBody]
}
final case class User(id: String, selector: String, userBody: UserBody)
object User {
implicit val schema: Schema.CaseClass3[String, String, UserBody, User] = DeriveSchema.gen[User]
val (id, selector, userBody) = ProjectionExpression.accessors[User]

// smart constructors manage the id and sort keys

def makeProfile(username: String, fullName: String, email: String, createdAt: Instant): User =
User(s"USER:$username", s"Profile:$username", UserBody.Profile(username, fullName, email, createdAt))

def makeOrder(username: String, orderId: String, status: String, createdAt: Instant): User =
User(s"USER:$username", s"Order:$orderId", UserBody.Order(username, orderId, status, createdAt))
}
override def spec: Spec[Environment with TestEnvironment with Scope, Any] =
suite("suite")(
test("test") {
withIdAndSelectorKeyTable { tableName =>
for {
now <- zio.Clock.instant
_ <- DynamoDBQuery.put(tableName, User.makeProfile("Bob", "Bob Smith", "[email protected]", now)).execute
_ <- DynamoDBQuery.put(tableName, User.makeOrder("Bob", "123", "pending", now)).execute
_ <- DynamoDBQuery.put(tableName, User.makeOrder("Bob", "124", "pending", now)).execute
stream <- DynamoDBQuery
.queryAll[User](tableName)
.whereKey(
User.id.partitionKey === "USER:Bob" && (User.selector.sortKey.beginsWith("Order"))
)
.execute
_ <- stream.tap(ZIO.debug(_)).runDrain
// User(USER:Bob,Order#123,Order(Bob,123,pending,1970-01-01T00:00:00Z))
// User(USER:Bob,Order#124,Order(Bob,124,pending,1970-01-01T00:00:00Z))
} yield assertTrue(true)
}

}
) @@ TestAspect.nondeterministic
}
2 changes: 0 additions & 2 deletions dynamodb/src/main/scala/zio/dynamodb/AliasMap.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package zio.dynamodb

import scala.annotation.tailrec
import scala.annotation.nowarn

@nowarn
private[dynamodb] final case class AliasMap private[dynamodb] (map: Map[AliasMap.Key, String], index: Int) { self =>

private def +(entry: AttributeValue): (AliasMap, String) = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package zio.dynamodb

import scala.annotation.nowarn

@nowarn
final case class AttributeDefinition private (
name: String,
attributeType: PrimitiveValueType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,9 @@ import zio.{ Chunk, NonEmptyChunk, ZIO }

import scala.collection.immutable.{ Map => ScalaMap }
import software.amazon.awssdk.services.dynamodb.model.{ DynamoDbException => AwsSdkDynamoDbException }
import scala.annotation.nowarn
import zio.durationInt
import zio.Schedule

@nowarn
private[dynamodb] final case class DynamoDBExecutorImpl private[dynamodb] (dynamoDb: DynamoDb)
extends DynamoDBExecutor {
import DynamoDBExecutorImpl._
Expand Down
3 changes: 0 additions & 3 deletions dynamodb/src/main/scala/zio/dynamodb/KeySchema.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package zio.dynamodb

import scala.annotation.nowarn

@nowarn
final case class KeySchema private (hashKey: String, sortKey: Option[String])
object KeySchema {
def apply(hashKey: String): KeySchema = KeySchema(hashKey, sortKey = None)
Expand Down
2 changes: 0 additions & 2 deletions dynamodb/src/main/scala/zio/dynamodb/MapOfSet.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package zio.dynamodb

import scala.collection.immutable.{ Map => ScalaMap }
import scala.annotation.nowarn

@nowarn
private[dynamodb] final case class MapOfSet[K, V] private (private val map: ScalaMap[K, Set[V]])
extends Iterable[(K, Set[V])] { self =>

Expand Down
3 changes: 0 additions & 3 deletions dynamodb/src/main/scala/zio/dynamodb/NonEmptySet.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package zio.dynamodb

import scala.annotation.nowarn

@nowarn
private[dynamodb] final case class NonEmptySet[A] private (private val set: Set[A]) extends Iterable[A] {
self =>
def +(a: A): NonEmptySet[A] = new NonEmptySet(set + a)
Expand Down
3 changes: 0 additions & 3 deletions dynamodb/src/main/scala/zio/dynamodb/ProjectionType.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package zio.dynamodb

import scala.annotation.nowarn

sealed trait ProjectionType
object ProjectionType {

case object KeysOnly extends ProjectionType
// count must not exceed 20
@nowarn
final case class Include private (nonKeyAttributes: NonEmptySet[String]) extends ProjectionType
object Include {
def apply(nonKeyAttribute: String, nonKeyAttributes: String*): Include =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import zio.dynamodb.TestDynamoDBExecutor.PkAndItem
import zio.stm.{ STM, TMap, ZSTM }
import zio.stream.{ Stream, ZStream }
import zio.{ Chunk, IO, Ref, UIO, ZIO }
import scala.annotation.nowarn

@nowarn
private[dynamodb] final case class TestDynamoDBExecutorImpl private[dynamodb] (
queries: Ref[List[DynamoDBQuery[_, _]]],
tableMap: TMap[TableName, TMap[PrimaryKey, Item]],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package zio.dynamodb.examples

import zio.dynamodb.json._
import zio.dynamodb.AttrMap
import zio.schema.DeriveSchema
import zio.schema.annotation.discriminatorName

Expand Down Expand Up @@ -45,13 +44,8 @@ object ZioDynamodbJsonExample extends App {
val errorOrItem = parseItem(jsonString)
println(errorOrItem) // Right(AttrMap(Map("sku" -> S("sku"), "id" -> S("id"), "invoiceType" -> S("PreBilled"))))

// get the rendered json string from an Item
errorOrItem
.map(item => item.toJsonString)
.map(println) // {"sku":{"S":"sku"},"id":{"S":"id"},"invoiceType":{"S":"PreBilled"}}
// decode the json string to a case class
val errorOrClass = parse[Invoice](jsonString)
println(errorOrClass) // Right(PreBilled("id", "sku"))

// random AttrMap with no Schema
val attrMap = AttrMap("foo" -> "foo", "bar" -> "bar", "count" -> 1)
println(attrMap.toJsonString) // {"foo":{"S":"foo"},"bar":{"S":"bar"},"baz":{"S":"baz"}}
println(attrMap.toJsonStringPretty)
}
3 changes: 1 addition & 2 deletions project/BuildHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ object BuildHelper {
"-Ypatmat-exhaust-depth",
"40",
"-Ywarn-numeric-widen",
"-Ywarn-value-discard",
"-Xsource:3.0"
"-Ywarn-value-discard"
)

private val stdOpts213 = Seq(
Expand Down

0 comments on commit 3ab1bcc

Please sign in to comment.