Skip to content

Commit

Permalink
Implement create (#17)
Browse files Browse the repository at this point in the history
* Implement create

* Minor changes
  • Loading branch information
KapStorm authored Sep 25, 2023
1 parent 4b6be66 commit f3c951f
Show file tree
Hide file tree
Showing 25 changed files with 234 additions and 86 deletions.
33 changes: 11 additions & 22 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +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 consoleDisabledOptions = Seq("-Xfatal-warnings", "-Ywarn-unused", "-Ywarn-unused-import")

Expand Down Expand Up @@ -80,24 +81,12 @@ lazy val baseLibSettings: Project => Project = _.settings(
"-encoding",
"UTF-8",
"-feature",
"-language:implicitConversions"
"-language:implicitConversions",
// disabled during the migration
// "-Xfatal-warnings"
) ++
(CrossVersion.partialVersion(scalaVersion.value) match {
case Some((3, _)) =>
Seq(
"-unchecked",
"-source:3.0-migration"
)
case _ =>
Seq(
"-deprecation",
"-Xfatal-warnings",
"-Wunused:imports,privates,locals",
"-Wvalue-discard"
)
})
"-unchecked",
"-source:3.0-migration"
)
},
libraryDependencies ++= Seq(
"org.scalatest" %%% "scalatest" % "3.2.12" % Test
Expand Down Expand Up @@ -261,12 +250,12 @@ lazy val spraWeb = (project in file("spra-web"))
"react" -> "17.0.0",
"react-dom" -> "17.0.0",
"react-scripts" -> "5.0.0",
"react-admin" -> "4.1.0",
"ra-ui-materialui" -> "4.1.0",
"ra-data-simple-rest" -> "4.1.0",
"ra-i18n-polyglot" -> "4.1.0",
"ra-language-english" -> "4.1.0",
"ra-core" -> "4.1.0",
"react-admin" -> reactAdmin,
"ra-ui-materialui" -> reactAdmin,
"ra-data-simple-rest" -> reactAdmin,
"ra-i18n-polyglot" -> reactAdmin,
"ra-language-english" -> reactAdmin,
"ra-core" -> reactAdmin,
"@mui/material" -> "5.8.1",
"@emotion/styled" -> "11.8.1"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ object AdminGetTables {
`type`: String,
editable: Boolean,
reference: Option[TableReference],
filterable: Boolean
filterable: Boolean,
isVisible: Boolean,
isRequiredOnCreate: Option[Boolean]
)
case class TableReference(referencedTable: String, referenceField: String)

Expand Down
22 changes: 13 additions & 9 deletions spra-play-server/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ dataExplorer {
# here you can define the tables that will be displayed in the data explorer with their
# respective configuration

# users {
# tableName = "users"
# primaryKeyField = "user_id"
# hiddenColumns = ["password", "email"]
# nonEditableColumns = ["user_id", "email", "created_at", "verified_on", "name", "last_name"]
# canBeDeleted = false
# filterableColumns = ["name", "last_name"]
# }
users {
tableName = "users"
primaryKeyField = "user_id"
hiddenColumns = ["password", "email"]
nonEditableColumns = ["user_id", "email", "created_at", "verified_on", "name"]
canBeDeleted = false
filterableColumns = ["name", "last_name"]
createFilter {
requiredColumns = ["name", "email", "password"]
nonRequiredColumns = ["last_name", "discord_webhook_url"]
}
}
}
}

