Browse Source

server: Add support for ordering in "GET /addresses/:address/transactions"

prometheus-integration
Alexis Hernandez 7 years ago
parent
commit
510b127d3c
  1. 8
      server/app/com/xsn/explorer/data/TransactionDataHandler.scala
  2. 8
      server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala
  3. 20
      server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala
  4. 8
      server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala
  5. 23
      server/app/com/xsn/explorer/models/fields/TransactionField.scala
  6. 16
      server/app/com/xsn/explorer/parsers/TransactionOrderingParser.scala
  7. 10
      server/app/com/xsn/explorer/services/TransactionService.scala
  8. 6
      server/app/controllers/AddressesController.scala
  9. 2
      server/conf/routes
  10. 9
      server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala
  11. 8
      server/test/com/xsn/explorer/processors/BlockEventsProcessorSpec.scala
  12. 2
      server/test/com/xsn/explorer/processors/BlockOpsSpec.scala
  13. 5
      server/test/controllers/AddressesControllerSpec.scala

8
server/app/com/xsn/explorer/data/TransactionDataHandler.scala

@ -1,8 +1,9 @@
package com.xsn.explorer.data
import com.alexitc.playsonify.core.ApplicationResult
import com.alexitc.playsonify.models.{PaginatedQuery, PaginatedResult}
import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery, PaginatedResult}
import com.xsn.explorer.models._
import com.xsn.explorer.models.fields.TransactionField
import scala.language.higherKinds
@ -14,7 +15,10 @@ trait TransactionDataHandler[F[_]] {
def deleteBy(blockhash: Blockhash): F[List[Transaction]]
def getBy(address: Address, paginatedQuery: PaginatedQuery): F[PaginatedResult[TransactionWithValues]]
def getBy(
address: Address,
paginatedQuery: PaginatedQuery,
ordering: FieldOrdering[TransactionField]): F[PaginatedResult[TransactionWithValues]]
}
trait TransactionBlockingDataHandler extends TransactionDataHandler[ApplicationResult]

8
server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala

