Skip to main content
Version: 2.x

Field Traversal

We will be using the below model for the examples which contains a complex collection field hobbies and a nested case class field address. Note that ZIO DynamoDB also supports case classes as collection elements (not shown in this example).

final case class Address(number: String, street: String)
object Address {
implicit lazy val schema: Schema.CaseClass2[String, String, Address] = DeriveSchema.gen[Address]

// (1) number and street are ProjectionExpressions
val (number, street) = ProjectionExpression.accessors[Address]
}
final case class Person(email: String, hobbies: Map[String, List[String]], address: Address)
object Person {
implicit lazy val schema: Schema.CaseClass3[String, Map[String, List[String]], Address, Person] = DeriveSchema.gen[Person]

// (2) email, hobbies and address are ProjectionExpressions
val (email, hobbies, address) = ProjectionExpression.accessors[Person]
}

ProjectionExpression field traversal combinators​

DynamoDB allows us to dig into nested structures when updating or querying data using path expressions. Using the High Level API we do this in a type safe way by using the ProjectionExpression's generated by the accessors method (see comments 1 and 2 above) as springboard for a bunch of traversal combinators. Note the accessors method in turn uses the reified optics feature of ZIO Schema.

Traversal CombinatorDescription
>>>Returns a ProjectionExpression that traverses into a product (case class) or a sum type (sealed trait concrete instance)
valueAt(key)Returns a ProjectionExpression that traverses into a map field using a string key
elementAt(N)Returns a ProjectionExpression that traverses into a list field using a zero based index
  val person                                                             =
Person("john@gmail.com", Map("sports" -> List("cricket", "football")), Address("1", "Main St"))

// ProjectionExpressions extracted deliberately to illustrate the types
val addressToStreetPE: ProjectionExpression[Person, String] =
(Person.address >>> Address.street)
val valueAtHobbiesPE: ProjectionExpression[Person, List[String]] =
Person.hobbies.valueAt("sports")
val valueAtAndElementAtHobbiesPE: ProjectionExpression[Person, String] =
Person.hobbies.valueAt("sports").elementAt(0)

for {
_ <- DynamoDBQuery.put("people", person).execute

_ <- DynamoDBQuery
.update("people")(Person.email.partitionKey === "john@gmail.com")(
addressToStreetPE.set("2nd St")
)
.execute

_ <- DynamoDBQuery
.update("people")(Person.email.partitionKey === "john@gmail.com")(
valueAtHobbiesPE.set(List("tennis", "rugby"))
)
.execute

_ <- DynamoDBQuery
.update("people")(Person.email.partitionKey === "john@gmail.com")(
valueAtAndElementAtHobbiesPE.set("cricket")
)
.execute

} yield ()

Product traversal​

From the the example above:

  val addressToStreetPE: ProjectionExpression[Person, String]            =
(Person.address >>> Address.street)

addressToStreetPE is an example of traversing into a product (case class).

Sum type traversal​

To illustrate sum type traversal, consider the below model with sum type (sealed trait) BilledBody:

  @discriminatorName("billedType")
sealed trait BilledBody

object BilledBody {
final case class BilledMonthly(month: Int) extends BilledBody
object BilledMonthly {
implicit lazy val schema: Schema.CaseClass1[Int, BilledMonthly] = DeriveSchema.gen[BilledMonthly]
val month: ProjectionExpression[BilledMonthly, Int] = ProjectionExpression.accessors[BilledMonthly]
}
final case class BilledYearly(year: Int) extends BilledBody
object BilledYearly {
implicit lazy val schema: Schema.CaseClass1[Int, BilledYearly] = DeriveSchema.gen[BilledYearly]
val year: ProjectionExpression[BilledYearly, Int] = ProjectionExpression.accessors[BilledYearly]
}
implicit val schema: Schema.Enum2[BilledMonthly, BilledYearly, BilledBody] = DeriveSchema.gen[BilledBody]
val (monthly, yearly) = ProjectionExpression.accessors[BilledBody]
}

final case class Invoice(int: Int, body: BilledBody)
object Invoice {
implicit lazy val schema: Schema.CaseClass2[Int, BilledBody, Invoice] = DeriveSchema.gen[Invoice]
val (int, body) = ProjectionExpression.accessors[Invoice]
}

We can access the BilledYearly case class field year using the >>> combinator as shown below:

  val invoiceToBilledYearlyToYearPE: ProjectionExpression[Invoice, Int] =
Invoice.body >>> BilledBody.yearly >>> BilledYearly.year

val yearlyInvoice = Invoice(1, BilledYearly(2021))

for {
_ <- DynamoDBQuery.put("invoices", yearlyInvoice).execute
_ <- DynamoDBQuery
.update("invoices")(Invoice.int.partitionKey === 1)(
invoiceToBilledYearlyToYearPE.set(2022)
)
.execute

} yield ()