Skip to content

Commit ce07a04

Browse files
authored
Fix SQL parsing when creating and tests (#20)
* Fix UUID field references * Tests * Address reviewer changes * Fix create for all types * Fix QueryBuild tests
1 parent f3c951f commit ce07a04

File tree

14 files changed

+161
-44
lines changed

14 files changed

+161
-44
lines changed

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ inThisBuild(
2727
resolvers += Resolver.sonatypeRepo("releases")
2828

2929
val play = "2.9.0-M6"
30-
val playJson = "2.10.0-RC9"
30+
val playJson = "2.10.1"
3131
val sttp = "3.5.0"
3232
val anorm = "2.7.0"
3333
val scalaTestPlusPlay = "6.0.0-M6"

spra-api/shared/src/main/scala/net/wiringbits/spra/api/AdminDataExplorerApiClient.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ object AdminDataExplorerApiClient {
142142

143143
prepareRequest[AdminCreateTable.Response]
144144
.post(uri)
145-
.body(Json.toJson(request).toString())
145+
// TODO: check the AdminCreateTable.Request model, it isn't parsing correctly to json
146+
.body(Json.toJson(request.data).toString())
146147
.send(backend)
147148
.map(_.body)
148149
.flatMap(Future.fromTry)

spra-api/shared/src/main/scala/net/wiringbits/spra/api/models/AdminCreateTable.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import play.api.libs.json.{Format, Json}
55
object AdminCreateTable {
66
case class Request(data: Map[String, String])
77

8-
case class Response(noData: String = "")
8+
case class Response(id: String)
99

1010
implicit val adminCreateTableRequestFormat: Format[Request] =
1111
Json.format[Request]

spra-play-server/src/main/resources/application.conf

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,18 @@ dataExplorer {
1515
filterableColumns = ["name", "last_name"]
1616
createFilter {
1717
requiredColumns = ["name", "email", "password"]
18-
nonRequiredColumns = ["last_name", "discord_webhook_url"]
18+
nonRequiredColumns = ["last_name"]
19+
}
20+
}
21+
22+
userLogs {
23+
tableName = "user_logs"
24+
primaryKeyField = "user_log_id"
25+
nonEditableColumns = ["user_log_id", "user_id", "created_at"]
26+
canBeDeleted = true
27+
filterableColumns = ["user_id"]
28+
createFilter {
29+
requiredColumns = ["user_id", "message"]
1930
}
2031
}
2132
}

spra-play-server/src/main/scala/net/wiringbits/spra/admin/controllers/AdminController.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class AdminController @Inject() (
6060
_ <- adminUser(request)
6161
_ = logger.info(s"Create row in $tableName: ${body.data}")
6262
id <- adminService.create(tableName, body)
63-
} yield Ok(Json.toJson(Map("id" -> id)))
63+
} yield Ok(Json.toJson(AdminCreateTable.Response(id)))
6464
}
6565

6666
def update(tableName: String, primaryKeyValue: String) = handleJsonBody[Map[String, String]] { request =>

spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,15 @@ class DatabaseTablesRepository @Inject() (database: Database)(implicit
7171
database.withConnection { implicit conn =>
7272
val primaryKeyField = dataExplorerConfig.unsafeFindByName(tableName).primaryKeyField
7373
val primaryKeyType = dataExplorerConfig.unsafeFindByName(tableName).primaryKeyDataType
74+
val columns = DatabaseTablesDAO.getTableColumns(tableName)
75+
val fieldsAndValues = body.map { case (key, value) =>
76+
val field =
77+
columns.find(_.name == key).getOrElse(throw new RuntimeException(s"Invalid property in body request: $key"))
78+
(field, value)
79+
}
7480
DatabaseTablesDAO.create(
7581
tableName = tableName,
76-
body = body,
82+
fieldsAndValues = fieldsAndValues,
7783
primaryKeyField = primaryKeyField,
7884
primaryKeyType = primaryKeyType
7985
)

spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,13 @@ object DatabaseTablesDAO {
230230
}
231231
def create(
232232
tableName: String,
233-
body: Map[String, String],
233+
fieldsAndValues: Map[TableColumn, String],
234234
primaryKeyField: String,
235235
primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID
236236
)(implicit
237237
conn: Connection
238238
): String = {
239-
val sql = QueryBuilder.create(tableName, body, primaryKeyField, primaryKeyType)
239+
val sql = QueryBuilder.create(tableName, fieldsAndValues, primaryKeyField, primaryKeyType)
240240
val preparedStatement = conn.prepareStatement(sql)
241241

242242
var i = 0
@@ -249,8 +249,8 @@ object DatabaseTablesDAO {
249249
// eg. NULL can be used in MySQL to generate default value in an autoincrement column, but not Postgres unfortunately
250250
// Postgres: INSERT INTO test_serial (id) VALUES(DEFAULT); MySQL: INSERT INTO table (id) VALUES(NULL)
251251

252-
for (j <- i + 1 to body.size + i) {
253-
val value = body(body.keys.toList(j - i - 1))
252+
for (j <- i + 1 to fieldsAndValues.size + i) {
253+
val value = fieldsAndValues(fieldsAndValues.keys.toList(j - i - 1))
254254
preparedStatement.setObject(j, value)
255255
}
256256
val result = preparedStatement.executeQuery()

spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import scala.collection.mutable
88
object QueryBuilder {
99
def create(
1010
tableName: String,
11-
body: Map[String, String],
11+
fieldsAndValues: Map[TableColumn, String],
1212
primaryKeyField: String,
1313
primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID
1414
): String = {
@@ -18,9 +18,9 @@ object QueryBuilder {
1818
case PrimaryKeyDataType.Serial => new mutable.StringBuilder("DEFAULT")
1919
case PrimaryKeyDataType.BigSerial => new mutable.StringBuilder("DEFAULT")
2020
}
21-
for ((key, _) <- body) {
22-
sqlFields.append(s", $key")
23-
sqlValues.append(s", ?")
21+
for ((tableColumn, _) <- fieldsAndValues) {
22+
sqlFields.append(s", ${tableColumn.name}")
23+
sqlValues.append(s", ?::${tableColumn.`type`}")
2424
}
2525

2626
s"""

spra-play-server/src/test/scala/controllers/AdminControllerSpec.scala

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import net.wiringbits.spra.api.models.AdminCreateTable
99
import org.apache.commons.lang3.StringUtils
1010
import play.api.inject
1111
import play.api.inject.guice.GuiceApplicationBuilder
12+
import utils.AdminUtils
1213

1314
import java.util.UUID
1415
import java.util.regex.Pattern
1516
import scala.util.Random
1617

17-
class AdminControllerSpec extends PlayPostgresSpec {
18+
class AdminControllerSpec extends PlayPostgresSpec with AdminUtils {
1819
val dataExplorerConfigTables: List[TableSettings] = List(
1920
TableSettings("users", "user_id"), // "UUID" default
21+
TableSettings("user_logs", "user_log_id", Some("users")), // "UUID" default
2022
TableSettings(
2123
tableName = "uuid_table",
2224
primaryKeyField = "id",
@@ -42,11 +44,12 @@ class AdminControllerSpec extends PlayPostgresSpec {
4244
val dataExplorerConfig: DataExplorerConfig = DataExplorerConfig("http://localhost:9000", dataExplorerConfigTables)
4345
def usersSettings: TableSettings = dataExplorerConfig.tablesSettings.headOption.value
4446
// TODO: loop through dataExplorerSettings for each table instead of defining usersSettings, uuidSettings
45-
def uuidSettings: TableSettings = dataExplorerConfig.tablesSettings(1)
46-
def serialSettings: TableSettings = dataExplorerConfig.tablesSettings(2)
47-
def bigSerialSettings: TableSettings = dataExplorerConfig.tablesSettings(3)
48-
def serialOverflowSettings: TableSettings = dataExplorerConfig.tablesSettings(4)
49-
def bigSerialOverflowSettings: TableSettings = dataExplorerConfig.tablesSettings(5)
47+
def userLogsSettings: TableSettings = dataExplorerConfig.tablesSettings(1)
48+
def uuidSettings: TableSettings = dataExplorerConfig.tablesSettings(2)
49+
def serialSettings: TableSettings = dataExplorerConfig.tablesSettings(3)
50+
def bigSerialSettings: TableSettings = dataExplorerConfig.tablesSettings(4)
51+
def serialOverflowSettings: TableSettings = dataExplorerConfig.tablesSettings(5)
52+
def bigSerialOverflowSettings: TableSettings = dataExplorerConfig.tablesSettings(6)
5053

5154
def isValidUUID(str: String): Boolean = {
5255
if (str == null) return false
@@ -66,14 +69,14 @@ class AdminControllerSpec extends PlayPostgresSpec {
6669
"GET /admin/tables" should {
6770
"return tables from modules" in withApiClient { client =>
6871
val response = client.getTables.futureValue
69-
val tableName = response.data.map(_.name).headOption.value // users
70-
tableName must be(usersSettings.tableName)
71-
val uuidTable = response.data.map(_.name)(1) // table 2
72-
uuidTable must be(uuidSettings.tableName)
73-
val serialTable = response.data.map(_.name)(2) // table 3
74-
serialTable must be(serialSettings.tableName)
75-
val bigSerialTable = response.data.map(_.name)(3) // table 4
76-
bigSerialTable must be(bigSerialSettings.tableName)
72+
response.data.map(_.name) match
73+
case List(users, userLogs, uuidTable, serialTable, bigSerialTable, _, _) =>
74+
users must be(usersSettings.tableName)
75+
userLogs must be(userLogsSettings.tableName)
76+
uuidTable must be(uuidSettings.tableName)
77+
serialTable must be(serialSettings.tableName)
78+
bigSerialTable must be(bigSerialSettings.tableName)
79+
case list => fail(s"Unexpected response: ${list.mkString(", ")}")
7780
}
7881

7982
"return extra config from module" in withApiClient { client =>
@@ -83,24 +86,55 @@ class AdminControllerSpec extends PlayPostgresSpec {
8386
usersSettings.referenceField must be(None)
8487
usersSettings.hiddenColumns must be(List.empty)
8588
usersSettings.nonEditableColumns must be(List.empty)
89+
usersSettings.canBeDeleted must be(true)
90+
usersSettings.columnTypeOverrides must be(Map.empty)
91+
usersSettings.filterableColumns must be(List.empty)
92+
usersSettings.createSettings.nonRequiredColumns must be(List.empty)
93+
usersSettings.createSettings.requiredColumns must be(List.empty)
8694

8795
val head2 = response.data(1)
88-
head2.primaryKeyName must be(uuidSettings.primaryKeyField)
96+
head2.primaryKeyName must be(userLogsSettings.primaryKeyField)
97+
userLogsSettings.referenceField must be(Some("users"))
98+
userLogsSettings.hiddenColumns must be(List.empty)
99+
userLogsSettings.nonEditableColumns must be(List.empty)
100+
userLogsSettings.canBeDeleted must be(true)
101+
userLogsSettings.columnTypeOverrides must be(Map.empty)
102+
userLogsSettings.filterableColumns must be(List.empty)
103+
userLogsSettings.createSettings.nonRequiredColumns must be(List.empty)
104+
userLogsSettings.createSettings.requiredColumns must be(List.empty)
105+
106+
val head3 = response.data(2)
107+
head3.primaryKeyName must be(uuidSettings.primaryKeyField)
89108
uuidSettings.referenceField must be(None)
90109
uuidSettings.hiddenColumns must be(List.empty)
91110
uuidSettings.nonEditableColumns must be(List.empty)
111+
uuidSettings.canBeDeleted must be(true)
112+
uuidSettings.columnTypeOverrides must be(Map.empty)
113+
uuidSettings.filterableColumns must be(List.empty)
114+
uuidSettings.createSettings.nonRequiredColumns must be(List.empty)
115+
uuidSettings.createSettings.requiredColumns must be(List.empty)
92116

93-
val head3 = response.data(2)
94-
head3.primaryKeyName must be(serialSettings.primaryKeyField)
117+
val head4 = response.data(3)
118+
head4.primaryKeyName must be(serialSettings.primaryKeyField)
95119
serialSettings.referenceField must be(None)
96120
serialSettings.hiddenColumns must be(List.empty)
97121
serialSettings.nonEditableColumns must be(List.empty)
98-
99-
val head4 = response.data(3)
100-
head4.primaryKeyName must be(bigSerialSettings.primaryKeyField)
122+
serialSettings.canBeDeleted must be(true)
123+
serialSettings.columnTypeOverrides must be(Map.empty)
124+
serialSettings.filterableColumns must be(List.empty)
125+
serialSettings.createSettings.nonRequiredColumns must be(List.empty)
126+
serialSettings.createSettings.requiredColumns must be(List.empty)
127+
128+
val head5 = response.data(4)
129+
head5.primaryKeyName must be(bigSerialSettings.primaryKeyField)
101130
bigSerialSettings.referenceField must be(None)
102131
bigSerialSettings.hiddenColumns must be(List.empty)
103132
bigSerialSettings.nonEditableColumns must be(List.empty)
133+
bigSerialSettings.canBeDeleted must be(true)
134+
bigSerialSettings.columnTypeOverrides must be(Map.empty)
135+
bigSerialSettings.filterableColumns must be(List.empty)
136+
bigSerialSettings.createSettings.nonRequiredColumns must be(List.empty)
137+
bigSerialSettings.createSettings.requiredColumns must be(List.empty)
104138
}
105139
}
106140

@@ -123,6 +157,17 @@ class AdminControllerSpec extends PlayPostgresSpec {
123157
email must be(emailValue)
124158
}
125159

160+
"return data from user logs table" in withApiClient { implicit client =>
161+
val user = createUser.futureValue
162+
val userLog = createUserLog(user.userId).futureValue
163+
164+
val response =
165+
client.getTableMetadata(userLogsSettings.tableName, List("message", "ASC"), List(0, 9), "{}").futureValue.head
166+
response("message") mustBe userLog.message
167+
response("user_id") mustBe user.userId
168+
response("id") mustBe userLog.userLogId
169+
}
170+
126171
"return data from uuid table" in withApiClient { client =>
127172
val name = "wiringbits"
128173
// val uuid_id = UUID.randomUUID().toString
@@ -610,7 +655,7 @@ class AdminControllerSpec extends PlayPostgresSpec {
610655
val password = "wiringbits"
611656
val request = AdminCreateTable.Request(Map("name" -> name, "email" -> email, "password" -> password))
612657
val response = client.createItem("users", request).futureValue
613-
response.noData must be(empty)
658+
response.id.nonEmpty mustBe true
614659
}
615660

616661
"create a new row for all tables" in withApiClient { client =>
@@ -620,10 +665,26 @@ class AdminControllerSpec extends PlayPostgresSpec {
620665
for (table <- tables) {
621666
val request = AdminCreateTable.Request(Map("name" -> name))
622667
val response = client.createItem(table.tableName, request).futureValue
623-
response.noData must be(empty)
668+
response.id.nonEmpty mustBe true
624669
}
625670
}
626671

672+
"return new user id" in withApiClient { implicit client =>
673+
val user = createUser.futureValue
674+
val response = client.getTableMetadata(usersSettings.tableName, List("name", "ASC"), List(0, 9), "{}").futureValue
675+
val userId = response.head("id")
676+
677+
userId mustBe user.userId
678+
}
679+
680+
"create a new user log" in withApiClient { implicit client =>
681+
val user = createUser.futureValue
682+
val request = AdminCreateTable.Request(Map("user_id" -> user.userId, "message" -> "Wiringbits"))
683+
684+
val response = client.createItem("user_logs", request).futureValue
685+
response.id.nonEmpty mustBe true
686+
}
687+
627688
"fail when a mandatory field is not sent" in withApiClient { client =>
628689
val name = "wiringbits"
629690
val request = AdminCreateTable.Request(Map("name" -> name))
@@ -647,7 +708,7 @@ class AdminControllerSpec extends PlayPostgresSpec {
647708
val table = serialOverflowSettings
648709
val request = AdminCreateTable.Request(Map("name" -> name))
649710
val ignore = client.createItem(table.tableName, request).futureValue
650-
ignore.noData must be(empty)
711+
ignore.id.nonEmpty mustBe true
651712
val request2 = AdminCreateTable.Request(Map("name" -> s"asdf"))
652713
val error = client.createItem(table.tableName, request2).expectError
653714
error must be(s"ERROR: integer out of range")
@@ -657,7 +718,7 @@ class AdminControllerSpec extends PlayPostgresSpec {
657718
val table = bigSerialOverflowSettings
658719
val request = AdminCreateTable.Request(Map("name" -> name))
659720
val ignore = client.createItem(table.tableName, request).futureValue
660-
ignore.noData must be(empty)
721+
ignore.id.nonEmpty mustBe true
661722
val request2 = AdminCreateTable.Request(Map("name" -> s"asdf"))
662723
val error = client.createItem(table.tableName, request2).expectError
663724
error must be(

spra-play-server/src/test/scala/net/wiringbits/spra/admin/QueryBuilderSpec.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@ class QueryBuilderSpec extends AnyWordSpec {
1414
|INSERT INTO users
1515
| (user_id, email, name)
1616
|VALUES (
17-
| ?, ?, ?
17+
| ?, ?::citext, ?::text
1818
|)
19+
|RETURNING user_id::TEXT
1920
|""".stripMargin
2021
val tableName = "users"
21-
val body = Map("email" -> "wiringbits@wiringbits.net", "name" -> "wiringbits")
22+
val body =
23+
Map(TableColumn("email", "citext") -> "wiringbits@wiringbits.net", TableColumn("name", "text") -> "wiringbits")
2224
val primaryKeyField = "user_id"
2325

2426
val response = QueryBuilder.create(tableName, body, primaryKeyField)
27+
println(response)
28+
println(expected)
2529
response must be(expected)
2630
}
2731

@@ -33,9 +37,10 @@ class QueryBuilderSpec extends AnyWordSpec {
3337
|VALUES (
3438
| ?
3539
|)
40+
|RETURNING user_id::TEXT
3641
|""".stripMargin
3742
val tableName = "users"
38-
val body = Map.empty[String, String]
43+
val body = Map.empty[TableColumn, String]
3944
val primaryKeyField = "user_id"
4045

4146
val response = QueryBuilder.create(tableName, body, primaryKeyField)

0 commit comments

Comments
 (0)