From d049867b828b42869e881e807c9fca7f8b79ece0 Mon Sep 17 00:00:00 2001 From: Antonio17 Date: Wed, 5 Jun 2024 20:40:19 -0700 Subject: [PATCH 01/13] Update React-Admin version to '4.14.4' --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 81e32aa..11ed192 100644 --- a/build.sbt +++ b/build.sbt @@ -32,7 +32,7 @@ val sttp = "3.5.0" val anorm = "2.7.0" val scalaTestPlusPlay = "6.0.0-M6" val scalaTestPlusMockito = "3.2.15.0" -val reactAdmin = "4.14.3" +val reactAdmin = "4.14.4" val consoleDisabledOptions = Seq("-Xfatal-warnings", "-Ywarn-unused", "-Ywarn-unused-import") From 112e1a5e873cf7d88e11b0fcc034cf20ada93b5e Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Wed, 5 Jun 2024 20:50:07 -0700 Subject: [PATCH 02/13] Added tables from DB --- .../src/main/resources/application.conf | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/spra-play-server/src/main/resources/application.conf b/spra-play-server/src/main/resources/application.conf index 1fe710f..01263cd 100644 --- a/spra-play-server/src/main/resources/application.conf +++ b/spra-play-server/src/main/resources/application.conf @@ -30,6 +30,63 @@ dataExplorer { } referenceDisplayField = "email" } + + userTokens { + tableName = "user_tokens" + primaryKeyField = "user_token_id" + nonEditableColumns = ["user_token_id", "user_id", "token", "token_type", "create_at" "expires_at"] + canBeDeleted = false + filterableColumns = ["token_type", "user_id"] + createFilter { + requiredColumns = ["token", "token_type", "user_id"] + } + referenceDisplayField = "email" + } + + employeeVacationDays { + tableName = "employee_vacation_days" + primaryKeyField = "employee_vacation_day_id" + nonEditableColumns = ["employee_vacation_day_id", "company_employee_id", "created_at", "updated_at"] + canBeDeleted = true + filterableColumns = ["company_employee_id", "date"] + createFilter { + requiredColumns = ["company_employee_id", "date", "comment"] + } + referenceDisplayField = "full_name" + } + + companyHolidays { + tableName = "company_holidays" + primaryKeyField = "company_holiday_id" + nonEditableColumns = ["company_holiday_id", "user_id", "create_at"] + canBeDeleted = true + filterableColumns = ["user_id", "date"] + createFilter { + requiredColumns = ["user_id", "date", "description"] + } + referenceDisplayField = "email" + } + + companyEmployees { + tableName = "company_employees" + primaryKeyField = "company_employee_id" + nonEditableColumns = ["company_employee_id", "created_at", "updated_at", "user_id"] + canBeDeleted = true + filterableColumns = ["user_id", "full_name", "vacations_per_yer"] + createFilter { + requiredColumns = ["user_id", "full_name", "vacations_per_year"] + } + referenceDisplayField = "email" + } + + companyNotificationsHistory { + tableName = "company_notifications_history" + primaryKeyField = "company_notification_history_id" + nonEditableColumns = ["company_notification_history_id", "user_id", "sent_at"] + canBeDeleted = false + filterableColumns = ["user_id"] + referenceDisplayField = "email" + } } } From f6e188c96b58e2ca7fec2a6f7b15e8f4dfbc39f3 Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Wed, 5 Jun 2024 21:01:48 -0700 Subject: [PATCH 03/13] Fixed the filter functionality when the parameter is a reference. Requires Reference Input instead of Reference Field. --- .../wiringbits/spra/ui/web/components/ListGuesser.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala index eacb477..5c68d68 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala @@ -49,7 +49,14 @@ object ListGuesser { case ColumnType.Image => Fragment() case ColumnType.Number => NumberInput(source = field.name) case ColumnType.Reference(reference, source) => - defaultField(reference, field.name)(TextField(source = source)) + ReferenceInput( + source = field.name, + reference = reference + )( + SelectInput( + optionText = props.response.referenceDisplayField.getOrElse(source), + ) + ) } } From abc506852bebab3863e2ccb5d6a2c883e8b07562 Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Wed, 5 Jun 2024 22:07:39 -0700 Subject: [PATCH 04/13] Fixed an Internal Server Error, Caused when attempting to filter by a column whose data type is not a string. --- .../spra/admin/repositories/daos/DatabaseTablesDAO.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index 2aed493..7837c8b 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -96,7 +96,7 @@ object DatabaseTablesDAO { if (filterValue.toIntOption.isDefined || filterValue.toDoubleOption.isDefined) s"$filterField = ?" else - s"$filterField LIKE ?" + s"CAST($filterField AS TEXT) LIKE ?" } } .mkString("WHERE ", " AND ", " ") From 70ee1806a0dd9b97ea2eaf1fe3a95c603513d11b Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Wed, 5 Jun 2024 22:08:34 -0700 Subject: [PATCH 05/13] Fixed : Failed prop type: Invalid prop `children` of type `array` supplied to `ReferenceFieldView`, expected a single ReactElement. --- .../net/wiringbits/spra/ui/web/components/ListGuesser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala index 5c68d68..335726b 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala @@ -20,7 +20,7 @@ object ListGuesser { val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props => val fields = ResponseGuesser.getTypesFromResponse(props.response) - def defaultField(reference: String, source: String)(children: ReactElement*): ReactElement = + def defaultField(reference: String, source: String)(children: ReactElement): ReactElement = ReferenceField(reference = reference, source = source)(children) val widgetFields: Seq[ReactElement] = fields.map { field => From 39daeb2407098c132d7564182b9cdbc7546d59d7 Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Wed, 5 Jun 2024 22:15:36 -0700 Subject: [PATCH 06/13] Format --- .../net/wiringbits/spra/ui/web/components/ListGuesser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala index 335726b..64fe18d 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/ListGuesser.scala @@ -54,7 +54,7 @@ object ListGuesser { reference = reference )( SelectInput( - optionText = props.response.referenceDisplayField.getOrElse(source), + optionText = props.response.referenceDisplayField.getOrElse(source) ) ) } From d874d437093dfa98ce2f94faa08ef241a422035c Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Wed, 5 Jun 2024 22:30:00 -0700 Subject: [PATCH 07/13] Revert "Added tables from DB" This reverts commit 112e1a5e873cf7d88e11b0fcc034cf20ada93b5e. --- .../src/main/resources/application.conf | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/spra-play-server/src/main/resources/application.conf b/spra-play-server/src/main/resources/application.conf index 01263cd..1fe710f 100644 --- a/spra-play-server/src/main/resources/application.conf +++ b/spra-play-server/src/main/resources/application.conf @@ -30,63 +30,6 @@ dataExplorer { } referenceDisplayField = "email" } - - userTokens { - tableName = "user_tokens" - primaryKeyField = "user_token_id" - nonEditableColumns = ["user_token_id", "user_id", "token", "token_type", "create_at" "expires_at"] - canBeDeleted = false - filterableColumns = ["token_type", "user_id"] - createFilter { - requiredColumns = ["token", "token_type", "user_id"] - } - referenceDisplayField = "email" - } - - employeeVacationDays { - tableName = "employee_vacation_days" - primaryKeyField = "employee_vacation_day_id" - nonEditableColumns = ["employee_vacation_day_id", "company_employee_id", "created_at", "updated_at"] - canBeDeleted = true - filterableColumns = ["company_employee_id", "date"] - createFilter { - requiredColumns = ["company_employee_id", "date", "comment"] - } - referenceDisplayField = "full_name" - } - - companyHolidays { - tableName = "company_holidays" - primaryKeyField = "company_holiday_id" - nonEditableColumns = ["company_holiday_id", "user_id", "create_at"] - canBeDeleted = true - filterableColumns = ["user_id", "date"] - createFilter { - requiredColumns = ["user_id", "date", "description"] - } - referenceDisplayField = "email" - } - - companyEmployees { - tableName = "company_employees" - primaryKeyField = "company_employee_id" - nonEditableColumns = ["company_employee_id", "created_at", "updated_at", "user_id"] - canBeDeleted = true - filterableColumns = ["user_id", "full_name", "vacations_per_yer"] - createFilter { - requiredColumns = ["user_id", "full_name", "vacations_per_year"] - } - referenceDisplayField = "email" - } - - companyNotificationsHistory { - tableName = "company_notifications_history" - primaryKeyField = "company_notification_history_id" - nonEditableColumns = ["company_notification_history_id", "user_id", "sent_at"] - canBeDeleted = false - filterableColumns = ["user_id"] - referenceDisplayField = "email" - } } } From d401e5f39dc8e61da90d6336da1fc76416f6e9d5 Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Thu, 6 Jun 2024 13:13:12 -0700 Subject: [PATCH 08/13] Fixed Internal server error --- .../admin/repositories/daos/DatabaseTablesDAO.scala | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index 7837c8b..6896796 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -93,10 +93,7 @@ object DatabaseTablesDAO { s"DATE($filterField) = ?" case _ => - if (filterValue.toIntOption.isDefined || filterValue.toDoubleOption.isDefined) - s"$filterField = ?" - else - s"CAST($filterField AS TEXT) LIKE ?" + s"CAST($filterField AS TEXT) LIKE ?" } } .mkString("WHERE ", " AND ", " ") @@ -121,12 +118,7 @@ object DatabaseTablesDAO { preparedStatement.setDate(sqlIndex, Date.valueOf(parsedDate)) case _ => - if (filterValue.toIntOption.isDefined) - preparedStatement.setInt(sqlIndex, filterValue.toInt) - else if (filterValue.toDoubleOption.isDefined) - preparedStatement.setDouble(sqlIndex, filterValue.toDouble) - else - preparedStatement.setString(sqlIndex, s"%$filterValue%") + preparedStatement.setString(sqlIndex, s"%$filterValue%") } } From 5cbf089f434a12720644037d18d2e1c31306722a Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Thu, 6 Jun 2024 20:26:32 -0700 Subject: [PATCH 09/13] Corrections to the filter --- .../repositories/daos/DatabaseTablesDAO.scala | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index 6896796..c70526c 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -73,6 +73,23 @@ object DatabaseTablesDAO { """.as(foreignKeyParser.*) } + private def isDouble(columnType: String): Boolean = { + columnType.contains("int") || columnType.contains("float") || columnType.contains("decimal") + } + private def isInt(columnType: String): Boolean = { + columnType.contains("int") || columnType == "serial" + } + private def isUUID(value: String): Boolean = { + try { + UUID.fromString(value) + true + } catch { + case _: IllegalArgumentException => false + } + } + private def isText(columnType: String): Boolean = { + columnType == "text" || columnType == "citext" || columnType == "varchar" || columnType == "char" + } def getTableData( settings: TableSettings, columns: List[TableColumn], @@ -88,12 +105,21 @@ object DatabaseTablesDAO { val conditionsSql = queryParameters.filters .map { case FilterParameter(filterField, filterValue) => + val columnType = columns.find(_.name.equals(filterField)).getOrElse(TableColumn("", "text")).`type` filterValue match { - case dateRegex(_, _, _) => + case dateRegex(_, _, _) if columnType == "date" => s"DATE($filterField) = ?" - case _ => - s"CAST($filterField AS TEXT) LIKE ?" + if ( + (filterValue.toIntOption.isDefined && isInt(columnType)) || + (filterValue.toDoubleOption.isDefined && isDouble(columnType)) || + (isUUID(filterValue) && columnType == "uuid") + ) + s"$filterField = ?" + else if (isText(columnType)) + s"$filterField LIKE ?" + else + s"CAST($filterField AS TEXT) LIKE ?" } } .mkString("WHERE ", " AND ", " ") @@ -108,17 +134,24 @@ object DatabaseTablesDAO { val preparedStatement = conn.prepareStatement(sql) queryParameters.filters.zipWithIndex - .foreach { case (FilterParameter(_, filterValue), index) => + .foreach { case (FilterParameter(filterField, filterValue), index) => // We have to increment index by 1 because SQL parameterIndex starts in 1 val sqlIndex = index + 1 - + val columnType = columns.find(_.name.equals(filterField)).getOrElse(TableColumn(filterField, "text")).`type` filterValue match { - case dateRegex(year, month, day) => + case dateRegex(year, month, day) if columnType == "date" => val parsedDate = LocalDate.of(year.toInt, month.toInt, day.toInt) preparedStatement.setDate(sqlIndex, Date.valueOf(parsedDate)) case _ => - preparedStatement.setString(sqlIndex, s"%$filterValue%") + if (filterValue.toIntOption.isDefined && isInt(columnType)) + preparedStatement.setInt(sqlIndex, filterValue.toInt) + else if (filterValue.toDoubleOption.isDefined && isDouble(columnType)) + preparedStatement.setDouble(sqlIndex, filterValue.toDouble) + else if (isUUID(filterValue) && columnType == "uuid") + preparedStatement.setObject(1, UUID.fromString(filterValue)) + else + preparedStatement.setString(sqlIndex, s"%$filterValue%") } } From 16ba68d1e9b87336d0194369ed4fb31667379471 Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Thu, 6 Jun 2024 20:54:55 -0700 Subject: [PATCH 10/13] Fixed Internal server error --- .../spra/admin/repositories/daos/DatabaseTablesDAO.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index c70526c..307ed87 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -149,7 +149,7 @@ object DatabaseTablesDAO { else if (filterValue.toDoubleOption.isDefined && isDouble(columnType)) preparedStatement.setDouble(sqlIndex, filterValue.toDouble) else if (isUUID(filterValue) && columnType == "uuid") - preparedStatement.setObject(1, UUID.fromString(filterValue)) + preparedStatement.setObject(sqlIndex, UUID.fromString(filterValue)) else preparedStatement.setString(sqlIndex, s"%$filterValue%") } From 58067018c466aa7424b9e0611fb4c86d8932ec69 Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Mon, 26 Aug 2024 18:38:04 -0700 Subject: [PATCH 11/13] Fixed bugs related to image handling (saving and reading). --- .../src/main/resources/application.conf | 12 ++- .../DatabaseTablesRepository.scala | 11 ++- .../repositories/daos/DatabaseTablesDAO.scala | 4 +- .../spra/admin/utils/QueryBuilder.scala | 4 +- .../spra/admin/utils/StringParse.scala | 13 ++++ .../wiringbits/spra/ui/web/AdminView.scala | 4 +- .../ui/web/components/CreateGuesser.scala | 2 +- .../spra/ui/web/components/EditGuesser.scala | 2 +- .../spra/ui/web/facades/DataProvider.scala | 75 +++++++++++++++++++ .../spra/ui/web/facades/package.scala | 26 ++++++- .../web/facades/reactadmin/ImageField.scala | 14 ++-- .../web/facades/reactadmin/ImageInput.scala | 29 +++++++ .../ui/web/facades/reactadmin/package.scala | 8 +- 13 files changed, 178 insertions(+), 26 deletions(-) create mode 100644 spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala create mode 100644 spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala diff --git a/spra-play-server/src/main/resources/application.conf b/spra-play-server/src/main/resources/application.conf index 1fe710f..65db230 100644 --- a/spra-play-server/src/main/resources/application.conf +++ b/spra-play-server/src/main/resources/application.conf @@ -30,6 +30,16 @@ dataExplorer { } referenceDisplayField = "email" } + + images { + tableName = "images" + primaryKeyField = "image_id" + nonEditableColumns = ["image_id", "created_at"] + canBeDeleted = false + createFilter { + requiredColumns = ["name", "data"] + } + } } } @@ -56,7 +66,7 @@ play.filters.enabled += "play.filters.cors.CORSFilter" db.default { driver = "org.postgresql.Driver" host = "localhost:5432" - database = "vacation_tracker_db" + database = "agriculture_global_db" username = "postgres" password = "postgres" diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala index 4319c31..bd7d1f7 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala @@ -4,6 +4,7 @@ import net.wiringbits.spra.admin.config.{DataExplorerConfig, TableSettings} import net.wiringbits.spra.admin.executors.DatabaseExecutionContext 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.StringParse import net.wiringbits.spra.admin.utils.models.QueryParameters import play.api.db.Database @@ -75,7 +76,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`.equals("bytea")) + (field, StringParse.stringToByteArray(value)) + else + (field, value) } DatabaseTablesDAO.create( tableName = tableName, @@ -100,7 +104,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`.equals("bytea")) + (field, StringParse.stringToByteArray(value)) + else + (field, value) } val primaryKeyType = settings.primaryKeyDataType DatabaseTablesDAO.update( diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index 307ed87..c3383b3 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -255,7 +255,7 @@ object DatabaseTablesDAO { } def create( tableName: String, - fieldsAndValues: Map[TableColumn, String], + fieldsAndValues: Map[TableColumn, Serializable], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID )(implicit @@ -285,7 +285,7 @@ object DatabaseTablesDAO { def update( tableName: String, - fieldsAndValues: Map[TableColumn, String], + fieldsAndValues: Map[TableColumn, Serializable], primaryKeyField: String, primaryKeyValue: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala index 8193eba..9372e3a 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala @@ -8,7 +8,7 @@ import scala.collection.mutable object QueryBuilder { def create( tableName: String, - fieldsAndValues: Map[TableColumn, String], + fieldsAndValues: Map[TableColumn, Serializable], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID ): String = { @@ -33,7 +33,7 @@ object QueryBuilder { |""".stripMargin } - def update(tableName: String, body: Map[TableColumn, String], primaryKeyField: String): String = { + def update(tableName: String, body: Map[TableColumn, Serializable], primaryKeyField: String): String = { val updateStatement = new mutable.StringBuilder("SET") for ((tableField, value) <- body) { val resultStatement = if (value == "null") "NULL" else s"?::${tableField.`type`}" diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala new file mode 100644 index 0000000..4918abe --- /dev/null +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala @@ -0,0 +1,13 @@ +package net.wiringbits.spra.admin.utils + +object StringParse { + + def stringToByteArray(value: String): Array[Byte] = { + try { + value.replaceAll("[\\[\\]\\s]", "").split(",").map(_.toByte) + } catch { + case _: NumberFormatException => Array.emptyByteArray + } + } + +} diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala index d80196b..4cfe8cb 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/AdminView.scala @@ -3,7 +3,7 @@ package net.wiringbits.spra.ui.web import net.wiringbits.spra.api.models.AdminGetTables import net.wiringbits.spra.ui.web.components.{CreateGuesser, EditGuesser, ListGuesser} import net.wiringbits.spra.ui.web.facades.reactadmin.{Admin, Resource} -import net.wiringbits.spra.ui.web.facades.simpleRestProvider +import net.wiringbits.spra.ui.web.facades.createDataProvider import net.wiringbits.spra.ui.web.models.DataExplorerSettings import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global import slinky.core.facade.{Hooks, ReactElement} @@ -52,7 +52,7 @@ object AdminView { } div()( - Admin(simpleRestProvider(tablesUrl))(buildResources), + Admin(createDataProvider(tablesUrl))(buildResources), error.map(h1(_)) ) } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala index d348793..4aa6712 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/CreateGuesser.scala @@ -29,7 +29,7 @@ object CreateGuesser { case ColumnType.Email => TextInput(source = field.name, isRequired = isRequired, validate = required) case ColumnType.Image => - ImageField(source = field.name, isRequired = isRequired, validate = required) + ImageInput(source = field.name, isRequired = isRequired, validate = required)(ImageField(source = "src")) case ColumnType.Number => NumberInput(source = field.name, isRequired = isRequired, validate = required) case ColumnType.Reference(reference, source) => diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala index 5b468f9..8e0a161 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/components/EditGuesser.scala @@ -33,7 +33,7 @@ object EditGuesser { case ColumnType.Date => DateTimeInput(source = field.name, disabled = field.disabled) case ColumnType.Text => TextInput(source = field.name, disabled = field.disabled) case ColumnType.Email => TextInput(source = field.name, disabled = field.disabled) - case ColumnType.Image => ImageField(source = field.name) + case ColumnType.Image => ImageInput(source = field.name)(ImageField(source = "src")) case ColumnType.Number => NumberInput(source = field.name, disabled = field.disabled) case ColumnType.Reference(reference, source) => ReferenceInput( diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala index 020648c..df60c1c 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala @@ -1,6 +1,81 @@ package net.wiringbits.spra.ui.web.facades +import org.scalajs.dom +import org.scalajs.dom.{Blob, File} + +import scala.concurrent.Future import scala.scalajs.js +import scala.scalajs.js.{JSON, Promise} +import scala.scalajs.js.annotation.{JSExportTopLevel, JSImport} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.scalajs.js.typedarray.{ArrayBuffer, Int8Array, Uint8Array} @js.native trait DataProvider extends js.Object + +@js.native +@JSImport("ra-data-simple-rest", JSImport.Default) +def simpleRestProvider(url: String): DataProvider = js.native +@js.native +@JSImport("react-admin", "withLifecycleCallbacks") +object WithLifecycleCallbacks extends js.Object { + def apply(dataProvider: DataProvider, callbacks: js.Array[js.Object]): DataProvider = js.native +} + +def prepareRequest(params: js.Dynamic) = { + val rawFile = params.data.rawFile.asInstanceOf[File] + + val imageFuture = convertImageToByteArray(rawFile) + + imageFuture.`then` { value => + val newParams = params + params.updateDynamic("data")(value.asInstanceOf[js.Any]) + params + } +} + +def processResponse(record: js.Dynamic) = { + val hexImage = record.data.asInstanceOf[String] + val urlImage = convertHexToImage(hexImage) + record.updateDynamic("data")(urlImage) + record +} + +def convertImageToByteArray(file: dom.File): js.Promise[String] = { + val promise = new js.Promise[String]((resolve, reject) => { + val reader = new dom.FileReader() + reader.onload = { (e: dom.Event) => + val arrayBuffer = reader.result.asInstanceOf[ArrayBuffer] + val byteArray = new Int8Array(arrayBuffer).toArray + resolve(byteArray.mkString("[", ", ", "]")) + } + reader.onerror = { (e: dom.Event) => + reject(new js.Error("Failed to read file")) + } + reader.readAsArrayBuffer(file) + }) + + promise +} +def convertHexToImage(imageHex: String): String = { + + val hex = imageHex.tail.tail + + val imageBinary: Array[Byte] = + if ((hex.length % 2) == 1) + Array.empty + else + try { + val binary = hex + .grouped(2) + .map { hex => + Integer.parseInt(hex, 16).toByte + } + .toArray + binary + } catch case _ => Array.empty + + val byteArray = Uint8Array(js.Array(imageBinary.map(_.toShort): _*)) + + dom.URL.createObjectURL(dom.Blob(js.Array(byteArray.buffer))) +} diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala index f6c484e..db2d980 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala @@ -1,10 +1,28 @@ package net.wiringbits.spra.ui.web import scala.scalajs.js -import scala.scalajs.js.annotation.JSImport package object facades { - @js.native - @JSImport("ra-data-simple-rest", JSImport.Default) - def simpleRestProvider(url: String): DataProvider = js.native + + def createDataProvider(url: String): DataProvider = { + + val baseDataProvider = simpleRestProvider(url) + + val dataProvider = WithLifecycleCallbacks( + baseDataProvider, + js.Array( + js.Dynamic.literal( + resource = "images", + afterRead = (record: js.Dynamic, dataProvider: js.Any) => { + processResponse(record) + }, + beforeSave = (data: js.Dynamic, dataProvider: js.Any) => { + prepareRequest(data) + } + ) + ) + ) + + dataProvider + } } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala index 0330951..fa440e4 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageField.scala @@ -8,20 +8,20 @@ import scala.scalajs.js.| object ImageField extends ExternalComponent { case class Props( source: String, + title: String, + sortable: Boolean = false, disabled: Boolean = false, - sx: js.Dynamic = js.Dynamic.literal(), - isRequired: Boolean = false, - validate: js.UndefOr[js.Any] = js.undefined + sx: js.Dynamic = js.Dynamic.literal() ) def apply( source: String, + title: String = "title", + sortable: Boolean = false, disabled: Boolean = false, - sx: js.Dynamic = js.Dynamic.literal(), - isRequired: Boolean = false, - validate: js.UndefOr[js.Any] = js.undefined + sx: js.Dynamic = js.Dynamic.literal() ): BuildingComponent[_, _] = { - super.apply(Props(source, disabled, sx, isRequired, validate)) + super.apply(Props(source, title, sortable, disabled, sx)) } override val component: String | js.Object = ReactAdmin.ImageField diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala new file mode 100644 index 0000000..10e9f8c --- /dev/null +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala @@ -0,0 +1,29 @@ +package net.wiringbits.spra.ui.web.facades.reactadmin + +import slinky.core.{BuildingComponent, ExternalComponent} + +import scala.scalajs.js +import scala.scalajs.js.| + +object ImageInput extends ExternalComponent { + case class Props( + source: String, + disabled: Boolean = false, + sx: js.Dynamic = js.Dynamic.literal(), + isRequired: Boolean = false, + validate: js.UndefOr[js.Any] = js.undefined + ) + + def apply( + source: String, + disabled: Boolean = false, + sx: js.Dynamic = js.Dynamic.literal(), + isRequired: Boolean = false, + validate: js.UndefOr[js.Any] = js.undefined, + onDrop: js.UndefOr[js.Function2[js.Array[js.Any], js.Function1[js.Array[js.Object], Unit], Unit]] = js.undefined + ): BuildingComponent[_, _] = { + super.apply(Props(source, disabled, sx, isRequired, validate)) + } + + override val component: String | js.Object = ReactAdmin.ImageInput +} diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala index 6d34e3f..ead6b49 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/package.scala @@ -11,10 +11,10 @@ package object reactadmin { def required(): js.Any = js.native - val Admin, Resource, EditGuesser, ListGuesser, TextInput, ImageField, NumberInput, DateTimeInput, ReferenceInput, - SelectInput, Button, DeleteButton, SaveButton, TopToolbar, Toolbar, Edit, SimpleForm, DateField, TextField, - EmailField, NumberField, ReferenceField, DateInput, FilterButton, ExportButton, List, Datagrid, Create, - CreateButton: js.Object = + val Admin, Resource, EditGuesser, ListGuesser, TextInput, ImageField, ImageInput, NumberInput, DateTimeInput, + ReferenceInput, SelectInput, Button, DeleteButton, SaveButton, TopToolbar, Toolbar, Edit, SimpleForm, DateField, + TextField, EmailField, NumberField, ReferenceField, DateInput, FilterButton, ExportButton, List, Datagrid, + Create, CreateButton: js.Object = js.native } } From ef10525e0a6cc341760701dd9c6177b72f5045ee Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Mon, 2 Sep 2024 20:45:42 -0700 Subject: [PATCH 12/13] Changes required for the PR --- .../src/main/resources/application.conf | 2 +- .../spra/admin/models/FieldValue.scala | 6 ++ .../DatabaseTablesRepository.scala | 17 ++-- .../repositories/daos/DatabaseTablesDAO.scala | 78 ++++++++++++------- .../spra/admin/utils/QueryBuilder.scala | 7 +- .../spra/admin/utils/StringParse.scala | 11 +-- .../spra/ui/web/facades/DataProvider.scala | 70 +---------------- .../spra/ui/web/facades/package.scala | 19 +++-- .../web/facades/reactadmin/ImageInput.scala | 1 + .../wiringbits/spra/ui/web/utils/Images.scala | 42 ++++++++++ 10 files changed, 133 insertions(+), 120 deletions(-) create mode 100644 spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala create mode 100644 spra-web/src/main/scala/net/wiringbits/spra/ui/web/utils/Images.scala diff --git a/spra-play-server/src/main/resources/application.conf b/spra-play-server/src/main/resources/application.conf index 65db230..1a7b3da 100644 --- a/spra-play-server/src/main/resources/application.conf +++ b/spra-play-server/src/main/resources/application.conf @@ -66,7 +66,7 @@ play.filters.enabled += "play.filters.cors.CORSFilter" db.default { driver = "org.postgresql.Driver" host = "localhost:5432" - database = "agriculture_global_db" + database = "vacation_tracker_db" username = "postgres" password = "postgres" diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala new file mode 100644 index 0000000..0fbcbe8 --- /dev/null +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala @@ -0,0 +1,6 @@ +package net.wiringbits.spra.admin.models + +trait FieldValue extends Serializable + +case class StringValue(value: String) extends FieldValue +case class ByteArrayValue(value: Array[Byte]) extends FieldValue diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala index bd7d1f7..aba603c 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/DatabaseTablesRepository.scala @@ -2,6 +2,7 @@ package net.wiringbits.spra.admin.repositories import net.wiringbits.spra.admin.config.{DataExplorerConfig, TableSettings} import net.wiringbits.spra.admin.executors.DatabaseExecutionContext +import net.wiringbits.spra.admin.models.{ByteArrayValue, StringValue} 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.StringParse @@ -76,10 +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")) - if (field.`type`.equals("bytea")) - (field, StringParse.stringToByteArray(value)) - else - (field, value) + if (field.`type` == "bytea") + val byteaValue = StringParse.stringToByteArray(value) + (field, ByteArrayValue(byteaValue)) + else (field, StringValue(value)) } DatabaseTablesDAO.create( tableName = tableName, @@ -104,10 +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")) - if (field.`type`.equals("bytea")) - (field, StringParse.stringToByteArray(value)) - else - (field, value) + if (field.`type` == "bytea") + val byteaValue = StringParse.stringToByteArray(value) + (field, ByteArrayValue(byteaValue)) + else (field, StringValue(value)) } val primaryKeyType = settings.primaryKeyDataType DatabaseTablesDAO.update( diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index c3383b3..2d01fc5 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -2,6 +2,7 @@ package net.wiringbits.spra.admin.repositories.daos import anorm.{SqlParser, SqlStringInterpolation} import net.wiringbits.spra.admin.config.{CustomDataType, PrimaryKeyDataType, TableSettings} +import net.wiringbits.spra.admin.models.{ByteArrayValue, FieldValue, StringValue} import net.wiringbits.spra.admin.repositories.models.* import net.wiringbits.spra.admin.utils.models.{FilterParameter, QueryParameters} import net.wiringbits.spra.admin.utils.{QueryBuilder, StringRegex} @@ -10,7 +11,7 @@ import java.sql.{Connection, Date, PreparedStatement, ResultSet} import java.time.LocalDate import java.util.UUID import scala.collection.mutable.ListBuffer -import scala.util.Try +import scala.util.{Failure, Success, Try} object DatabaseTablesDAO { @@ -73,23 +74,38 @@ object DatabaseTablesDAO { """.as(foreignKeyParser.*) } - private def isDouble(columnType: String): Boolean = { - columnType.contains("int") || columnType.contains("float") || columnType.contains("decimal") + private def columnTypeIsDouble(columnType: String): Boolean = { + // 'contains' is used because PostgreSQL types may include additional details like precision or scale + // https://www.postgresql.org/docs/8.1/datatype.html + List("float", "decimal").exists(columnType.contains) } - private def isInt(columnType: String): Boolean = { - columnType.contains("int") || columnType == "serial" + + private def columnTypeIsInt(columnType: String): Boolean = { + List("int", "serial").exists(columnType.contains) } - private def isUUID(value: String): Boolean = { - try { - UUID.fromString(value) - true - } catch { - case _: IllegalArgumentException => false + + private def isUUID(value: String, columnType: String): Boolean = { + val isUUID = Try(UUID.fromString(value)) match { + case Success(_) => true + case Failure(_) => false } + isUUID && columnType == "uuid" + } + + private def isInt(value: String, columnType: String): Boolean = { + value.toIntOption.isDefined && columnTypeIsInt(columnType) + } + + private def isDecimal(value: String, columnType: String): Boolean = { + value.toDoubleOption.isDefined && columnTypeIsDouble(columnType) } - private def isText(columnType: String): Boolean = { - columnType == "text" || columnType == "citext" || columnType == "varchar" || columnType == "char" + + private def isANumberOrUUID(value: String, columnType: String): Boolean = { + isInt(value, columnType) || + isDecimal(value, columnType) || + isUUID(value, columnType) } + def getTableData( settings: TableSettings, columns: List[TableColumn], @@ -105,21 +121,18 @@ object DatabaseTablesDAO { val conditionsSql = queryParameters.filters .map { case FilterParameter(filterField, filterValue) => - val columnType = columns.find(_.name.equals(filterField)).getOrElse(TableColumn("", "text")).`type` + val columnType = columns.find(_.name == filterField) match { + case Some(column) => column.`type` + case None => throw Exception(s"Column with name '$filterField' not found.") + } filterValue match { case dateRegex(_, _, _) if columnType == "date" => s"DATE($filterField) = ?" case _ => - if ( - (filterValue.toIntOption.isDefined && isInt(columnType)) || - (filterValue.toDoubleOption.isDefined && isDouble(columnType)) || - (isUUID(filterValue) && columnType == "uuid") - ) + if (isANumberOrUUID(filterValue, columnType)) s"$filterField = ?" - else if (isText(columnType)) - s"$filterField LIKE ?" else - s"CAST($filterField AS TEXT) LIKE ?" + s"$filterField LIKE ?" } } .mkString("WHERE ", " AND ", " ") @@ -144,11 +157,11 @@ object DatabaseTablesDAO { preparedStatement.setDate(sqlIndex, Date.valueOf(parsedDate)) case _ => - if (filterValue.toIntOption.isDefined && isInt(columnType)) + if (isInt(filterValue, columnType)) preparedStatement.setInt(sqlIndex, filterValue.toInt) - else if (filterValue.toDoubleOption.isDefined && isDouble(columnType)) + else if (isDecimal(filterValue, columnType)) preparedStatement.setDouble(sqlIndex, filterValue.toDouble) - else if (isUUID(filterValue) && columnType == "uuid") + else if (isUUID(filterValue, columnType)) preparedStatement.setObject(sqlIndex, UUID.fromString(filterValue)) else preparedStatement.setString(sqlIndex, s"%$filterValue%") @@ -255,7 +268,7 @@ object DatabaseTablesDAO { } def create( tableName: String, - fieldsAndValues: Map[TableColumn, Serializable], + fieldsAndValues: Map[TableColumn, FieldValue], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID )(implicit @@ -275,7 +288,9 @@ 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)) match + case value: StringValue => value.value + case value: ByteArrayValue => value.value preparedStatement.setObject(j, value) } val result = preparedStatement.executeQuery() @@ -285,7 +300,7 @@ object DatabaseTablesDAO { def update( tableName: String, - fieldsAndValues: Map[TableColumn, Serializable], + fieldsAndValues: Map[TableColumn, FieldValue], primaryKeyField: String, primaryKeyValue: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID @@ -293,8 +308,11 @@ object DatabaseTablesDAO { 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) => + val notNullData = fieldsAndValues.filterNot { case (_, value) => value.equals("null") } + notNullData.zipWithIndex.foreach { case ((_, fieldValue), i) => + val value = fieldValue match + case value: StringValue => value.value + case value: ByteArrayValue => value.value preparedStatement.setObject(i + 1, value) } // where ... = ? diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala index 9372e3a..8ba3742 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala @@ -1,6 +1,7 @@ package net.wiringbits.spra.admin.utils import net.wiringbits.spra.admin.config.PrimaryKeyDataType +import net.wiringbits.spra.admin.models.FieldValue import net.wiringbits.spra.admin.repositories.models.TableColumn import scala.collection.mutable @@ -8,7 +9,7 @@ import scala.collection.mutable object QueryBuilder { def create( tableName: String, - fieldsAndValues: Map[TableColumn, Serializable], + fieldsAndValues: Map[TableColumn, FieldValue], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID ): String = { @@ -33,10 +34,10 @@ object QueryBuilder { |""".stripMargin } - def update(tableName: String, body: Map[TableColumn, Serializable], 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`}" + val resultStatement = if (value.equals("null")) "NULL" else s"?::${tableField.`type`}" val statement = s" ${tableField.name} = $resultStatement," updateStatement.append(statement) } diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala index 4918abe..7d234d4 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/StringParse.scala @@ -1,13 +1,14 @@ package net.wiringbits.spra.admin.utils +import scala.util.{Failure, Success, Try} + object StringParse { def stringToByteArray(value: String): Array[Byte] = { - try { - value.replaceAll("[\\[\\]\\s]", "").split(",").map(_.toByte) - } catch { - case _: NumberFormatException => Array.emptyByteArray - } + // 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 } } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala index df60c1c..08a6ae6 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/DataProvider.scala @@ -1,81 +1,19 @@ package net.wiringbits.spra.ui.web.facades -import org.scalajs.dom -import org.scalajs.dom.{Blob, File} - -import scala.concurrent.Future import scala.scalajs.js -import scala.scalajs.js.{JSON, Promise} -import scala.scalajs.js.annotation.{JSExportTopLevel, JSImport} -import scala.concurrent.ExecutionContext.Implicits.global -import scala.scalajs.js.typedarray.{ArrayBuffer, Int8Array, Uint8Array} +import scala.scalajs.js.annotation.JSImport @js.native trait DataProvider extends js.Object @js.native @JSImport("ra-data-simple-rest", JSImport.Default) +// https://www.npmjs.com/package/ra-data-simple-rest def simpleRestProvider(url: String): DataProvider = js.native + @js.native @JSImport("react-admin", "withLifecycleCallbacks") +// https://marmelab.com/react-admin/withLifecycleCallbacks.html object WithLifecycleCallbacks extends js.Object { def apply(dataProvider: DataProvider, callbacks: js.Array[js.Object]): DataProvider = js.native } - -def prepareRequest(params: js.Dynamic) = { - val rawFile = params.data.rawFile.asInstanceOf[File] - - val imageFuture = convertImageToByteArray(rawFile) - - imageFuture.`then` { value => - val newParams = params - params.updateDynamic("data")(value.asInstanceOf[js.Any]) - params - } -} - -def processResponse(record: js.Dynamic) = { - val hexImage = record.data.asInstanceOf[String] - val urlImage = convertHexToImage(hexImage) - record.updateDynamic("data")(urlImage) - record -} - -def convertImageToByteArray(file: dom.File): js.Promise[String] = { - val promise = new js.Promise[String]((resolve, reject) => { - val reader = new dom.FileReader() - reader.onload = { (e: dom.Event) => - val arrayBuffer = reader.result.asInstanceOf[ArrayBuffer] - val byteArray = new Int8Array(arrayBuffer).toArray - resolve(byteArray.mkString("[", ", ", "]")) - } - reader.onerror = { (e: dom.Event) => - reject(new js.Error("Failed to read file")) - } - reader.readAsArrayBuffer(file) - }) - - promise -} -def convertHexToImage(imageHex: String): String = { - - val hex = imageHex.tail.tail - - val imageBinary: Array[Byte] = - if ((hex.length % 2) == 1) - Array.empty - else - try { - val binary = hex - .grouped(2) - .map { hex => - Integer.parseInt(hex, 16).toByte - } - .toArray - binary - } catch case _ => Array.empty - - val byteArray = Uint8Array(js.Array(imageBinary.map(_.toShort): _*)) - - dom.URL.createObjectURL(dom.Blob(js.Array(byteArray.buffer))) -} diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala index db2d980..46f61ff 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/package.scala @@ -1,28 +1,33 @@ package net.wiringbits.spra.ui.web import scala.scalajs.js +import net.wiringbits.spra.ui.web.utils.Images.* +import org.scalajs.dom.File package object facades { def createDataProvider(url: String): DataProvider = { - val baseDataProvider = simpleRestProvider(url) - - val dataProvider = WithLifecycleCallbacks( + WithLifecycleCallbacks( baseDataProvider, js.Array( js.Dynamic.literal( resource = "images", afterRead = (record: js.Dynamic, dataProvider: js.Any) => { - processResponse(record) + val hexImage = record.data.asInstanceOf[String] + val urlImage = convertHexToImage(hexImage) + record.updateDynamic("data")(urlImage) + record }, beforeSave = (data: js.Dynamic, dataProvider: js.Any) => { - prepareRequest(data) + val rawFile = data.data.rawFile.asInstanceOf[File] + convertImageToByteArray(rawFile).`then` { value => + data.updateDynamic("data")(value.asInstanceOf[js.Any]) + data + } } ) ) ) - - dataProvider } } diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala index 10e9f8c..5146380 100644 --- a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/facades/reactadmin/ImageInput.scala @@ -5,6 +5,7 @@ import slinky.core.{BuildingComponent, ExternalComponent} import scala.scalajs.js import scala.scalajs.js.| +// https://marmelab.com/react-admin/ImageInput.html object ImageInput extends ExternalComponent { case class Props( source: String, diff --git a/spra-web/src/main/scala/net/wiringbits/spra/ui/web/utils/Images.scala b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/utils/Images.scala new file mode 100644 index 0000000..87efd81 --- /dev/null +++ b/spra-web/src/main/scala/net/wiringbits/spra/ui/web/utils/Images.scala @@ -0,0 +1,42 @@ +package net.wiringbits.spra.ui.web.utils + +import org.scalajs.dom +import org.scalajs.dom.{Blob, File} +import scala.util.{Failure, Success, Try} +import scala.scalajs.js.Promise +import scala.scalajs.js.typedarray.{ArrayBuffer, Int8Array, Uint8Array} +import scala.scalajs.js + +object Images { + + def convertImageToByteArray(image: dom.File): js.Promise[String] = { + new js.Promise[String]((resolve, reject) => { + val reader = new dom.FileReader() + reader.onload = { (e: dom.Event) => + val arrayBuffer = reader.result.asInstanceOf[ArrayBuffer] + val byteArray = new Int8Array(arrayBuffer).toArray + resolve(byteArray.mkString("[", ", ", "]")) + } + reader.onerror = { (e: dom.Event) => + reject(new js.Error("Failed to read file")) + } + reader.readAsArrayBuffer(image) + }) + } + + def convertHexToImage(imageHex: String): String = { + // Remove the "0x" prefix from the hex string, as it's not part of the actual image data + val hex = imageHex.tail.tail + val imageBinary: Array[Byte] = + if ((hex.length % 2) == 1) + Array.empty + else + Try(hex.grouped(2).map { hex => Integer.parseInt(hex, 16).toByte }.toArray) match { + case Success(value) => value + case Failure(_) => Array.empty + } + val byteArray = Uint8Array(js.Array(imageBinary.map(_.toShort): _*)) + dom.URL.createObjectURL(dom.Blob(js.Array(byteArray.buffer))) + } + +} From 9aa43c46b93c28bb198ca7ad3a64c3b72139016a Mon Sep 17 00:00:00 2001 From: Antonio171003 Date: Mon, 2 Sep 2024 22:06:19 -0700 Subject: [PATCH 13/13] Changes required for the PR --- .../spra/admin/models/FieldValue.scala | 8 +++-- .../repositories/daos/DatabaseTablesDAO.scala | 31 +++++++++---------- .../spra/admin/utils/QueryBuilder.scala | 6 ++-- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala index 0fbcbe8..5781e02 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/models/FieldValue.scala @@ -1,6 +1,8 @@ package net.wiringbits.spra.admin.models -trait FieldValue extends Serializable +trait FieldValue[T] extends Serializable { + val value: T +} -case class StringValue(value: String) extends FieldValue -case class ByteArrayValue(value: Array[Byte]) extends FieldValue +case class StringValue(value: String) extends FieldValue[String] +case class ByteArrayValue(value: Array[Byte]) extends FieldValue[Array[Byte]] diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala index 2d01fc5..ab0e804 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/repositories/daos/DatabaseTablesDAO.scala @@ -85,11 +85,10 @@ object DatabaseTablesDAO { } private def isUUID(value: String, columnType: String): Boolean = { - val isUUID = Try(UUID.fromString(value)) match { - case Success(_) => true + Try(UUID.fromString(value)) match { + case Success(_) => columnType == "uuid" case Failure(_) => false } - isUUID && columnType == "uuid" } private def isInt(value: String, columnType: String): Boolean = { @@ -100,7 +99,7 @@ object DatabaseTablesDAO { value.toDoubleOption.isDefined && columnTypeIsDouble(columnType) } - private def isANumberOrUUID(value: String, columnType: String): Boolean = { + private def isNumberOrUUID(value: String, columnType: String): Boolean = { isInt(value, columnType) || isDecimal(value, columnType) || isUUID(value, columnType) @@ -129,7 +128,7 @@ object DatabaseTablesDAO { case dateRegex(_, _, _) if columnType == "date" => s"DATE($filterField) = ?" case _ => - if (isANumberOrUUID(filterValue, columnType)) + if (isNumberOrUUID(filterValue, columnType)) s"$filterField = ?" else s"$filterField LIKE ?" @@ -150,7 +149,10 @@ object DatabaseTablesDAO { .foreach { case (FilterParameter(filterField, filterValue), index) => // We have to increment index by 1 because SQL parameterIndex starts in 1 val sqlIndex = index + 1 - val columnType = columns.find(_.name.equals(filterField)).getOrElse(TableColumn(filterField, "text")).`type` + val columnType = columns.find(_.name == filterField) match { + case Some(column) => column.`type` + case None => throw Exception(s"Column with name '$filterField' not found.") + } filterValue match { case dateRegex(year, month, day) if columnType == "date" => val parsedDate = LocalDate.of(year.toInt, month.toInt, day.toInt) @@ -268,7 +270,7 @@ object DatabaseTablesDAO { } def create( tableName: String, - fieldsAndValues: Map[TableColumn, FieldValue], + fieldsAndValues: Map[TableColumn, FieldValue[_]], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID )(implicit @@ -288,9 +290,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)) match - case value: StringValue => value.value - case value: ByteArrayValue => value.value + val value = fieldsAndValues(fieldsAndValues.keys.toList(j - i - 1)).value preparedStatement.setObject(j, value) } val result = preparedStatement.executeQuery() @@ -300,7 +300,7 @@ object DatabaseTablesDAO { def update( tableName: String, - fieldsAndValues: Map[TableColumn, FieldValue], + fieldsAndValues: Map[TableColumn, FieldValue[_]], primaryKeyField: String, primaryKeyValue: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID @@ -308,12 +308,9 @@ object DatabaseTablesDAO { val sql = QueryBuilder.update(tableName, fieldsAndValues, primaryKeyField) val preparedStatement = conn.prepareStatement(sql) - val notNullData = fieldsAndValues.filterNot { case (_, value) => value.equals("null") } - notNullData.zipWithIndex.foreach { case ((_, fieldValue), i) => - val value = fieldValue match - case value: StringValue => value.value - case value: ByteArrayValue => value.value - preparedStatement.setObject(i + 1, value) + val notNullData = fieldsAndValues.filterNot { case (_, value) => value.value == "null" } + notNullData.zipWithIndex.foreach { case ((_, value), i) => + preparedStatement.setObject(i + 1, value.value) } // where ... = ? setPreparedStatementKey(preparedStatement, primaryKeyValue, primaryKeyType, notNullData.size + 1) diff --git a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala index 8ba3742..e7e3e40 100644 --- a/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala +++ b/spra-play-server/src/main/scala/net/wiringbits/spra/admin/utils/QueryBuilder.scala @@ -9,7 +9,7 @@ import scala.collection.mutable object QueryBuilder { def create( tableName: String, - fieldsAndValues: Map[TableColumn, FieldValue], + fieldsAndValues: Map[TableColumn, FieldValue[_]], primaryKeyField: String, primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID ): String = { @@ -34,10 +34,10 @@ object QueryBuilder { |""".stripMargin } - def update(tableName: String, body: Map[TableColumn, FieldValue], 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.equals("null")) "NULL" else s"?::${tableField.`type`}" + val resultStatement = if (value.value == "null") "NULL" else s"?::${tableField.`type`}" val statement = s" ${tableField.name} = $resultStatement," updateStatement.append(statement) }