414 lines
15 KiB
Kotlin
414 lines
15 KiB
Kotlin
package com.restapi.controllers
|
|
|
|
import com.fasterxml.jackson.core.JsonParser
|
|
import com.fasterxml.jackson.databind.DeserializationContext
|
|
import com.fasterxml.jackson.databind.JsonDeserializer
|
|
import com.fasterxml.jackson.databind.JsonNode
|
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
|
import com.restapi.domain.*
|
|
import com.restapi.domain.Session.currentRoles
|
|
import com.restapi.domain.Session.currentUser
|
|
import com.restapi.domain.Session.database
|
|
import com.restapi.domain.Session.findDataModelByEntityAndUniqId
|
|
import com.restapi.domain.Session.objectMapper
|
|
import com.restapi.integ.Scripting
|
|
import io.ebean.RawSqlBuilder
|
|
import io.javalin.http.BadRequestResponse
|
|
import io.javalin.http.Context
|
|
import io.javalin.http.HttpStatus
|
|
import io.javalin.http.bodyAsClass
|
|
import org.slf4j.LoggerFactory
|
|
import java.time.LocalDate
|
|
import java.time.LocalDateTime
|
|
import java.time.LocalTime
|
|
import java.time.format.DateTimeFormatter
|
|
import java.time.format.DateTimeParseException
|
|
|
|
enum class QueryParamType {
|
|
STRING, NUMBER, DATETIME, DATE
|
|
}
|
|
|
|
|
|
@JsonDeserialize(using = QueryParamDeSerializer::class)
|
|
sealed class QueryParam {
|
|
data class StrParam(val str: String) : QueryParam()
|
|
data class NumberParam(val nbr: Number) : QueryParam()
|
|
data class BooleanParam(val bool: Boolean) : QueryParam()
|
|
data class ComplexParam(val type: QueryParamType, val value: String) : QueryParam() {
|
|
fun getValueComplex(): Any {
|
|
return when (type) {
|
|
QueryParamType.STRING -> value
|
|
QueryParamType.NUMBER -> (if (value.matches(Regex("\\d+"))) value.toLongOrNull() else value.toDoubleOrNull())
|
|
?: throw IllegalArgumentException("$value is not a number")
|
|
|
|
QueryParamType.DATETIME -> try {
|
|
LocalDateTime.parse(
|
|
value,
|
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
|
)
|
|
} catch (e: DateTimeParseException) {
|
|
throw IllegalArgumentException("unable to parse $value as datetime")
|
|
}
|
|
|
|
QueryParamType.DATE -> try {
|
|
LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
|
} catch (e: DateTimeParseException) {
|
|
throw IllegalArgumentException("unable to parse $value as date")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getValue(): Any {
|
|
return when (this) {
|
|
is ComplexParam -> getValueComplex()
|
|
is StrParam -> str
|
|
is NumberParam -> nbr
|
|
is BooleanParam -> bool
|
|
}
|
|
}
|
|
}
|
|
|
|
class QueryParamDeSerializer : JsonDeserializer<QueryParam>() {
|
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): QueryParam {
|
|
val node = p.readValueAsTree<JsonNode>()
|
|
return if (node.isTextual) {
|
|
QueryParam.StrParam(node.asText())
|
|
} else if (node.isNumber) {
|
|
QueryParam.NumberParam(node.numberValue())
|
|
} else if (node.isBoolean) {
|
|
QueryParam.BooleanParam(node.booleanValue())
|
|
} else if (node.isObject) {
|
|
QueryParam.ComplexParam(
|
|
QueryParamType.valueOf(node.get("type").textValue()),
|
|
node.get("value").textValue(),
|
|
)
|
|
} else {
|
|
throw BadRequestResponse("unable to find valid query param for $node")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
object Entities {
|
|
private val logger = LoggerFactory.getLogger("Entities")
|
|
private val OK = mapOf("status" to true)
|
|
fun delete(ctx: Context) {
|
|
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
|
|
e.deletedBy = Session.currentUser()
|
|
e.deletedOn = LocalDateTime.now()
|
|
e.update()
|
|
e.delete()
|
|
ctx.json(OK)
|
|
}
|
|
|
|
fun patch(ctx: Context) {
|
|
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
|
|
val pv = ctx.bodyAsClass<Map<String, Any>>()
|
|
verifyKeys(pv)
|
|
|
|
pv.forEach { (key, value) ->
|
|
e.data[key] = value;
|
|
}
|
|
|
|
e.update()
|
|
ctx.json(OK)
|
|
}
|
|
|
|
fun update(ctx: Context) {
|
|
val purgeExisting = ctx.queryParam("purge")?.toBooleanStrictOrNull() == true
|
|
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
|
|
|
|
val newData = ctx.bodyAsClass<Map<String, Any>>()
|
|
verifyKeys(newData)
|
|
if (purgeExisting) {
|
|
e.data.clear();
|
|
}
|
|
e.data.putAll(newData)
|
|
|
|
e.update()
|
|
ctx.json(OK)
|
|
}
|
|
|
|
private fun verifyKeys(newData: Map<String, Any>) {
|
|
newData.keys.forEach { key ->
|
|
if (!SafeStringDeserializer.isSafe(key)) throw IllegalArgumentException("$key is invalid from $newData ")
|
|
}
|
|
}
|
|
|
|
|
|
fun search(ctx: Context) {
|
|
val sql = ctx.bodyAsClass<SearchParams>()
|
|
verifyKeys(sql.params)
|
|
|
|
val entity = ctx.pathParam("entity").lowercase()
|
|
val noCreatedFilter = currentRoles().contains("ROLE_ADMIN") || sql.createdBy.isNullOrEmpty()
|
|
val createdFilter = if (noCreatedFilter) "" else "and created_by = :cBy"
|
|
val searchJsonMap = sql.params.map { e -> Pair(e.key, e.value.getValue()) }
|
|
.filter {
|
|
val second = it.second
|
|
if (second is String) {
|
|
second.isNotEmpty()
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
.toMap()
|
|
logger.warn("convert ${sql.params} to $searchJsonMap")
|
|
val fl = database.find(DataModel::class.java)
|
|
.setRawSql(
|
|
RawSqlBuilder.parse(
|
|
"""
|
|
select sys_pk,
|
|
deleted_on,
|
|
current_approval_level,
|
|
required_approval_levels,
|
|
deleted,
|
|
version,
|
|
created_at,
|
|
modified_at,
|
|
deleted_by,
|
|
approval_status,
|
|
tags,
|
|
comments,
|
|
tenant_id,
|
|
unique_identifier,
|
|
entity_name,
|
|
data,
|
|
created_by,
|
|
modified_by
|
|
from data_model
|
|
where entity_name = :e
|
|
and created_at between :from and :to
|
|
and data @> cast(:search as jsonb)
|
|
and deleted = false
|
|
$createdFilter
|
|
order by ${sql.orderBy}
|
|
""".trimIndent()
|
|
).create()
|
|
)
|
|
.setParameter("from", sql.dateRange.first())
|
|
.setParameter("to", sql.dateRange.last().plusDays(1))
|
|
.setParameter("e", entity)
|
|
.setParameter("search", objectMapper.writeValueAsString(searchJsonMap))
|
|
.apply {
|
|
if (!noCreatedFilter) {
|
|
logger.warn("Set Created By Filter to ${currentUser()}")
|
|
setParameter("cBy", currentUser())
|
|
}
|
|
}
|
|
.findList()
|
|
|
|
logger.warn("Search jsonMap [$searchJsonMap] => ${fl.size} entries")
|
|
|
|
ctx.json(fl)
|
|
|
|
}
|
|
|
|
|
|
fun getAll(ctx: Context) {
|
|
val entity = ctx.pathParam("entity").uppercase()
|
|
val pageNo = ctx.queryParam("pageNo")?.toInt() ?: 1
|
|
val perPage = ctx.queryParam("perPage")?.toInt() ?: 100
|
|
val cnt = database.find(DataModel::class.java)
|
|
.where()
|
|
.eq("entityName", entity.lowercase())
|
|
.setFirstRow((pageNo - 1) * perPage)
|
|
.setMaxRows(perPage)
|
|
.orderBy("sysPk desc")
|
|
.findPagedList()
|
|
|
|
ctx.json(cnt)
|
|
}
|
|
|
|
fun getNextSeqNo(ctx: Context) {
|
|
val entity = ctx.pathParam("entity").uppercase()
|
|
|
|
val prefix = "$entity/"
|
|
val plantId = ctx.queryParam("plantId") ?: throw BadRequestResponse("plantId not sent")
|
|
val plant = database.find(Plant::class.java)
|
|
.where()
|
|
.eq("plantId", plantId)
|
|
.findOne() ?: throw BadRequestResponse("plant missing for $plantId")
|
|
|
|
val inventoryPrefix = plant.prefixes?.get(entity) ?: prefix
|
|
|
|
val cnt = (database.find(DataModel::class.java)
|
|
.where()
|
|
.eq("entityName", entity.lowercase())
|
|
.eq("data->>'plantId'", plantId)
|
|
.findCount() + 1)
|
|
.toString()
|
|
.padStart(6, '0')
|
|
|
|
val seq = SequenceNumber(inventoryPrefix + cnt)
|
|
ctx.json(seq).status(HttpStatus.OK)
|
|
}
|
|
|
|
fun view(it: Context) {
|
|
database.save(
|
|
AuditLog().apply {
|
|
auditType = AuditType.VIEW
|
|
entity = it.pathParam("entity")
|
|
uniqueIdentifier = it.pathParam("id")
|
|
}
|
|
)
|
|
it.json(
|
|
database.findDataModelByEntityAndUniqId(it.pathParam("entity"), it.pathParam("id"))
|
|
)
|
|
}
|
|
|
|
data class Execute(val script: String, val fn: String, val params: Map<String, Any>)
|
|
|
|
fun execute(ctx: Context) {
|
|
val entity = ctx.pathParam("entity")
|
|
|
|
val body = ctx.bodyAsClass<Execute>()
|
|
|
|
//may be approval flow is configured?
|
|
val setupEntity = database.find(EntityModel::class.java)
|
|
.where()
|
|
.eq("name", entity)
|
|
.findOne()
|
|
|
|
ctx.json(
|
|
Scripting.execute(body.script, body.fn, body.params, setupEntity)
|
|
)
|
|
|
|
}
|
|
|
|
fun create(ctx: Context) {
|
|
|
|
val entity = ctx.pathParam("entity")
|
|
|
|
|
|
//may be approval flow is configured?
|
|
val setupEntity = database.find(EntityModel::class.java)
|
|
.where()
|
|
.eq("name", entity)
|
|
.findOne()
|
|
|
|
Session.creatSeq(entity)
|
|
val dataModel = ctx.bodyAsClass<DataModel>().apply {
|
|
this.entityName = entity
|
|
if (this.uniqueIdentifier.isEmpty()) {
|
|
this.uniqueIdentifier = Session.nextUniqId(entity)
|
|
}
|
|
this.approvalStatus = ApprovalStatus.APPROVED
|
|
}
|
|
verifyKeys(dataModel.data)
|
|
|
|
database.save(
|
|
dataModel.apply {
|
|
if (setupEntity != null) {
|
|
|
|
val allowedFields = setupEntity.allowedFields.map { it.lowercase() }
|
|
if (allowedFields.isNotEmpty()) {
|
|
|
|
val moreFields =
|
|
dataModel.data.keys.map { it.lowercase() }.filter { !allowedFields.contains(it) }
|
|
|
|
if (moreFields.isNotEmpty()) {
|
|
logger.warn("Data Keys = ${dataModel.data.keys} is more than $allowedFields, extra fields = $moreFields")
|
|
throw BadRequestResponse("data contains more fields than allowed")
|
|
}
|
|
|
|
setupEntity.allowedFieldTypes.forEach { (key, expectedType) ->
|
|
|
|
val valueFromUser = dataModel.data[key] ?: return@forEach
|
|
val isDate = expectedType.equals("date", ignoreCase = true)
|
|
val isDateTime = expectedType.equals("datetime", ignoreCase = true)
|
|
val isTime = expectedType.equals("time", ignoreCase = true)
|
|
if (isDate || isDateTime || isTime) {
|
|
//this should be a string of a particular format
|
|
if (valueFromUser !is String) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
} else {
|
|
val dtPattern = Regex("^\\d{4}-\\d{2}-\\d{2}$")
|
|
val dtmPattern = Regex("^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}$")
|
|
val timePattern = Regex("^\\d{2}:\\d{2}$")
|
|
|
|
|
|
if (isDate
|
|
&& !dtPattern.matches(valueFromUser)
|
|
&& !isValidDate(valueFromUser)
|
|
) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
|
|
|
|
if (isDateTime
|
|
&& !dtmPattern.matches(valueFromUser)
|
|
&& !isValidDateTime(valueFromUser)
|
|
) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
|
|
if (isTime
|
|
&& !timePattern.matches(valueFromUser)
|
|
&& !isValidTime(valueFromUser)
|
|
) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
}
|
|
}
|
|
if (valueFromUser.javaClass.simpleName != expectedType) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
)
|
|
|
|
if (setupEntity != null && !setupEntity.postSaveScript.isNullOrEmpty()) {
|
|
Scripting.execute(setupEntity.postSaveScript!!, "postSave", dataModel, setupEntity)
|
|
}
|
|
|
|
|
|
database.save(
|
|
AuditLog().apply {
|
|
this.auditType = AuditType.CREATE
|
|
this.entity = entity
|
|
this.uniqueIdentifier = dataModel.uniqueIdentifier
|
|
this.data = dataModel.data
|
|
}
|
|
)
|
|
ctx.json(OK)
|
|
}
|
|
|
|
private fun isValidDate(f: String) = try {
|
|
LocalDate.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
|
true
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
|
|
private fun isValidDateTime(f: String) = try {
|
|
LocalDateTime.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
|
true
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
|
|
private fun isValidTime(f: String) = try {
|
|
LocalTime.parse(f, DateTimeFormatter.ofPattern("HH:mm"))
|
|
true
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
}
|
|
|
|
data class SearchParams(
|
|
val params: Map<String, QueryParam> = mapOf(),
|
|
val createdBy: String?,
|
|
val dateRange: List<LocalDate> = listOf(LocalDate.now().minusDays(7), LocalDate.now()),
|
|
val orderBy: String = "sysPk asc"
|
|
)
|
|
|
|
data class SequenceNumber(val number: String)
|
|
|