2023-11-13 19:59:41 +05:30

320 lines
12 KiB
Kotlin

package com.restapi.controllers
import com.restapi.domain.*
import com.restapi.domain.Session.currentUser
import com.restapi.domain.Session.database
import com.restapi.domain.Session.findDataModelByEntityAndUniqId
import com.restapi.integ.Scripting
import io.ebean.CallableSql
import io.ebean.RawSqlBuilder
import io.javalin.http.BadRequestResponse
import io.javalin.http.Context
import io.javalin.http.NotFoundResponse
import io.javalin.http.bodyAsClass
import org.slf4j.LoggerFactory
import java.sql.Types
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
enum class QueryParamType {
STRING, NUMBER, DATETIME, DATE
}
data class QueryParam(val type: QueryParamType, val value: String) {
fun getValue(): Any {
return when (type) {
QueryParamType.STRING -> value
QueryParamType.NUMBER -> if (value.matches(Regex("\\d+"))) value.toLong() else value.toDouble()
QueryParamType.DATETIME -> LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
QueryParamType.DATE -> LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
}
}
}
data class RawQuery(
val sql: String,
val params: Map<String, QueryParam>
)
data class QueryById(
val params: List<QueryParam>
)
enum class ResultType {
INTEGER, DECIMAL, STRING, DATETIME, ARRAY, OBJECT
}
data class RejectAction(val reason: String)
data class StoredProcedure(val input: Map<String, Any>, val output: Map<String, ResultType> = hashMapOf())
object Entities {
private val logger = LoggerFactory.getLogger("Entities")
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()
}
fun patch(ctx: Context) {
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
val pv = ctx.bodyAsClass<Map<String, Any>>()
pv.forEach { (key, value) ->
e.data[key] = value;
}
e.update()
}
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>>()
if (purgeExisting) {
e.data.clear();
}
e.data.putAll(newData)
e.update()
}
fun action(ctx: Context) {}
fun approve(ctx: Context) {
approveOrReject(ctx, ApprovalStatus.APPROVED)
}
fun reject(ctx: Context) {
approveOrReject(ctx, ApprovalStatus.REJECTED)
}
private fun approveOrReject(ctx: Context, rejected: ApprovalStatus) {
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
val reject = ctx.bodyAsClass<RejectAction>()
e.approvalStatus = rejected
e.comments.add(Comments(text = reject.reason, by = currentUser()))
e.save()
}
fun executeScript(ctx: Context) {
val name = ctx.pathParam("name")
val file = ctx.pathParam("file")
val params = ctx.bodyAsClass<Map<String, Any>>()
ctx.json(
Scripting.execute(file, name, params)
)
}
fun executeStoredProcedure(ctx: Context) {
val name = ctx.pathParam("name")
val sp = ctx.bodyAsClass<StoredProcedure>()
val inputParams = sp.input.entries.toList()
val outputParams = sp.output.entries.toList()
val placeholders = (0..inputParams.size + 1).joinToString(",") { "?" }
val sql = "{call $name($placeholders)}"
val cs: CallableSql = database.createCallableSql(sql)
inputParams.forEachIndexed { index, entry ->
cs.setParameter(index + 1, entry.value)
}
cs.setParameter(inputParams.size + 1, Session.currentTenant())
outputParams.forEachIndexed { idx, entry ->
when (entry.value) {
ResultType.INTEGER -> cs.registerOut(idx + 1, Types.INTEGER)
ResultType.DECIMAL -> cs.registerOut(idx + 1, Types.DOUBLE)
ResultType.STRING -> cs.registerOut(idx + 1, Types.VARCHAR)
ResultType.DATETIME -> cs.registerOut(idx + 1, Types.DATE)
ResultType.ARRAY -> cs.registerOut(idx + 1, Types.ARRAY)
ResultType.OBJECT -> cs.registerOut(idx + 1, Types.JAVA_OBJECT)
}
}
val done = database.execute(cs)
val output = outputParams.mapIndexed { index, entry ->
Pair(entry.key, cs.getObject(index + 1))
}.toMap()
ctx.json(
mapOf(
"done" to done,
"output" to output
)
)
}
fun sqlQueryRaw(ctx: Context) {
val sql = ctx.bodyAsClass<RawQuery>()
logger.warn("running sql ${sql.sql}, with params ${sql.params}")
ctx.json(
database.find(DataModel::class.java)
.setRawSql(
RawSqlBuilder.parse(sql.sql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u.getValue())
}
}
.findList()
)
}
fun sqlQueryById(ctx: Context) {
val sql = ctx.bodyAsClass<QueryById>()
val sqlId = ctx.pathParam("id")
logger.warn("running sqlId $sqlId, with params ${sql.params}")
val entity = ctx.pathParam("entity")
val query = database.find(SqlModel::class.java)
.where()
.eq("entityName", entity)
.eq("sqlId", sqlId)
.findOne() ?: throw NotFoundResponse("sql not found for $entity, $sqlId")
ctx.json(
database.find(DataModel::class.java)
.setRawSql(RawSqlBuilder.parse(query.sql).create())
.apply {
sql.params.forEachIndexed { index, entry ->
setParameter(index+1, entry.getValue())
}
}
.findList()
)
}
fun view(it: Context) {
it.json(
database.findDataModelByEntityAndUniqId(it.pathParam("entity"), it.pathParam("id"))
)
}
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
}
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.preSaveScript.isNullOrEmpty()) {
val ok = Scripting.execute(setupEntity.preSaveScript!!, "preSave", this) as Boolean
if (!ok) {
throw BadRequestResponse("PreSave Failed")
}
}
if (setupEntity.approvalLevels > 0) {
this.approvalStatus = ApprovalStatus.PENDING
this.requiredApprovalLevels = setupEntity.approvalLevels
}
}
}
)
if (setupEntity != null && !setupEntity.postSaveScript.isNullOrEmpty()) {
Scripting.execute(setupEntity.postSaveScript!!, "postSave", dataModel)
}
}
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
}
}