Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Handle the bytea data received to be stored correctly in the database. #35

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.wiringbits.spra.admin.models

sealed trait FieldValue[T] {
val value: T
}

case class StringValue(value: String) extends FieldValue[String]
case class ByteArrayValue(value: Array[Byte]) extends FieldValue[Array[Byte]]
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import net.wiringbits.spra.admin.repositories.daos.DatabaseTablesDAO
import net.wiringbits.spra.admin.repositories.models.{DatabaseTable, ForeignKey, TableColumn, TableData}
import net.wiringbits.spra.admin.utils.models.QueryParameters
import play.api.db.Database
import net.wiringbits.spra.admin.models.{ByteArrayValue, StringValue}
import net.wiringbits.spra.admin.utils.StringParse

import javax.inject.Inject
import scala.concurrent.Future
Expand Down Expand Up @@ -75,7 +77,10 @@ class DatabaseTablesRepository @Inject() (database: Database)(implicit
val fieldsAndValues = body.map { case (key, value) =>
val field =
columns.find(_.name == key).getOrElse(throw new RuntimeException(s"Invalid property in body request: $key"))
(field, value)
if (field.`type` == "bytea")
Antonio171003 marked this conversation as resolved.
Show resolved Hide resolved
val byteaValue = StringParse.stringToByteArray(value)
(field, ByteArrayValue(byteaValue))
else (field, StringValue(value))
}
DatabaseTablesDAO.create(
tableName = tableName,
Expand All @@ -100,7 +105,10 @@ class DatabaseTablesRepository @Inject() (database: Database)(implicit
val fieldsAndValues = bodyWithoutNonEditableColumns.map { case (key, value) =>
val field =
columns.find(_.name == key).getOrElse(throw new RuntimeException(s"Invalid property in body request: $key"))
(field, value)
if (field.`type` == "bytea")
val byteaValue = StringParse.stringToByteArray(value)
(field, ByteArrayValue(byteaValue))
else (field, StringValue(value))
}
val primaryKeyType = settings.primaryKeyDataType
DatabaseTablesDAO.update(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import net.wiringbits.spra.admin.config.{CustomDataType, PrimaryKeyDataType, Tab
import net.wiringbits.spra.admin.repositories.models.*
import net.wiringbits.spra.admin.utils.models.{FilterParameter, QueryParameters}
import net.wiringbits.spra.admin.utils.{QueryBuilder, StringRegex}

import net.wiringbits.spra.admin.models.{ByteArrayValue, FieldValue, StringValue}
import java.sql.{Connection, Date, PreparedStatement, ResultSet}
import java.time.LocalDate
import java.util.UUID
Expand Down Expand Up @@ -230,7 +230,7 @@ object DatabaseTablesDAO {
}
def create(
tableName: String,
fieldsAndValues: Map[TableColumn, String],
fieldsAndValues: Map[TableColumn, FieldValue[_]],
primaryKeyField: String,
primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID
)(implicit
Expand All @@ -250,7 +250,7 @@ object DatabaseTablesDAO {
// Postgres: INSERT INTO test_serial (id) VALUES(DEFAULT); MySQL: INSERT INTO table (id) VALUES(NULL)

for (j <- i + 1 to fieldsAndValues.size + i) {
val value = fieldsAndValues(fieldsAndValues.keys.toList(j - i - 1))
val value = fieldsAndValues(fieldsAndValues.keys.toList(j - i - 1)).value
preparedStatement.setObject(j, value)
}
val result = preparedStatement.executeQuery()
Expand All @@ -260,17 +260,17 @@ object DatabaseTablesDAO {

def update(
tableName: String,
fieldsAndValues: Map[TableColumn, String],
fieldsAndValues: Map[TableColumn, FieldValue[_]],
primaryKeyField: String,
primaryKeyValue: String,
primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID
)(implicit conn: Connection): Unit = {
val sql = QueryBuilder.update(tableName, fieldsAndValues, primaryKeyField)
val preparedStatement = conn.prepareStatement(sql)

val notNullData = fieldsAndValues.filterNot { case (_, value) => value == "null" }
notNullData.zipWithIndex.foreach { case ((_, value), i) =>
preparedStatement.setObject(i + 1, value)
val notNullData = fieldsAndValues.filterNot { case (_, field) => field.value == "null" }
notNullData.zipWithIndex.foreach { case ((_, field), i) =>
preparedStatement.setObject(i + 1, field.value)
}
// where ... = ?
setPreparedStatementKey(preparedStatement, primaryKeyValue, primaryKeyType, notNullData.size + 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package net.wiringbits.spra.admin.utils

import net.wiringbits.spra.admin.config.PrimaryKeyDataType
import net.wiringbits.spra.admin.repositories.models.TableColumn

import net.wiringbits.spra.admin.models.FieldValue
import scala.collection.mutable

object QueryBuilder {
def create(
tableName: String,
fieldsAndValues: Map[TableColumn, String],
fieldsAndValues: Map[TableColumn, FieldValue[_]],
primaryKeyField: String,
primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID
): String = {
Expand All @@ -33,10 +33,10 @@ object QueryBuilder {
|""".stripMargin
}

def update(tableName: String, body: Map[TableColumn, String], primaryKeyField: String): String = {
def update(tableName: String, body: Map[TableColumn, FieldValue[_]], primaryKeyField: String): String = {
val updateStatement = new mutable.StringBuilder("SET")
for ((tableField, value) <- body) {
val resultStatement = if (value == "null") "NULL" else s"?::${tableField.`type`}"
for ((tableField, field) <- body) {
val resultStatement = if (field.value == "null") "NULL" else s"?::${tableField.`type`}"
val statement = s" ${tableField.name} = $resultStatement,"
updateStatement.append(statement)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.wiringbits.spra.admin.utils

import scala.util.{Failure, Success, Try}

object StringParse {
def stringToByteArray(value: String): Array[Byte] = {
// Removes whitespace characters (\\s) and brackets ([, ]) to prepare the string for byte array conversion
Try(value.replaceAll("[\\[\\]\\s]", "").split(",").map(_.toByte)) match
case Success(value) => value
case Failure(_) => Array.emptyByteArray
}
}
7 changes: 7 additions & 0 deletions spra-play-server/src/test/resources/evolutions/default/3.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

-- !Ups

CREATE TABLE bytea_table (
id UUID NOT NULL PRIMARY KEY,
data BYTEA NOT NULL
);
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class AdminControllerSpec extends PlayPostgresSpec with AdminUtils {
tableName = "big_serial_table_overflow",
primaryKeyField = "id",
primaryKeyDataType = PrimaryKeyDataType.BigSerial
),
TableSettings(
tableName = "bytea_table",
primaryKeyField = "id"
)
)
val dataExplorerConfig: DataExplorerConfig = DataExplorerConfig("http://localhost:9000", dataExplorerConfigTables)
Expand All @@ -50,6 +54,7 @@ class AdminControllerSpec extends PlayPostgresSpec with AdminUtils {
def bigSerialSettings: TableSettings = dataExplorerConfig.tablesSettings(4)
def serialOverflowSettings: TableSettings = dataExplorerConfig.tablesSettings(5)
def bigSerialOverflowSettings: TableSettings = dataExplorerConfig.tablesSettings(6)
def byteaSettings: TableSettings = dataExplorerConfig.tablesSettings(7)

def isValidUUID(str: String): Boolean = {
if (str == null) return false
Expand All @@ -70,12 +75,13 @@ class AdminControllerSpec extends PlayPostgresSpec with AdminUtils {
"return tables from modules" in withApiClient { client =>
val response = client.getTables.futureValue
response.data.map(_.name) match
case List(users, userLogs, uuidTable, serialTable, bigSerialTable, _, _) =>
case List(users, userLogs, uuidTable, serialTable, bigSerialTable, _, _, byteaTable) =>
users must be(usersSettings.tableName)
userLogs must be(userLogsSettings.tableName)
uuidTable must be(uuidSettings.tableName)
serialTable must be(serialSettings.tableName)
bigSerialTable must be(bigSerialSettings.tableName)
byteaTable must be(byteaSettings.tableName)
case list => fail(s"Unexpected response: ${list.mkString(", ")}")
}

Expand Down Expand Up @@ -135,6 +141,17 @@ class AdminControllerSpec extends PlayPostgresSpec with AdminUtils {
bigSerialSettings.filterableColumns must be(List.empty)
bigSerialSettings.createSettings.nonRequiredColumns must be(List.empty)
bigSerialSettings.createSettings.requiredColumns must be(List.empty)

val head6 = response.data(5)
head6.primaryKeyName must be(byteaSettings.primaryKeyField)
byteaSettings.referenceField must be(None)
byteaSettings.hiddenColumns must be(List.empty)
byteaSettings.nonEditableColumns must be(List.empty)
byteaSettings.canBeDeleted must be(true)
byteaSettings.columnTypeOverrides must be(Map.empty)
byteaSettings.filterableColumns must be(List.empty)
byteaSettings.createSettings.nonRequiredColumns must be(List.empty)
byteaSettings.createSettings.requiredColumns must be(List.empty)
}
}

Expand Down Expand Up @@ -738,6 +755,18 @@ class AdminControllerSpec extends PlayPostgresSpec with AdminUtils {
s"ERROR: nextval: reached maximum value of sequence \"big_serial_table_overflow_seq\" (9223372036854775807)"
)
}

"create a new bytea" in withApiClient { implicit client =>
val stringBytea = "[0, 10, 20, 30]"
// The response returns the bytea as Hex; this would be its equivalent in Hex.
val correctValue = "\\x000a141e"
val request = AdminCreateTable.Request(Map("data" -> stringBytea))
val byteaId = client.createItem(byteaSettings.tableName, request).futureValue.id

val response = client.viewItem(byteaSettings.tableName, byteaId).futureValue
val dataResponse = response.find(_._1 == "data").value._2
dataResponse must be(correctValue)
}
}

"fail when field in request doesn't exists" in withApiClient { client =>
Expand Down Expand Up @@ -780,6 +809,22 @@ class AdminControllerSpec extends PlayPostgresSpec with AdminUtils {
emailResponse must be(email)
}

"update a new bytea" in withApiClient { client =>
val request = AdminCreateTable.Request(Map("data" -> "[10, 10, 10, 10]"))
val byteaId = client.createItem(byteaSettings.tableName, request).futureValue.id

val stringBytea = "[0, 10, 20, 30]"
// The response returns the bytea as Hex; this would be its equivalent in Hex.
val correctValue = "\\x000a141e"
val updateRequest = AdminUpdateTable.Request(Map("data" -> stringBytea))
val updateResponse = client.updateItem(byteaSettings.tableName, byteaId, updateRequest).futureValue

val newResponse = client.viewItem(byteaSettings.tableName, byteaId).futureValue
val dataResponse = newResponse.find(_._1 == "data").value._2
updateResponse.id must be(byteaId)
dataResponse must be(correctValue)
}

"update a new row for all tables" in withApiClient { client =>
val tables = List(serialSettings, bigSerialSettings)
for (table <- tables) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.wiringbits.spra.admin

import net.wiringbits.spra.admin.models.{FieldValue, StringValue}
import net.wiringbits.spra.admin.repositories.models.TableColumn
import net.wiringbits.spra.admin.utils.QueryBuilder
import org.scalatest.matchers.must.Matchers.{be, must}
Expand All @@ -20,7 +21,10 @@ class QueryBuilderSpec extends AnyWordSpec {
|""".stripMargin
val tableName = "users"
val body =
Map(TableColumn("email", "citext") -> "[email protected]", TableColumn("name", "text") -> "wiringbits")
Map(
TableColumn("email", "citext") -> StringValue("[email protected]"),
TableColumn("name", "text") -> StringValue("wiringbits")
)
val primaryKeyField = "user_id"

val response = QueryBuilder.create(tableName, body, primaryKeyField)
Expand All @@ -40,7 +44,7 @@ class QueryBuilderSpec extends AnyWordSpec {
|RETURNING user_id::TEXT
|""".stripMargin
val tableName = "users"
val body = Map.empty[TableColumn, String]
val body = Map.empty[TableColumn, FieldValue[_]]
val primaryKeyField = "user_id"

val response = QueryBuilder.create(tableName, body, primaryKeyField)
Expand All @@ -58,8 +62,8 @@ class QueryBuilderSpec extends AnyWordSpec {
|""".stripMargin
val tableName = "users"
val body = Map(
TableColumn("email", "citext") -> "[email protected]",
TableColumn("name", "text") -> "[email protected]"
TableColumn("email", "citext") -> StringValue("[email protected]"),
TableColumn("name", "text") -> StringValue("[email protected]")
)
val primaryKeyField = "user_id"

Expand All @@ -76,9 +80,9 @@ class QueryBuilderSpec extends AnyWordSpec {
|""".stripMargin
val tableName = "users"
val body = Map(
TableColumn("email", "citext") -> "[email protected]",
TableColumn("name", "text") -> "[email protected]",
TableColumn("phone_number", "text") -> "null"
TableColumn("email", "citext") -> StringValue("[email protected]"),
TableColumn("name", "text") -> StringValue("[email protected]"),
TableColumn("phone_number", "text") -> StringValue("null")
)
val primaryKeyField = "user_id"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.wiringbits.spra.admin

import net.wiringbits.spra.admin.utils.StringParse.stringToByteArray
import org.scalatest.matchers.must.Matchers.{be, must}
import org.scalatest.wordspec.AnyWordSpec

class StringParseSpec extends AnyWordSpec {
"dataParse" should {
val data = List(
("[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", Array[Byte](0, 1, 2, 3, 4, 5, 6, 7, 8, 9)),
("[-128, -64, 0, 64, 127]", Array[Byte](-128, -64, 0, 64, 127)),
("[10, 20, 30, 40, 50]", Array[Byte](10, 20, 30, 40, 50)),
("[127, -127, 127, -127]", Array[Byte](127, -127, 127, -127))
)

data.foreach { (data, valid) =>
s"accept valid conversion: $data" in {
stringToByteArray(data) must be(valid)
}
}
}
}
Loading