Expand All @@ -40,7 +44,7 @@ play.filters.enabled += "play.filters.cors.CORSFilter"
db.default {
driver = "org.postgresql.Driver"
host = "localhost:5432"
database = "wiringbits_db"
database = "vacation_tracker_db"
username = "postgres"
password = "postgres"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import scala.util.Try
* columns that are filterable via react-admin
*/

case class CreateSettings(requiredColumns: List[String] = List.empty, nonRequiredColumns: List[String] = List.empty) {
override def toString: String =
s"CreateSettings(requiredColumns = $requiredColumns, nonRequiredColumns = $nonRequiredColumns)"
}

case class TableSettings(
tableName: String,
primaryKeyField: String,
Expand All @@ -34,13 +39,14 @@ case class TableSettings(
canBeDeleted: Boolean = true,
primaryKeyDataType: PrimaryKeyDataType = PrimaryKeyDataType.UUID,
columnTypeOverrides: Map[String, CustomDataType] = Map.empty,
filterableColumns: List[String] = List.empty
filterableColumns: List[String] = List.empty,
createSettings: CreateSettings = CreateSettings()
) {
override def toString: String =
s"""TableSettings(tableName = $tableName, primaryKeyField = $primaryKeyField, referenceField = $referenceField,
hiddenColumns = $hiddenColumns, nonEditableColumns = $nonEditableColumns, canBeDeleted = $canBeDeleted,
primaryKeyDataType = $primaryKeyDataType, columnTypeOverrides = $columnTypeOverrides,
filterableColumns = $filterableColumns)"""
filterableColumns = $filterableColumns, createSettings = $createSettings)"""
}

object TableSettings {
Expand Down Expand Up @@ -92,7 +98,11 @@ object TableSettings {
case string => throw new RuntimeException(s"Invalid primary key data type: $string")
},
columnTypeOverrides = handleColumnTypeOverrides(),
filterableColumns = getList[String]("filterableColumns")
filterableColumns = getList[String]("filterableColumns"),
createSettings = CreateSettings(
requiredColumns = getList[String]("createFilter.requiredColumns"),
nonRequiredColumns = getList[String]("createFilter.nonRequiredColumns")
)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.wiringbits.spra.admin.controllers
import net.wiringbits.spra.admin.config.DataExplorerConfig
import net.wiringbits.spra.admin.services.AdminService
import net.wiringbits.spra.admin.utils.models.QueryParameters
import net.wiringbits.spra.api.models
import net.wiringbits.spra.api.models.*
import org.slf4j.LoggerFactory
import play.api.libs.json.Json
Expand Down Expand Up @@ -53,14 +54,13 @@ class AdminController @Inject() (
} yield Ok(Json.toJson(response.map(Json.toJson(_))))
}

def create(tableName: String) = handleJsonBody[AdminCreateTable.Request] { request =>
val body = request.body
def create(tableName: String) = handleJsonBody[Map[String, String]] { request =>
val body = AdminCreateTable.Request(request.body)
for {
_ <- adminUser(request)
_ = logger.info(s"Create row in $tableName: ${body.data}")
_ <- adminService.create(tableName, body)
response = AdminCreateTable.Response()
} yield Ok(Json.toJson(response))
id <- adminService.create(tableName, body)
} yield Ok(Json.toJson(Map("id" -> id)))
}

def update(tableName: String, primaryKeyValue: String) = handleJsonBody[Map[String, String]] { request =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class DatabaseTablesRepository @Inject() (database: Database)(implicit
}
}

def create(tableName: String, body: Map[String, String]): Future[Unit] = Future {
def create(tableName: String, body: Map[String, String]): Future[String] = Future {
database.withConnection { implicit conn =>
val primaryKeyField = dataExplorerConfig.unsafeFindByName(tableName).primaryKeyField
val primaryKeyType = dataExplorerConfig.unsafeFindByName(tableName).primaryKeyDataType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ object DatabaseTablesDAO {
primaryKeyType: PrimaryKeyDataType = PrimaryKeyDataType.UUID
)(implicit
conn: Connection
): Unit = {
): String = {
val sql = QueryBuilder.create(tableName, body, primaryKeyField, primaryKeyType)
val preparedStatement = conn.prepareStatement(sql)

Expand All @@ -253,7 +253,9 @@ object DatabaseTablesDAO {
val value = body(body.keys.toList(j - i - 1))
preparedStatement.setObject(j, value)
}
val _ = preparedStatement.executeUpdate()
val result = preparedStatement.executeQuery()
result.next()
result.getString(1)
}

def update(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,27 @@ class AdminService @Inject() (
tableColumns <- databaseTablesRepository.getTableColumns(settings.tableName)
foreignKeys <- databaseTablesRepository.getForeignKeys(settings.tableName)

visibleColumns = tableColumns.filterNot(column => hiddenColumns.contains(column.name))
columns = visibleColumns.map { column =>
columns = tableColumns.map { column =>
val isVisible = !hiddenColumns.contains(column.name)
val fieldName = getColumnName(column.name, settings.primaryKeyField)
val isEditable = !settings.nonEditableColumns.contains(column.name)
val reference = getColumnReference(foreignKeys, column.name)
val isFilterable = settings.filterableColumns.contains(column.name)
val isRequiredOnCreate =
if (settings.createSettings.requiredColumns.contains(column.name))
Some(true)
else if (settings.createSettings.nonRequiredColumns.contains(column.name))
Some(false)
else
None
AdminGetTables.Response.TableColumn(
name = fieldName,
`type` = column.`type`,
editable = isEditable,
reference = reference,
filterable = isFilterable
filterable = isFilterable,
isVisible = isVisible,
isRequiredOnCreate = isRequiredOnCreate
)
}
} yield AdminGetTables.Response.DatabaseTable(
Expand Down Expand Up @@ -147,7 +156,7 @@ class AdminService @Inject() (
} yield maskedTableData
}

def create(tableName: String, request: AdminCreateTable.Request): Future[Unit] = {
def create(tableName: String, request: AdminCreateTable.Request): Future[String] = {
val body = request.data
val validations = {
validateTableName(tableName)
Expand All @@ -159,8 +168,8 @@ class AdminService @Inject() (

for {
_ <- validations
_ <- databaseTablesRepository.create(tableName, body)
} yield ()
id <- databaseTablesRepository.create(tableName, body)
} yield id
}

private def validateMissingFields(tableName: String, data: Map[String, String]): Future[Unit] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ object QueryBuilder {
|VALUES (
| ${sqlValues.toString()}
|)
|RETURNING $primaryKeyField::TEXT
|""".stripMargin
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package net.wiringbits.spra.ui.web

import net.wiringbits.spra.api.models.AdminGetTables
import net.wiringbits.spra.ui.web.components.{EditGuesser, ListGuesser}
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.models.DataExplorerSettings
Expand Down Expand Up @@ -46,7 +46,8 @@ object AdminView {
Resource.Props(
name = table.name,
list = ListGuesser(table),
edit = EditGuesser(table, props.dataExplorerSettings)
edit = EditGuesser(table, props.dataExplorerSettings),
create = CreateGuesser(table)
)
).withKey(table.name)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package net.wiringbits.spra.ui.web.components

import net.wiringbits.spra.api.models.AdminGetTables
import net.wiringbits.spra.ui.web.facades.reactadmin.*
import net.wiringbits.spra.ui.web.models.ColumnType
import net.wiringbits.spra.ui.web.utils.ResponseGuesser
import slinky.core.facade.{Fragment, ReactElement}
import slinky.core.{FunctionalComponent, KeyAddingStage}

import scala.scalajs.js

object CreateGuesser {
case class Props(response: AdminGetTables.Response.DatabaseTable)

def apply(response: AdminGetTables.Response.DatabaseTable): KeyAddingStage = component(Props(response = response))

val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
val fields = ResponseGuesser.getTypesFromResponse(props.response)
val inputs: Seq[ReactElement] = fields.map { field =>
field.isRequiredOnCreate
.map { isRequired =>
val required = if isRequired then ReactAdmin.required() else js.undefined

field.`type` match {
case ColumnType.Date =>
DateTimeInput(DateTimeInput.Props(source = field.name, isRequired = isRequired, validate = required))
case ColumnType.Text =>
TextInput(TextInput.Props(source = field.name, isRequired = isRequired, validate = required))
case ColumnType.Email =>
TextInput(TextInput.Props(source = field.name, isRequired = isRequired, validate = required))
case ColumnType.Image =>
ImageField(ImageField.Props(source = field.name, isRequired = isRequired, validate = required))
case ColumnType.Number =>
NumberInput(NumberInput.Props(source = field.name, isRequired = isRequired, validate = required))
case ColumnType.Reference(reference, source) =>
ReferenceInput(
ReferenceInput.Props(
source = field.name,
reference = reference,
children = Seq(SelectInput(SelectInput.Props(optionText = source, disabled = field.disabled))),
isRequired = isRequired,
validate = required
)
)
}
}
.getOrElse(Fragment())
}

Create(
Create.Props(
SimpleForm(
SimpleForm.Props(toolbar = Fragment(), children = inputs :+ SaveButton())
)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,25 @@ object EditGuesser {

val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
val fields = ResponseGuesser.getTypesFromResponse(props.response)
println(fields)
val inputs: Seq[ReactElement] = fields.map { field =>
field.`type` match {
case ColumnType.Date => DateTimeInput(DateTimeInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Text => TextInput(TextInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Email => TextInput(TextInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Image => ImageField(ImageField.Props(source = field.name))
case ColumnType.Number => NumberInput(NumberInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Reference(reference, source) =>
ReferenceInput(
ReferenceInput.Props(
source = field.name,
reference = reference,
children = Seq(SelectInput(SelectInput.Props(optionText = source, disabled = field.disabled)))
if !field.isVisible then Fragment()
else
field.`type` match {
case ColumnType.Date => DateTimeInput(DateTimeInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Text => TextInput(TextInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Email => TextInput(TextInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Image => ImageField(ImageField.Props(source = field.name))
case ColumnType.Number => NumberInput(NumberInput.Props(source = field.name, disabled = field.disabled))
case ColumnType.Reference(reference, source) =>
ReferenceInput(
ReferenceInput.Props(
source = field.name,
reference = reference,
children = Seq(SelectInput(SelectInput.Props(optionText = source, disabled = field.disabled)))
)
)
)
}
}
}

def onClick(action: ButtonAction, ctx: js.Dictionary[js.Any]): Unit = {
Expand Down
Loading

0 comments on commit f3c951f

Please sign in to comment.