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.Product import com.restapi.domain.PurchaseOrder import com.restapi.domain.Quotation import com.restapi.domain.Session.currentUser import com.restapi.domain.Session.database import com.restapi.domain.Session.findDataModelByEntityAndUniqId import com.restapi.domain.Vendor import com.restapi.integ.Scripting import io.ebean.CallableSql import io.ebean.RawSqlBuilder import io.javalin.http.* 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 RawQuery( val sql: String, val params: Map ) data class QueryById( val params: List ) @JsonDeserialize(using = QueryByIdParamsDeSerializer::class) sealed class QueryParam { data class Simple(val simple: String) : QueryParam() data class Complex(val type: QueryParamType, val value: String) : QueryParam() { fun getValueComplex(): 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")) } } } fun getValue(): Any { return when (this) { is Complex -> getValueComplex() is Simple -> simple } } } class QueryByIdParamsDeSerializer : JsonDeserializer() { override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): QueryParam { val node = p.readValueAsTree() return if (node.isTextual) { QueryParam.Simple(node.asText()) } else { QueryParam.Complex( QueryParamType.valueOf(node.get("type").textValue()), node.get("value").textValue(), ) } } } 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) { 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")) ) } 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) } database.save( AuditLog().apply { auditType = AuditType.CREATE this.entity = entity uniqueIdentifier = dataModel.uniqueIdentifier this.data = dataModel.data } ) } 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 Filters(val common :CommonFilters, val custom :CustomFilters) object PurchaseOrder { fun get(ctx :Context){ val id = ctx.pathParam("id") val po = database.find(PurchaseOrder::class.java, id) ?: throw NotFoundResponse("po not found for $id") ctx.json(po) } fun getAll(ctx :Context){ val filters = ctx.bodyAsClass() val poFilters :POFilters? = filters.custom as? POFilters val pos = searchPos(filters.common, poFilters) ctx.json(pos) } fun create(ctx :Context){ val po = ctx.bodyAsClass() database.save(po) ctx.result("po created") } fun approve(ctx :Context){ val id = ctx.pathParam("id") val po = database.find(PurchaseOrder::class.java, id) ?: throw NotFoundResponse("po not found for $id") po.approvalStatus = ApprovalStatus.APPROVED po.save() ctx.result("po with id $id approved") //reject all other pos pertaining to the same tx ?? } fun reject(ctx :Context){ val id = ctx.pathParam("id") val po = database.find(PurchaseOrder::class.java, id) ?: throw NotFoundResponse("po not found for $id") po.approvalStatus = ApprovalStatus.REJECTED po.save() ctx.result("po with id $id rejected") } fun quoteReference(ctx :Context){ //gets the quote reference on which this po is based on val id = ctx.pathParam("id") val quote = database.find(Quotation::class.java) .where() .eq("referenceQuotation", id) ?: throw NotFoundResponse("reference quotation not found for po $id") ctx.json(quote) } } data class ProductSearch( var isSort: String? = null ) object ProductCtrl { fun get(ctx :Context){ val hsnCode = ctx.pathParam("hsnCode") val product = database.find(Product::class.java, hsnCode) ?: throw NotFoundResponse("Product not found for $hsnCode") ctx.json(product) } fun getAll(ctx: Context){ val productList = Session.database.find(Product::class.java) .findList() .sortedBy { it.hsnCode } ctx.json(productList) } fun create(ctx :Context){ val product = ctx.bodyAsClass() database.save(product) } fun delete(ctx: Context) { val id = ctx.pathParam("id") val product = database.delete(Product::class.java, id) } fun patch(ctx: Context) { } fun update(ctx: Context) { val id = ctx.pathParam("id") } } object Quotation { fun get(ctx :Context){ val id = ctx.pathParam("id") val quote = database.find(Quotation::class.java, id) ?: throw NotFoundResponse("quote not found for $id") ctx.json(quote) } fun create(ctx :Context){ val quote = ctx.bodyAsClass() //we have to check if the quotation created date is below the expiry of rfq val rfq = database.find(com.restapi.domain.ReqForQuote::class.java) .where() .eq("reqForQuoteNum", quote.reqForQuoteNum) .findOne() if(rfq != null){ //compare dates if(quote.quoteDate!! <= rfq.openTill) { //valid database.save(quote) ctx.result("quote created") }else { ctx.result("request for quote closed") } }else { throw NotFoundResponse("request for quote not found for this quotation") } } fun delete(ctx :Context){ val id = ctx.pathParam("id") val quote = database.find(Quotation::class.java, id) ?: throw NotFoundResponse("quote not found for id $id") quote.delete() ctx.result("quote with $id deleted") } fun generatePO(ctx :Context){ //user should be redirected to a po form submission with prefilled values //create a PO object with values from the quote and then send it as body to vendor/po/create ?? } fun reqForQuote(ctx :Context){ val reqForQuoteNum = ctx.pathParam(("rfqNum")) val rfq = database.find(RequestForQuote::class.java) .where() .eq("reqForQuoteNum", reqForQuoteNum) ?: throw NotFoundResponse("request for quote not found for this quotation") ctx.json(rfq) } } object Document { fun get(ctx :Context){ val id = ctx.pathParam("id") val doc = database.find(Document::class.java, id) ?: throw NotFoundResponse("no doc found with id $id") ctx.json(doc) } fun create(ctx :Context){ val doc = ctx.bodyAsClass() database.save(doc) ctx.result("doc created") } fun print(ctx :Context){ //would be handled in the frontend ?? } fun delete(ctx :Context){ val id = ctx.pathParam("id") val doc = database.find(Document::class.java, id) ?: throw NotFoundResponse("no doc found with id $id") //doc.delete() ctx.result("document deleted") } fun getWithRefId(ctx :Context){ //fetches a particular doc (po, quote) with ref id val refId = ctx.pathParam("refId") val doc = database.find(Document::class.java) .where() .eq("refId", refId) ?: throw NotFoundResponse("no doc found for refId $refId") ctx.json(doc) } } object Vendor { fun get(ctx :Context){ val id = ctx.pathParam("id") val vendor = database.find(Vendor::class.java, id) ?: throw NotFoundResponse("no vendor found with id $id") ctx.json(vendor) } fun create(ctx :Context){ val vendor = ctx.bodyAsClass() database.save(vendor) ctx.result("vendor created") } fun update(ctx :Context){ } fun delete(ctx :Context){ } fun getQuotes(ctx :Context){ val id = ctx.pathParam("id") val quotes = database.find(Quotation::class.java) .where() .eq("vendor", id) .findList() ctx.json(quotes) } fun getPos(ctx :Context){ val id = ctx.pathParam("id") val pos = database.find(PurchaseOrder::class.java) .where() .eq("vendor", id) .findList() ctx.json(pos) } fun rate(ctx :Context){ val id = ctx.pathParam("id") val rating = ctx.pathParam("rating").toDouble() val vendor = database.find(Vendor::class.java, id) ?: throw NotFoundResponse("vendor not found for id $id") //could place some rating validation checks vendor.rating = rating vendor.save() ctx.result("rating changed") } } object RequestForQuote { fun create(ctx :Context) { val rfq = ctx.bodyAsClass() database.save(rfq) //ctx.result("request for quote created") //ctx.json(rfq) //ctx.status(HttpStatus.CREATED) //ctx.json("asss") } fun get(ctx :Context){ val id = ctx.pathParam("id") val rfq = database.find(ReqForQuote::class.java, id) ?: throw NotFoundResponse("request for quote not found for id $id") ctx.json(rfq) } fun update(ctx :Context){ //shuld we compare the new body fields with preexisting ones and prepare a sql query to update those fields?? } }