2024-08-15 15:29:13 +05:30

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)