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 ) data class QueryById( val params: List ) enum class ResultType { INTEGER, DECIMAL, STRING, DATETIME, ARRAY, OBJECT } data class RejectAction(val reason: String) data class StoredProcedure(val input: Map, val output: Map = 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>() 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>() 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() 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>() ctx.json( Scripting.execute(file, name, params) ) } fun executeStoredProcedure(ctx: Context) { val name = ctx.pathParam("name") val sp = ctx.bodyAsClass() 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() 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() 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().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 } }