aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/ledger/database
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/moe/nea/ledger/database')
-rw-r--r--src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt19
-rw-r--r--src/main/kotlin/moe/nea/ledger/database/DBSchema.kt545
-rw-r--r--src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt68
-rw-r--r--src/main/kotlin/moe/nea/ledger/database/Database.kt54
-rw-r--r--src/main/kotlin/moe/nea/ledger/database/Upgrades.kt20
-rw-r--r--src/main/kotlin/moe/nea/ledger/database/schema.dot23
6 files changed, 0 insertions, 729 deletions
diff --git a/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt b/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt
deleted file mode 100644
index 77ac215..0000000
--- a/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package moe.nea.ledger.database
-
-import moe.nea.ledger.ItemChange
-import moe.nea.ledger.ItemId
-import moe.nea.ledger.TransactionType
-
-object DBLogEntry : Table("LogEntry") {
- val transactionId = column("transactionId", DBUlid)
- val type = column("type", DBEnum<TransactionType>())
- val profileId = column("profileId", DBUuid)
- val playerId = column("playerId", DBUuid)
-}
-
-object DBItemEntry : Table("ItemEntry") {
- val transactionId = column("transactionId", DBUlid) // TODO: add foreign keys
- val mode = column("mode", DBEnum<ItemChange.ChangeDirection>())
- val itemId = column("item", DBString.mapped(ItemId::string, ::ItemId))
- val size = column("size", DBDouble)
-}
diff --git a/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt b/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt
deleted file mode 100644
index 492f261..0000000
--- a/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt
+++ /dev/null
@@ -1,545 +0,0 @@
-package moe.nea.ledger.database
-
-import moe.nea.ledger.UUIDUtil
-import java.sql.Connection
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.time.Instant
-import java.util.UUID
-
-interface DBSchema {
- val tables: List<Table>
-}
-
-interface DBType<T> {
- val dbType: String
-
- fun get(result: ResultSet, index: Int): T
- fun set(stmt: PreparedStatement, index: Int, value: T)
- fun getName(): String = javaClass.simpleName
- fun <R> mapped(
- from: (R) -> T,
- to: (T) -> R,
- ): DBType<R> {
- return object : DBType<R> {
- override fun getName(): String {
- return "Mapped(${this@DBType.getName()})"
- }
-
- override val dbType: String
- get() = this@DBType.dbType
-
- override fun get(result: ResultSet, index: Int): R {
- return to(this@DBType.get(result, index))
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: R) {
- this@DBType.set(stmt, index, from(value))
- }
- }
- }
-}
-
-object DBUuid : DBType<UUID> {
- override val dbType: String
- get() = "TEXT"
-
- override fun get(result: ResultSet, index: Int): UUID {
- return UUIDUtil.parseDashlessUuid(result.getString(index))
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: UUID) {
- stmt.setString(index, value.toString())
- }
-}
-
-object DBUlid : DBType<UUIDUtil.ULIDWrapper> {
- override val dbType: String
- get() = "TEXT"
-
- override fun get(result: ResultSet, index: Int): UUIDUtil.ULIDWrapper {
- val text = result.getString(index)
- return UUIDUtil.ULIDWrapper(text)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: UUIDUtil.ULIDWrapper) {
- stmt.setString(index, value.wrapped)
- }
-}
-
-object DBString : DBType<String> {
- override val dbType: String
- get() = "TEXT"
-
- override fun get(result: ResultSet, index: Int): String {
- return result.getString(index)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: String) {
- stmt.setString(index, value)
- }
-}
-
-class DBEnum<T : Enum<T>>(
- val type: Class<T>,
-) : DBType<T> {
- companion object {
- inline operator fun <reified T : Enum<T>> invoke(): DBEnum<T> {
- return DBEnum(T::class.java)
- }
- }
-
- override val dbType: String
- get() = "TEXT"
-
- override fun getName(): String {
- return "DBEnum(${type.simpleName})"
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: T) {
- stmt.setString(index, value.name)
- }
-
- override fun get(result: ResultSet, index: Int): T {
- val name = result.getString(index)
- return java.lang.Enum.valueOf(type, name)
- }
-}
-
-object DBDouble : DBType<Double> {
- override val dbType: String
- get() = "DOUBLE"
-
- override fun get(result: ResultSet, index: Int): Double {
- return result.getDouble(index)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: Double) {
- stmt.setDouble(index, value)
- }
-}
-
-object DBInt : DBType<Long> {
- override val dbType: String
- get() = "INTEGER"
-
- override fun get(result: ResultSet, index: Int): Long {
- return result.getLong(index)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: Long) {
- stmt.setLong(index, value)
- }
-}
-
-object DBInstant : DBType<Instant> {
- override val dbType: String
- get() = "INTEGER"
-
- override fun set(stmt: PreparedStatement, index: Int, value: Instant) {
- stmt.setLong(index, value.toEpochMilli())
- }
-
- override fun get(result: ResultSet, index: Int): Instant {
- return Instant.ofEpochMilli(result.getLong(index))
- }
-}
-
-class Column<T> @Deprecated("Use Table.column instead") constructor(
- val table: Table,
- val name: String,
- val type: DBType<T>
-) {
- val sqlName get() = "`$name`"
- val qualifiedSqlName get() = table.sqlName + "." + sqlName
-}
-
-interface Constraint {
- val affectedColumns: Collection<Column<*>>
- fun asSQL(): String
-}
-
-class UniqueConstraint(val columns: List<Column<*>>) : Constraint {
- init {
- require(columns.isNotEmpty())
- }
-
- override val affectedColumns: Collection<Column<*>>
- get() = columns
-
- override fun asSQL(): String {
- return "UNIQUE (${columns.joinToString() { it.sqlName }})"
- }
-}
-
-abstract class Table(val name: String) {
- val sqlName get() = "`$name`"
- protected val _mutable_columns: MutableList<Column<*>> = mutableListOf()
- protected val _mutable_constraints: MutableList<Constraint> = mutableListOf()
- val columns: List<Column<*>> get() = _mutable_columns
- val constraints get() = _mutable_constraints
- protected fun unique(vararg columns: Column<*>) {
- _mutable_constraints.add(UniqueConstraint(columns.toList()))
- }
-
- protected fun <T> column(name: String, type: DBType<T>): Column<T> {
- @Suppress("DEPRECATION") val column = Column(this, name, type)
- _mutable_columns.add(column)
- return column
- }
-
- fun debugSchema() {
- val nameWidth = columns.maxOf { it.name.length }
- val typeWidth = columns.maxOf { it.type.getName().length }
- val totalWidth = maxOf(2 + nameWidth + 3 + typeWidth + 2, name.length + 4)
- val adjustedTypeWidth = totalWidth - nameWidth - 2 - 3 - 2
-
- var string = "\n"
- string += ("+" + "-".repeat(totalWidth - 2) + "+\n")
- string += ("| $name${" ".repeat(totalWidth - 4 - name.length)} |\n")
- string += ("+" + "-".repeat(totalWidth - 2) + "+\n")
- for (column in columns) {
- string += ("| ${column.name}${" ".repeat(nameWidth - column.name.length)} |")
- string += (" ${column.type.getName()}" +
- "${" ".repeat(adjustedTypeWidth - column.type.getName().length)} |\n")
- }
- string += ("+" + "-".repeat(totalWidth - 2) + "+")
- println(string)
- }
-
- fun createIfNotExists(
- connection: Connection,
- filteredColumns: List<Column<*>> = columns
- ) {
- val properties = mutableListOf<String>()
- for (column in filteredColumns) {
- properties.add("${column.sqlName} ${column.type.dbType}")
- }
- val columnSet = filteredColumns.toSet()
- for (constraint in constraints) {
- if (columnSet.containsAll(constraint.affectedColumns)) {
- properties.add(constraint.asSQL())
- }
- }
- connection.prepareAndLog("CREATE TABLE IF NOT EXISTS $sqlName (" + properties.joinToString() + ")")
- .execute()
- }
-
- fun alterTableAddColumns(
- connection: Connection,
- newColumns: List<Column<*>>
- ) {
- for (column in newColumns) {
- connection.prepareAndLog("ALTER TABLE $sqlName ADD ${column.sqlName} ${column.type.dbType}")
- .execute()
- }
- for (constraint in constraints) {
- // TODO: automatically add constraints, maybe (or maybe move constraints into the upgrade schema)
- }
- }
-
- enum class OnConflict {
- FAIL,
- IGNORE,
- REPLACE,
- ;
-
- fun asSql(): String {
- return name
- }
- }
-
- fun insert(connection: Connection, onConflict: OnConflict = OnConflict.FAIL, block: (InsertStatement) -> Unit) {
- val insert = InsertStatement(HashMap())
- block(insert)
- require(insert.properties.keys == columns.toSet())
- val columnNames = columns.joinToString { it.sqlName }
- val valueNames = columns.joinToString { "?" }
- val statement =
- connection.prepareAndLog("INSERT OR ${onConflict.asSql()} INTO $sqlName ($columnNames) VALUES ($valueNames)")
- for ((index, column) in columns.withIndex()) {
- (column as Column<Any>).type.set(statement, index + 1, insert.properties[column]!!)
- }
- statement.execute()
- }
-
- fun from(connection: Connection): Query {
- return Query(connection, mutableListOf(), this)
- }
-
- fun selectAll(connection: Connection): Query {
- return Query(connection, columns.toMutableList(), this)
- }
-}
-
-class InsertStatement(val properties: MutableMap<Column<*>, Any>) {
- operator fun <T : Any> set(key: Column<T>, value: T) {
- properties[key] = value
- }
-}
-
-fun Connection.prepareAndLog(statement: String): PreparedStatement {
- println("Preparing to execute $statement")
- return prepareStatement(statement)
-}
-
-interface SQLQueryComponent {
- fun asSql(): String
-
- /**
- * @return the next writable index (should equal to the amount of `?` in [asSql] + [startIndex])
- */
- fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int
-
- companion object {
- fun standalone(sql: String): SQLQueryComponent {
- return object : SQLQueryComponent {
- override fun asSql(): String {
- return sql
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- return startIndex
- }
- }
- }
- }
-}
-
-interface BooleanExpression : SQLQueryComponent
-
-data class ORExpression(
- val elements: List<BooleanExpression>
-) : BooleanExpression {
- init {
- require(elements.isNotEmpty())
- }
-
- override fun asSql(): String {
- return (elements + SQLQueryComponent.standalone("FALSE")).joinToString(" OR ", "(", ")") { it.asSql() }
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- for (element in elements) {
- index = element.appendToStatement(stmt, index)
- }
- return index
- }
-}
-
-
-data class ANDExpression(
- val elements: List<BooleanExpression>
-) : BooleanExpression {
- init {
- require(elements.isNotEmpty())
- }
-
- override fun asSql(): String {
- return (elements + SQLQueryComponent.standalone("TRUE")).joinToString(" AND ", "(", ")") { it.asSql() }
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- for (element in elements) {
- index = element.appendToStatement(stmt, index)
- }
- return index
- }
-}
-
-class ClauseBuilder {
- fun <T> column(column: Column<T>): Operand<T> = Operand.ColumnOperand(column)
- fun string(string: String): Operand.StringOperand = Operand.StringOperand(string)
- infix fun Operand<*>.eq(operand: Operand<*>) = Clause.EqualsClause(this, operand)
- infix fun Operand<*>.like(op: Operand.StringOperand) = Clause.LikeClause(this, op)
- infix fun Operand<*>.like(op: String) = Clause.LikeClause(this, string(op))
-}
-
-interface Clause : BooleanExpression {
- companion object {
- operator fun invoke(builder: ClauseBuilder.() -> Clause): Clause {
- return builder(ClauseBuilder())
- }
- }
-
- data class EqualsClause(val left: Operand<*>, val right: Operand<*>) : Clause { // TODO: typecheck this somehow
- override fun asSql(): String {
- return left.asSql() + " = " + right.asSql()
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- index = left.appendToStatement(stmt, index)
- index = right.appendToStatement(stmt, index)
- return index
- }
- }
-
- data class LikeClause<T>(val left: Operand<T>, val right: Operand.StringOperand) : Clause {
- //TODO: check type safety with this one
- override fun asSql(): String {
- return "(" + left.asSql() + " LIKE " + right.asSql() + ")"
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- index = left.appendToStatement(stmt, index)
- index = right.appendToStatement(stmt, index)
- return index
- }
- }
-}
-
-interface Operand<T> : SQLQueryComponent {
- data class ColumnOperand<T>(val column: Column<T>) : Operand<T> {
- override fun asSql(): String {
- return column.qualifiedSqlName
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- return startIndex
- }
- }
-
- data class StringOperand(val value: String) : Operand<String> {
- override fun asSql(): String {
- return "?"
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- stmt.setString(startIndex, value)
- return 1 + startIndex
- }
- }
-}
-
-data class Join(
- val table: Table,
-//TODO: aliased columns val tableAlias: String,
- val filter: Clause,
-) : SQLQueryComponent {
- // JOIN ItemEntry on LogEntry.transactionId = ItemEntry.transactionId
- override fun asSql(): String {
- return "JOIN ${table.sqlName} ON ${filter.asSql()}"
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- return filter.appendToStatement(stmt, startIndex)
- }
-}
-
-fun List<SQLQueryComponent>.concatToFilledPreparedStatement(connection: Connection): PreparedStatement {
- var query = ""
- for (element in this) {
- if (query.isNotEmpty()) {
- query += " "
- }
- query += element.asSql()
- }
- val statement = connection.prepareAndLog(query)
- var index = 1
- for (element in this) {
- val nextIndex = element.appendToStatement(statement, index)
- if (nextIndex < index) error("$element went back in time")
- index = nextIndex
- }
- return statement
-}
-
-class Query(
- val connection: Connection,
- val selectedColumns: MutableList<Column<*>>,
- var table: Table,
- var limit: UInt? = null,
- var skip: UInt? = null,
- val joins: MutableList<Join> = mutableListOf(),
- val conditions: MutableList<BooleanExpression> = mutableListOf(),
-// var order: OrderClause?= null,
-) : Iterable<ResultRow> {
- fun join(table: Table, on: Clause): Query {
- joins.add(Join(table, on))
- return this
- }
-
- fun where(binOp: BooleanExpression): Query {
- conditions.add(binOp)
- return this
- }
-
- fun select(vararg columns: Column<*>): Query {
- selectedColumns.addAll(columns)
- return this
- }
-
- fun skip(skip: UInt): Query {
- require(limit != null)
- this.skip = skip
- return this
- }
-
- fun limit(limit: UInt): Query {
- this.limit = limit
- return this
- }
-
- override fun iterator(): Iterator<ResultRow> {
- val columnSelections = selectedColumns.joinToString { it.qualifiedSqlName }
- val elements = mutableListOf(
- SQLQueryComponent.standalone("SELECT $columnSelections FROM ${table.sqlName}"),
- )
- elements.addAll(joins)
- if (conditions.any()) {
- elements.add(SQLQueryComponent.standalone("WHERE"))
- elements.add(ANDExpression(conditions))
- }
- if (limit != null) {
- elements.add(SQLQueryComponent.standalone("LIMIT $limit"))
- if (skip != null) {
- elements.add(SQLQueryComponent.standalone("OFFSET $skip"))
- }
- }
- val prepared = elements.concatToFilledPreparedStatement(connection)
- val results = prepared.executeQuery()
- return object : Iterator<ResultRow> {
- var hasAdvanced = false
- var hasEnded = false
- override fun hasNext(): Boolean {
- if (hasEnded) return false
- if (hasAdvanced) return true
- if (results.next()) {
- hasAdvanced = true
- return true
- } else {
- results.close() // TODO: somehow enforce closing this
- hasEnded = true
- return false
- }
- }
-
- override fun next(): ResultRow {
- if (!hasNext()) {
- throw NoSuchElementException()
- }
- hasAdvanced = false
- return ResultRow(selectedColumns.withIndex().associate {
- it.value to it.value.type.get(results, it.index + 1)
- })
- }
-
- }
- }
-}
-
-class ResultRow(val columnValues: Map<Column<*>, *>) {
- operator fun <T> get(column: Column<T>): T {
- val value = columnValues[column]
- ?: error("Invalid column ${column.name}. Only ${columnValues.keys.joinToString { it.name }} are available.")
- return value as T
- }
-}
-
-
-
-
diff --git a/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt b/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt
deleted file mode 100644
index 7d1782a..0000000
--- a/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package moe.nea.ledger.database
-
-import java.sql.Connection
-
-interface DBUpgrade {
- val toVersion: Long
- val fromVersion get() = toVersion - 1
- fun performUpgrade(connection: Connection)
-
- companion object {
-
- fun performUpgrades(
- connection: Connection,
- upgrades: Iterable<DBUpgrade>,
- ) {
- for (upgrade in upgrades) {
- upgrade.performUpgrade(connection)
- }
- }
-
- fun performUpgradeChain(
- connection: Connection,
- from: Long, to: Long,
- upgrades: Iterable<DBUpgrade>,
- afterEach: (newVersion: Long) -> Unit,
- ) {
- val table = buildLookup(upgrades)
- for (version in (from + 1)..(to)) {
- val currentUpgrades = table[version] ?: listOf()
- println("Scheduled ${currentUpgrades.size} upgrades to reach DB version $version")
- performUpgrades(connection, currentUpgrades)
- afterEach(version)
- }
- }
-
- fun buildLookup(upgrades: Iterable<DBUpgrade>): Map<Long, List<DBUpgrade>> {
- return upgrades.groupBy { it.toVersion }
- }
-
- fun createTable(to: Long, table: Table, vararg columns: Column<*>): DBUpgrade {
- require(columns.all { it in table.columns })
- return of("Create table ${table}", to) {
- table.createIfNotExists(it, columns.toList())
- }
- }
-
- fun addColumns(to: Long, table: Table, vararg columns: Column<*>): DBUpgrade {
- return of("Add columns to table $table", to) {
- table.alterTableAddColumns(it, columns.toList())
- }
- }
-
- fun of(name: String, to: Long, block: (Connection) -> Unit): DBUpgrade {
- return object : DBUpgrade {
- override val toVersion: Long
- get() = to
-
- override fun performUpgrade(connection: Connection) {
- block(connection)
- }
-
- override fun toString(): String {
- return name
- }
- }
- }
- }
-} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/database/Database.kt b/src/main/kotlin/moe/nea/ledger/database/Database.kt
deleted file mode 100644
index a77ea30..0000000
--- a/src/main/kotlin/moe/nea/ledger/database/Database.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package moe.nea.ledger.database
-
-import moe.nea.ledger.Ledger
-import java.sql.Connection
-import java.sql.DriverManager
-
-class Database {
- lateinit var connection: Connection
-
- object MetaTable : Table("LedgerMeta") {
- val key = column("key", DBString)
- val value = column("value", DBString)
-
- init {
- unique(key)
- }
- }
-
- data class MetaKey(val name: String) {
- companion object {
- val DATABASE_VERSION = MetaKey("databaseVersion")
- val LAST_LAUNCH = MetaKey("lastLaunch")
- }
- }
-
- fun setMetaKey(key: MetaKey, value: String) {
- MetaTable.insert(connection, Table.OnConflict.REPLACE) {
- it[MetaTable.key] = key.name
- it[MetaTable.value] = value
- }
- }
-
- val databaseVersion: Long = 1
-
- fun loadAndUpgrade() {
- connection = DriverManager.getConnection("jdbc:sqlite:${Ledger.dataFolder.resolve("database.db")}")
- MetaTable.createIfNotExists(connection)
- val meta = MetaTable.selectAll(connection).associate { MetaKey(it[MetaTable.key]) to it[MetaTable.value] }
- val lastLaunch = meta[MetaKey.LAST_LAUNCH]?.toLong() ?: 0L
- println("Last launch $lastLaunch")
- setMetaKey(MetaKey.LAST_LAUNCH, System.currentTimeMillis().toString())
-
- val oldVersion = meta[MetaKey.DATABASE_VERSION]?.toLong() ?: -1
- println("Old Database Version: $oldVersion; Current version: $databaseVersion")
- // TODO: create a backup if there is a db version upgrade happening
- DBUpgrade.performUpgradeChain(
- connection, oldVersion, databaseVersion,
- Upgrades().upgrades
- ) { version ->
- setMetaKey(MetaKey.DATABASE_VERSION, version.toString())
- }
- }
-
-} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt b/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt
deleted file mode 100644
index e83abe7..0000000
--- a/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package moe.nea.ledger.database
-
-class Upgrades {
- val upgrades = mutableListOf<DBUpgrade>()
-
- fun add(upgrade: DBUpgrade) = upgrades.add(upgrade)
-
- init {
- add(DBUpgrade.createTable(
- 0, DBLogEntry,
- DBLogEntry.type, DBLogEntry.playerId, DBLogEntry.profileId,
- DBLogEntry.transactionId))
- add(DBUpgrade.createTable(
- 0, DBItemEntry,
- DBItemEntry.itemId, DBItemEntry.size, DBItemEntry.mode, DBItemEntry.transactionId
- ))
- }
-
-
-} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/database/schema.dot b/src/main/kotlin/moe/nea/ledger/database/schema.dot
deleted file mode 100644
index d932f6a..0000000
--- a/src/main/kotlin/moe/nea/ledger/database/schema.dot
+++ /dev/null
@@ -1,23 +0,0 @@
-digraph {
- node [shape=plain];
- rankdir=LR;
- entry [label=<
- <table border="0" cellborder="1" cellspacing="0">
- <tr><td>Log Entry</td></tr>
- <tr><td port="player">playerId</td></tr>
- <tr><td port="profile">profileId</td></tr>
- <tr><td port="date">timestamp</td></tr>
- <tr><td port="type">Type</td></tr>
- </table>
- >];
- item [label=<
- <table border="0" cellborder="1" cellspacing="0">
- <tr><td>Item Stack</td><tr>
- <tr><td port="transaction">Transaction</td></tr>
- <tr><td port="id">Item ID</td></tr>
- <tr><td port="count">Count</td></tr>
- <tr><td port="direction">Transfer Direction</td></tr>
- </table>
- >];
-// item:transaction -> entry;
-} \ No newline at end of file