Skip to content

Commit

Permalink
Add support for conditional deletes
Browse files Browse the repository at this point in the history
  • Loading branch information
Philip Wills committed May 18, 2016
1 parent 1d25593 commit 37664db
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 114 deletions.
2 changes: 1 addition & 1 deletion src/main/scala/com/gu/scanamo/ScanamoFree.scala
Expand Up @@ -9,7 +9,7 @@ import com.gu.scanamo.query._

object ScanamoFree {

import ScanamoRequest._
import Requests._
import cats.std.list._
import cats.syntax.traverse._

Expand Down
18 changes: 9 additions & 9 deletions src/main/scala/com/gu/scanamo/ScanamoRequest.scala
Expand Up @@ -2,19 +2,19 @@ package com.gu.scanamo

import com.amazonaws.services.dynamodbv2.model._
import com.gu.scanamo.query.{Query, UniqueKey, UniqueKeys}
import com.gu.scanamo.request.ScanamoPutRequest
import com.gu.scanamo.request.{ScanamoDeleteRequest, ScanamoPutRequest}

import scala.collection.convert.decorateAll._

private object ScanamoRequest {
private object Requests {

/**
* {{{
* prop> import collection.convert.decorateAsJava._
* prop> import com.amazonaws.services.dynamodbv2.model._
*
* prop> (m: Map[String, Int], tableName: String) =>
* | val putRequest = ScanamoRequest.putRequest(tableName)(m)
* | val putRequest = Requests.putRequest(tableName)(m)
* | putRequest.tableName == tableName &&
* | putRequest.item == m.mapValues(i => new AttributeValue().withN(i.toString)).asJava
* }}}
Expand All @@ -34,7 +34,7 @@ private object ScanamoRequest {
* prop> import com.gu.scanamo.syntax._
*
* prop> (keyName: String, keyValue: Long, tableName: String) =>
* | val getRequest = ScanamoRequest.getRequest(tableName)(Symbol(keyName) -> keyValue)
* | val getRequest = Requests.getRequest(tableName)(Symbol(keyName) -> keyValue)
* | getRequest.getTableName == tableName &&
* | getRequest.getKey == Map(keyName -> new AttributeValue().withN(keyValue.toString)).asJava
* }}}
Expand All @@ -54,13 +54,13 @@ private object ScanamoRequest {
* prop> import com.gu.scanamo.syntax._
*
* prop> (keyName: String, keyValue: Long, tableName: String) =>
* | val deleteRequest = ScanamoRequest.deleteRequest(tableName)(Symbol(keyName) -> keyValue)
* | deleteRequest.getTableName == tableName &&
* | deleteRequest.getKey == Map(keyName -> new AttributeValue().withN(keyValue.toString)).asJava
* | val deleteRequest = Requests.deleteRequest(tableName)(Symbol(keyName) -> keyValue)
* | deleteRequest.tableName == tableName &&
* | deleteRequest.key == Map(keyName -> new AttributeValue().withN(keyValue.toString)).asJava
* }}}
*/
def deleteRequest[T](tableName: String)(key: UniqueKey[_]): DeleteItemRequest =
new DeleteItemRequest().withTableName(tableName).withKey(key.asAVMap.asJava)
def deleteRequest[T](tableName: String)(key: UniqueKey[_]): ScanamoDeleteRequest =
ScanamoDeleteRequest(tableName = tableName, key = key.asAVMap.asJava, None)

def queryRequest[T](tableName: String)(query: Query[_]): QueryRequest = {
query(new QueryRequest().withTableName(tableName))
Expand Down
75 changes: 52 additions & 23 deletions src/main/scala/com/gu/scanamo/Table.scala
Expand Up @@ -86,6 +86,38 @@ case class Table[V: DynamoFormat](name: String) {
* ... }
* Some(Right(Farmer(McDonald,156,Farm(List(sheep, chicken)))))
*
* >>> case class Letter(roman: String, greek: String)
* >>> val lettersTable = Table[Letter]("letters")
* >>> LocalDynamoDB.withTable(client)("letters")('roman -> S) {
* ... val ops = for {
* ... _ <- lettersTable.putAll(List(Letter("a", "alpha"), Letter("b", "beta"), Letter("c", "gammon")))
* ... _ <- lettersTable.given('greek beginsWith "ale").put(Letter("a", "aleph"))
* ... _ <- lettersTable.given('greek beginsWith "gam").put(Letter("c", "gamma"))
* ... letters <- lettersTable.scan()
* ... } yield letters
* ... Scanamo.exec(client)(ops).toList
* ... }
* List(Right(Letter(b,beta)), Right(Letter(c,gamma)), Right(Letter(a,alpha)))
*
* >>> import cats.implicits._
* >>> case class Turnip(size: Int, description: Option[String])
* >>> val turnipsTable = Table[Turnip]("turnips")
* >>> LocalDynamoDB.withTable(client)("turnips")('size -> N) {
* ... val ops = for {
* ... _ <- turnipsTable.putAll(List(Turnip(1, None), Turnip(1000, None)))
* ... initialTurnips <- turnipsTable.scan()
* ... _ <- initialTurnips.flatMap(_.toOption).traverse(t =>
* ... turnipsTable.given('size > 500).put(t.copy(description = Some("Big turnip in the country."))))
* ... turnips <- turnipsTable.scan()
* ... } yield turnips
* ... Scanamo.exec(client)(ops).toList
* ... }
* List(Right(Turnip(1,None)), Right(Turnip(1000,Some(Big turnip in the country.))))
* }}}
*
* Conditions can also make use of negation via `not`:
*
* {{{
* >>> case class Thing(a: String, maybe: Option[Int])
* >>> val thingTable = Table[Thing]("things")
* >>> LocalDynamoDB.withTable(client)("things")('a -> S) {
Expand All @@ -100,7 +132,11 @@ case class Table[V: DynamoFormat](name: String) {
* ... Scanamo.exec(client)(ops).toList
* ... }
* List(Right(Thing(b,Some(3))), Right(Thing(c,Some(42))), Right(Thing(a,None)))
* }}}
*
* be combined with `and`
*
* {{{
* >>> case class Compound(a: String, maybe: Option[Int])
* >>> val compoundTable = Table[Compound]("compounds")
* >>> LocalDynamoDB.withTable(client)("compounds")('a -> S) {
Expand All @@ -114,20 +150,11 @@ case class Table[V: DynamoFormat](name: String) {
* ... Scanamo.exec(client)(ops).toList
* ... }
* List(Right(Compound(beta,Some(3))), Right(Compound(alpha,None)), Right(Compound(gamma,None)))
* }}}
*
* >>> case class Letter(roman: String, greek: String)
* >>> val lettersTable = Table[Letter]("letters")
* >>> LocalDynamoDB.withTable(client)("letters")('roman -> S) {
* ... val ops = for {
* ... _ <- lettersTable.putAll(List(Letter("a", "alpha"), Letter("b", "beta"), Letter("c", "gammon")))
* ... _ <- lettersTable.given('greek beginsWith "ale").put(Letter("a", "aleph"))
* ... _ <- lettersTable.given('greek beginsWith "gam").put(Letter("c", "gamma"))
* ... letters <- lettersTable.scan()
* ... } yield letters
* ... Scanamo.exec(client)(ops).toList
* ... }
* List(Right(Letter(b,beta)), Right(Letter(c,gamma)), Right(Letter(a,alpha)))
* or with `or`
*
* {{{
* >>> case class Choice(number: Int, description: String)
* >>> val choicesTable = Table[Choice]("choices")
* >>> LocalDynamoDB.withTable(client)("choices")('number -> N) {
Expand All @@ -140,21 +167,23 @@ case class Table[V: DynamoFormat](name: String) {
* ... Scanamo.exec(client)(ops).toList
* ... }
* List(Right(Choice(2,crumble)), Right(Choice(1,victoria sponge)), Right(Choice(3,custard)))
* }}}
*
* >>> import cats.implicits._
* >>> case class Turnip(size: Int, description: Option[String])
* >>> val turnipsTable = Table[Turnip]("turnips")
* >>> LocalDynamoDB.withTable(client)("turnips")('size -> N) {
* The same forms of condition can be applied to deletions
*
* {{{
* >>> case class Gremlin(number: Int, wet: Boolean)
* >>> val gremlinsTable = Table[Gremlin]("gremlins")
* >>> LocalDynamoDB.withTable(client)("gremlins")('number -> N) {
* ... val ops = for {
* ... _ <- turnipsTable.putAll(List(Turnip(1, None), Turnip(1000, None)))
* ... initialTurnips <- turnipsTable.scan()
* ... _ <- initialTurnips.flatMap(_.toOption).traverse(t =>
* ... turnipsTable.given('size > 500).put(t.copy(description = Some("Big turnip in the country."))))
* ... turnips <- turnipsTable.scan()
* ... } yield turnips
* ... _ <- gremlinsTable.putAll(List(Gremlin(1, false), Gremlin(2, true)))
* ... _ <- gremlinsTable.given('wet -> true).delete('number -> 1)
* ... _ <- gremlinsTable.given('wet -> true).delete('number -> 2)
* ... remainingGremlins <- gremlinsTable.scan()
* ... } yield remainingGremlins
* ... Scanamo.exec(client)(ops).toList
* ... }
* List(Right(Turnip(1,None)), Right(Turnip(1000,Some(Big turnip in the country.))))
* List(Right(Gremlin(1,false)))
* }}}
*/
def given[T: ConditionExpression](condition: T) = ScanamoFree.given(name)(condition)
Expand Down
32 changes: 26 additions & 6 deletions src/main/scala/com/gu/scanamo/ops/ScanamoOpsA.scala
Expand Up @@ -6,16 +6,17 @@ import com.amazonaws.AmazonWebServiceRequest
import com.amazonaws.handlers.AsyncHandler
import com.amazonaws.services.dynamodbv2.model._
import com.amazonaws.services.dynamodbv2.{AmazonDynamoDB, AmazonDynamoDBAsync}
import com.gu.scanamo.request.ScanamoPutRequest
import com.gu.scanamo.request.{ScanamoDeleteRequest, ScanamoPutRequest}

import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success}

sealed trait ScanamoOpsA[A]
sealed trait ScanamoOpsA[A] extends Product with Serializable
final case class Put(req: ScanamoPutRequest) extends ScanamoOpsA[PutItemResult]
final case class ConditionalPut(req: ScanamoPutRequest) extends ScanamoOpsA[Xor[ConditionalCheckFailedException, PutItemResult]]
final case class Get(req: GetItemRequest) extends ScanamoOpsA[GetItemResult]
final case class Delete(req: DeleteItemRequest) extends ScanamoOpsA[DeleteItemResult]
final case class Delete(req: ScanamoDeleteRequest) extends ScanamoOpsA[DeleteItemResult]
final case class ConditionalDelete(req: ScanamoDeleteRequest) extends ScanamoOpsA[Xor[ConditionalCheckFailedException, DeleteItemResult]]
final case class Scan(req: ScanRequest) extends ScanamoOpsA[ScanResult]
final case class Query(req: QueryRequest) extends ScanamoOpsA[QueryResult]
final case class BatchWrite(req: BatchWriteItemRequest) extends ScanamoOpsA[BatchWriteItemResult]
Expand All @@ -28,7 +29,9 @@ object ScanamoOps {
def conditionalPut(req: ScanamoPutRequest): ScanamoOps[Xor[ConditionalCheckFailedException, PutItemResult]] =
liftF[ScanamoOpsA, Xor[ConditionalCheckFailedException, PutItemResult]](ConditionalPut(req))
def get(req: GetItemRequest): ScanamoOps[GetItemResult] = liftF[ScanamoOpsA, GetItemResult](Get(req))
def delete(req: DeleteItemRequest): ScanamoOps[DeleteItemResult] = liftF[ScanamoOpsA, DeleteItemResult](Delete(req))
def delete(req: ScanamoDeleteRequest): ScanamoOps[DeleteItemResult] = liftF[ScanamoOpsA, DeleteItemResult](Delete(req))
def conditionalDelete(req: ScanamoDeleteRequest): ScanamoOps[Xor[ConditionalCheckFailedException, DeleteItemResult]] =
liftF[ScanamoOpsA, Xor[ConditionalCheckFailedException, DeleteItemResult]](ConditionalDelete(req))
def scan(req: ScanRequest): ScanamoOps[ScanResult] = liftF[ScanamoOpsA, ScanResult](Scan(req))
def query(req: QueryRequest): ScanamoOps[QueryResult] = liftF[ScanamoOpsA, QueryResult](Query(req))
def batchWrite(req: BatchWriteItemRequest): ScanamoOps[BatchWriteItemResult] =
Expand All @@ -49,6 +52,15 @@ object ScanamoInterpreters {
)((cond, values) => cond.withExpressionAttributeValues(values.asJava))
)

def javaDeleteRequest(req: ScanamoDeleteRequest): DeleteItemRequest =
req.condition.foldLeft(
new DeleteItemRequest().withTableName(req.tableName).withKey(req.key)
)((r, c) =>
c.attributeValues.foldLeft(
r.withConditionExpression(c.expression).withExpressionAttributeNames(c.attributeNames.asJava)
)((cond, values) => cond.withExpressionAttributeValues(values.asJava))
)

def id(client: AmazonDynamoDB) = new (ScanamoOpsA ~> Id) {
def apply[A](op: ScanamoOpsA[A]): Id[A] = op match {
case Put(req) =>
Expand All @@ -60,7 +72,11 @@ object ScanamoInterpreters {
case Get(req) =>
client.getItem(req)
case Delete(req) =>
client.deleteItem(req)
client.deleteItem(javaDeleteRequest(req))
case ConditionalDelete(req) =>
Xor.catchOnly[ConditionalCheckFailedException] {
client.deleteItem(javaDeleteRequest(req))
}
case Scan(req) =>
client.scan(req)
case Query(req) =>
Expand Down Expand Up @@ -93,7 +109,11 @@ object ScanamoInterpreters {
case Get(req) =>
futureOf(client.getItemAsync, req)
case Delete(req) =>
futureOf(client.deleteItemAsync, req)
futureOf(client.deleteItemAsync, javaDeleteRequest(req))
case ConditionalDelete(req) =>
futureOf(client.deleteItemAsync, javaDeleteRequest(req)).map(Xor.right[ConditionalCheckFailedException, DeleteItemResult]).recover {
case e: ConditionalCheckFailedException => Xor.left(e)
}
case Scan(req) =>
futureOf(client.scanAsync, req)
case Query(req) =>
Expand Down

0 comments on commit 37664db

Please sign in to comment.