@ -3,11 +3,12 @@ package com.xsn.explorer.data.anorm
import javax.inject.Inject
import com.alexitc.playsonify.core.ApplicationResult
import com.alexitc.playsonify.models.{PaginatedQuery, PaginatedResult}
import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery, PaginatedResult}
import com.xsn.explorer.data.TransactionBlockingDataHandler
import com.xsn.explorer.data.anorm.dao.TransactionPostgresDAO
import com.xsn.explorer.errors.{TransactionNotFoundError, TransactionUnknownError}
import com.xsn.explorer.models._
import com.xsn.explorer.models.fields.TransactionField
import org.scalactic.{Good, One, Or}
import play.api.db.Database
@ -34,9 +35,10 @@ class TransactionPostgresDataHandler @Inject() (
override def getBy(
address: Address,
paginatedQuery: PaginatedQuery): ApplicationResult[PaginatedResult[TransactionWithValues]] = withConnection { implicit conn =>
paginatedQuery: PaginatedQuery,
ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = withConnection { implicit conn =>
val transactions = transactionPostgresDAO.getBy(address, paginatedQuery)
val transactions = transactionPostgresDAO.getBy(address, paginatedQuery, ordering)
val total = transactionPostgresDAO.countBy(address)
val result = PaginatedResult(paginatedQuery.offset, paginatedQuery.limit, total, transactions)

20
server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala

@ -1,13 +1,16 @@
package com.xsn.explorer.data.anorm.dao
import java.sql.Connection
import javax.inject.Inject
import anorm._
import com.alexitc.playsonify.models.{Count, PaginatedQuery}
import com.alexitc.playsonify.models.{Count, FieldOrdering, PaginatedQuery}
import com.xsn.explorer.data.anorm.interpreters.FieldOrderingSQLInterpreter
import com.xsn.explorer.data.anorm.parsers.TransactionParsers._
import com.xsn.explorer.models._
import com.xsn.explorer.models.fields.TransactionField
class TransactionPostgresDAO {
class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderingSQLInterpreter) {
/**
* NOTE: Ensure the connection has an open transaction.
@ -64,14 +67,21 @@ class TransactionPostgresDAO {
}
}
def getBy(address: Address, paginatedQuery: PaginatedQuery)(implicit conn: Connection): List[TransactionWithValues] = {
def getBy(
address: Address,
paginatedQuery: PaginatedQuery,
ordering: FieldOrdering[TransactionField])(
implicit conn: Connection): List[TransactionWithValues] = {
val orderBy = fieldOrderingSQLInterpreter.toOrderByClause(ordering)
/**
* TODO: The query is very slow while aggregating the spent and received values,
* it might be worth creating an index-like table to get the accumulated
* values directly.
*/
SQL(
"""
s"""
|SELECT t.txid, blockhash, time, size,
| (SELECT COALESCE(SUM(value), 0) FROM transaction_inputs WHERE txid = t.txid) AS sent,
| (SELECT COALESCE(SUM(value), 0) FROM transaction_outputs WHERE txid = t.txid) AS received
@ -85,7 +95,7 @@ class TransactionPostgresDAO {
| FROM transaction_outputs
| WHERE address = {address}
|)
|ORDER BY time DESC
|$orderBy
|OFFSET {offset}
|LIMIT {limit}
""".stripMargin

8
server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala

@ -3,10 +3,11 @@ package com.xsn.explorer.data.async
import javax.inject.Inject
import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult}
import com.alexitc.playsonify.models.PaginatedQuery
import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery}
import com.xsn.explorer.data.{TransactionBlockingDataHandler, TransactionDataHandler}
import com.xsn.explorer.executors.DatabaseExecutionContext
import com.xsn.explorer.models._
import com.xsn.explorer.models.fields.TransactionField
import scala.concurrent.Future
@ -29,8 +30,9 @@ class TransactionFutureDataHandler @Inject() (
override def getBy(
address: Address,
paginatedQuery: PaginatedQuery): FuturePaginatedResult[TransactionWithValues] = Future {
paginatedQuery: PaginatedQuery,
ordering: FieldOrdering[TransactionField]): FuturePaginatedResult[TransactionWithValues] = Future {
blockingDataHandler.getBy(address, paginatedQuery)
blockingDataHandler.getBy(address, paginatedQuery, ordering)
}
}

23
server/app/com/xsn/explorer/models/fields/TransactionField.scala

@ -0,0 +1,23 @@
package com.xsn.explorer.models.fields
import com.xsn.explorer.data.anorm.interpreters.ColumnNameResolver
import enumeratum._
sealed abstract class TransactionField(override val entryName: String) extends EnumEntry
object TransactionField extends Enum[TransactionField] {
val values = findValues
case object TransactionId extends TransactionField("txid")
case object Time extends TransactionField("time")
case object Sent extends TransactionField("sent")
case object Received extends TransactionField("received")
implicit val columnNameResolver: ColumnNameResolver[TransactionField] = new ColumnNameResolver[TransactionField] {
override def getUniqueColumnName: String = TransactionId.entryName
override def getColumnName(field: TransactionField): String = field.entryName
}
}

16
server/app/com/xsn/explorer/parsers/TransactionOrderingParser.scala

@ -0,0 +1,16 @@
package com.xsn.explorer.parsers
import com.alexitc.playsonify.models.OrderingCondition
import com.alexitc.playsonify.parsers.FieldOrderingParser
import com.xsn.explorer.models.fields.TransactionField
class TransactionOrderingParser extends FieldOrderingParser[TransactionField] {
override protected val defaultField = TransactionField.Time
override protected val defaultOrderingCondition: OrderingCondition = OrderingCondition.DescendingOrder
override protected def parseField(unsafeField: String): Option[TransactionField] = {
TransactionField.withNameOption(unsafeField)
}
}

10
server/app/com/xsn/explorer/services/TransactionService.scala

@ -4,12 +4,13 @@ import javax.inject.Inject
import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureListOps, FutureOps, OrOps}
import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult}
import com.alexitc.playsonify.models.PaginatedQuery
import com.alexitc.playsonify.models.{OrderingQuery, PaginatedQuery}
import com.alexitc.playsonify.validators.PaginatedQueryValidator
import com.xsn.explorer.data.async.TransactionFutureDataHandler
import com.xsn.explorer.errors.{AddressFormatError, InvalidRawTransactionError, TransactionFormatError, TransactionNotFoundError}
import com.xsn.explorer.models._
import com.xsn.explorer.models.rpc.TransactionVIN
import com.xsn.explorer.parsers.TransactionOrderingParser
import org.scalactic.{Bad, Good, One, Or}
import play.api.libs.json.{JsObject, JsString, JsValue}
@ -17,6 +18,7 @@ import scala.concurrent.{ExecutionContext, Future}
class TransactionService @Inject() (
paginatedQueryValidator: PaginatedQueryValidator,
transactionOrderingParser: TransactionOrderingParser,
xsnService: XSNService,
transactionFutureDataHandler: TransactionFutureDataHandler)(
implicit ec: ExecutionContext) {
@ -74,7 +76,8 @@ class TransactionService @Inject() (
def getTransactions(
addressString: String,
paginatedQuery: PaginatedQuery): FuturePaginatedResult[TransactionWithValues] = {
paginatedQuery: PaginatedQuery,
orderingQuery: OrderingQuery): FuturePaginatedResult[TransactionWithValues] = {
val result = for {
address <- {
@ -83,7 +86,8 @@ class TransactionService @Inject() (
}
paginatedQuery <- paginatedQueryValidator.validate(paginatedQuery, 100).toFutureOr
transactions <- transactionFutureDataHandler.getBy(address, paginatedQuery).toFutureOr
ordering <- transactionOrderingParser.from(orderingQuery).toFutureOr
transactions <- transactionFutureDataHandler.getBy(address, paginatedQuery, ordering).toFutureOr
} yield transactions
result.toFuture

6
server/app/controllers/AddressesController.scala

@ -2,7 +2,7 @@ package controllers
import javax.inject.Inject
import com.alexitc.playsonify.models.{Limit, Offset, PaginatedQuery}
import com.alexitc.playsonify.models.{Limit, Offset, OrderingQuery, PaginatedQuery}
import com.xsn.explorer.services.{AddressService, TransactionService}
import controllers.common.{MyJsonController, MyJsonControllerComponents}
@ -16,10 +16,10 @@ class AddressesController @Inject() (
addressService.getDetails(address)
}
def getTransactions(address: String, offset: Int, limit: Int) = publicNoInput { _ =>
def getTransactions(address: String, offset: Int, limit: Int, ordering: String) = publicNoInput { _ =>
val paginatedQuery = PaginatedQuery(Offset(offset), Limit(limit))
transactionService.getTransactions(address, paginatedQuery)
transactionService.getTransactions(address, paginatedQuery, OrderingQuery(ordering))
}
def getUnspentOutputs(address: String) = publicNoInput { _ =>

2
server/conf/routes

@ -10,7 +10,7 @@ GET /transactions/:txid/raw controllers.TransactionsController.getRawTransa
POST /transactions controllers.TransactionsController.sendRawTransaction()
GET /addresses/:address controllers.AddressesController.getDetails(address: String)
GET /addresses/:address/transactions controllers.AddressesController.getTransactions(address: String, offset: Int ?= 0, limit: Int ?= 10)
GET /addresses/:address/transactions controllers.AddressesController.getTransactions(address: String, offset: Int ?= 0, limit: Int ?= 10, orderBy: String ?= "")
GET /addresses/:address/utxos controllers.AddressesController.getUnspentOutputs(address: String)
GET /blocks controllers.BlocksController.getLatestBlocks()

9
server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala

@ -3,15 +3,18 @@ package com.xsn.explorer.data
import com.alexitc.playsonify.models._
import com.xsn.explorer.data.anorm.TransactionPostgresDataHandler
import com.xsn.explorer.data.anorm.dao.TransactionPostgresDAO
import com.xsn.explorer.data.anorm.interpreters.FieldOrderingSQLInterpreter
import com.xsn.explorer.data.common.PostgresDataHandlerSpec
import com.xsn.explorer.errors.TransactionNotFoundError
import com.xsn.explorer.helpers.DataHelper._
import com.xsn.explorer.models.fields.TransactionField
import com.xsn.explorer.models.{Size, Transaction, TransactionWithValues}
import org.scalactic.{Bad, Good}
class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec {
lazy val dataHandler = new TransactionPostgresDataHandler(database, new TransactionPostgresDAO)
lazy val dataHandler = new TransactionPostgresDataHandler(database, new TransactionPostgresDAO(new FieldOrderingSQLInterpreter))
val defaultOrdering = FieldOrdering(TransactionField.Time, OrderingCondition.DescendingOrder)
val inputs = List(
Transaction.Input(0, None, None),
@ -106,7 +109,7 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec {
"find no results" in {
val expected = PaginatedResult(query.offset, query.limit, Count(0), List.empty)
val result = dataHandler.getBy(address, query)
val result = dataHandler.getBy(address, query, defaultOrdering)
result mustEqual Good(expected)
}
@ -120,7 +123,7 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec {
val expected = PaginatedResult(query.offset, query.limit, Count(1), List(transactionWithValues))
dataHandler.upsert(transaction).isGood mustEqual true
val result = dataHandler.getBy(address, query)
val result = dataHandler.getBy(address, query, defaultOrdering)
result mustEqual Good(expected)
}
}

8
server/test/com/xsn/explorer/processors/BlockEventsProcessorSpec.scala

@ -13,6 +13,7 @@ import com.xsn.explorer.helpers.{BlockLoader, Executors, FileBasedXSNService}
import com.xsn.explorer.models.fields.BalanceField
import com.xsn.explorer.models.rpc.{Block, Transaction}
import com.xsn.explorer.models.{Blockhash, TransactionId}
import com.xsn.explorer.parsers.TransactionOrderingParser
import com.xsn.explorer.processors.BlockEventsProcessor._
import com.xsn.explorer.services.{TransactionService, XSNService}
import org.scalactic.{Bad, Good}
@ -24,7 +25,9 @@ import scala.concurrent.Future
class BlockEventsProcessorSpec extends PostgresDataHandlerSpec with ScalaFutures with BeforeAndAfter {
lazy val dataHandler = new BlockPostgresDataHandler(database, new BlockPostgresDAO)
lazy val transactionDataHandler = new TransactionPostgresDataHandler(database, new TransactionPostgresDAO)
lazy val transactionDataHandler = new TransactionPostgresDataHandler(
database,
new TransactionPostgresDAO(new FieldOrderingSQLInterpreter))
lazy val xsnService = new FileBasedXSNService
lazy val processor = blockEventsProcessor()
@ -321,7 +324,7 @@ class BlockEventsProcessorSpec extends PostgresDataHandlerSpec with ScalaFutures
val dataSeeder = new DatabasePostgresSeeder(
database,
new BlockPostgresDAO,
new TransactionPostgresDAO,
new TransactionPostgresDAO(new FieldOrderingSQLInterpreter),
new BalancePostgresDAO(new FieldOrderingSQLInterpreter))
val blockOps = new BlockOps(
@ -330,6 +333,7 @@ class BlockEventsProcessorSpec extends PostgresDataHandlerSpec with ScalaFutures
val transactionService = new TransactionService(
new PaginatedQueryValidator,
new TransactionOrderingParser,
xsn,
new TransactionFutureDataHandler(transactionDataHandler)(Executors.databaseEC))(Executors.globalEC)

2
server/test/com/xsn/explorer/processors/BlockOpsSpec.scala

@ -20,7 +20,7 @@ class BlockOpsSpec extends PostgresDataHandlerSpec with ScalaFutures with Before
lazy val dataSeeder = new DatabasePostgresSeeder(
database,
new BlockPostgresDAO,
new TransactionPostgresDAO,
new TransactionPostgresDAO(new FieldOrderingSQLInterpreter),
new BalancePostgresDAO(new FieldOrderingSQLInterpreter))
lazy val xsnService = new FileBasedXSNService

5
server/test/controllers/AddressesControllerSpec.scala

@ -2,11 +2,12 @@ package controllers
import com.alexitc.playsonify.PublicErrorRenderer
import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult}
import com.alexitc.playsonify.models.{Count, PaginatedQuery, PaginatedResult}
import com.alexitc.playsonify.models.{Count, FieldOrdering, PaginatedQuery, PaginatedResult}
import com.xsn.explorer.data.TransactionBlockingDataHandler
import com.xsn.explorer.errors.AddressFormatError
import com.xsn.explorer.helpers.{DataHelper, DummyXSNService}
import com.xsn.explorer.models._
import com.xsn.explorer.models.fields.TransactionField
import com.xsn.explorer.models.rpc.AddressBalance
import com.xsn.explorer.services.XSNService
import controllers.common.MyAPISpec
@ -100,7 +101,7 @@ class AddressesControllerSpec extends MyAPISpec {
override def deleteBy(blockhash: Blockhash): ApplicationResult[List[Transaction]] = ???
override def getBy(address: Address, paginatedQuery: PaginatedQuery): ApplicationResult[PaginatedResult[TransactionWithValues]] = {
override def getBy(address: Address, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = {
if (address == addressForTransactions) {
Good(PaginatedResult(paginatedQuery.offset, paginatedQuery.limit, Count(1), List(addressTransaction)))
} else {

Loading…
Cancel
Save