diff --git a/api.http b/api.http index 1e9aa29..00d5557 100644 --- a/api.http +++ b/api.http @@ -8,7 +8,7 @@ Authorization: {{auth-token}} "number": "KA01HD6677", "owner": "gowthaman" }, - "uniqueIdentifier": "" + "uniqueIdentifier": "KA01HD6677" } ### create row @@ -52,7 +52,7 @@ GET http://localhost:9001/api/log/log-0000000001 Authorization: Bearer {{auth-token}} ### get row -GET http://localhost:9001/api/vehicle/KA01HD6667 +GET http://localhost:9001/api/vehicle/KA01HD6677 Authorization: Bearer {{auth-token}} ### query row @@ -67,6 +67,15 @@ Authorization: {{set-auth-token}} } } +### search row +POST http://localhost:9001/api/vehicle/search +Content-Type: application/json +Authorization: {{auth-token}} + +{ + "number": "KA01HD6677" +} + ### update field PATCH http://localhost:9001/api/vehicle/KA01HD6667 Content-Type: application/json diff --git a/src/main/kotlin/com/restapi/Main.kt b/src/main/kotlin/com/restapi/Main.kt index 44eac63..857b6b3 100644 --- a/src/main/kotlin/com/restapi/Main.kt +++ b/src/main/kotlin/com/restapi/Main.kt @@ -300,17 +300,11 @@ fun main(args: Array) { ) } } - post("/script/database/{name}", Entities::executeStoredProcedure, Roles(adminRole, Role.DbOps)) - post("/script/{file}/{name}", Entities::executeScript, Roles(adminRole, Role.DbOps)) get("/{entity}/{id}", Entities::view, Roles(adminRole, viewRole)) - post("/{entity}/query/{id}", Entities::sqlQueryById, Roles(adminRole, viewRole)) - post("/{entity}/query", Entities::sqlQueryRaw, Roles(adminRole, viewRole)) + post("/{entity}/search", Entities::search, Roles(adminRole, viewRole)) post("/{entity}", Entities::create, Roles(adminRole, createRole)) - put("/{entity}/approve/{id}", Entities::approve, Roles(adminRole, approveOrRejectRole)) - put("/{entity}/reject/{id}", Entities::reject, Roles(adminRole, approveOrRejectRole)) - put("/{entity}/{action}/{id}", Entities::action, Roles(adminRole, Role.Entity)) put("/{entity}/{id}", Entities::update, Roles(adminRole, updateRole)) patch("/{entity}/{id}", Entities::patch, Roles(adminRole, updateRole)) diff --git a/src/main/kotlin/com/restapi/Test.kt b/src/main/kotlin/com/restapi/Test.kt deleted file mode 100644 index a1aef92..0000000 --- a/src/main/kotlin/com/restapi/Test.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.restapi - -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.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import com.restapi.controllers.QueryParam -import com.restapi.controllers.RawQuery - -class FDeserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, p1: DeserializationContext?): F { - //custom logic to do thia - val node = p.readValueAsTree() - - if (node.isObject) { - if (node.has("name")) { - return F.P(name = node.get("name").textValue()) - } else if (node.has("num")) { - return F.Q(num = node.get("num").textValue()) - } else if (node.has("tag")) { - return F.D(tag = node.get("tag").textValue()) - } - - } else { - //incorrect - } - throw IllegalArgumentException() - } - -} -val om = jacksonObjectMapper() - -@JsonDeserialize(using = FDeserializer::class) -sealed interface F { - data class P(val name: String) : F - data class Q(val num: String) : F - data class D(val tag: String) : F -} - -val j = """ - {"name":"a"} -""".trimIndent() - -val j2 = """ - {"num":"a"} -""".trimIndent() - -val j3 = """ - {"tag":"a"} -""".trimIndent() - -val j4 = """ - { - "sql":"aaaa", - "params": { - "a":"b", - "c": { - "type":"STRING", - "value":"aaaa" - } - - } - } -""".trimIndent() - -fun main() { - println(om.readValue(j)) - println(om.readValue(j2)) - println(om.readValue(j3)) - println(om.readValue(j4)) -} \ No newline at end of file diff --git a/src/main/kotlin/com/restapi/controllers/Entities.kt b/src/main/kotlin/com/restapi/controllers/Entities.kt index f74be3f..ecb57d5 100644 --- a/src/main/kotlin/com/restapi/controllers/Entities.kt +++ b/src/main/kotlin/com/restapi/controllers/Entities.kt @@ -6,82 +6,81 @@ 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.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.* +import io.javalin.http.BadRequestResponse +import io.javalin.http.Context +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 +import java.time.format.DateTimeParseException 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) +@JsonDeserialize(using = QueryParamDeSerializer::class) sealed class QueryParam { - data class Simple(val simple: String) : QueryParam() - data class Complex(val type: QueryParamType, val value: String) : QueryParam() { + data class StrParam(val str: String) : QueryParam() + data class NumberParam(val nbr: Number) : 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.toLong() else value.toDouble() - QueryParamType.DATETIME -> LocalDateTime.parse( - value, - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - ) + QueryParamType.NUMBER -> (if (value.matches(Regex("\\d+"))) value.toLongOrNull() else value.toDoubleOrNull()) + ?: throw IllegalArgumentException("$value is not a number") - QueryParamType.DATE -> LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd")) + 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 Complex -> getValueComplex() - is Simple -> simple - else -> {} + is ComplexParam -> getValueComplex() + is StrParam -> str + is NumberParam -> nbr } } } -class QueryByIdParamsDeSerializer : JsonDeserializer() { +class QueryParamDeSerializer : JsonDeserializer() { override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): QueryParam { val node = p.readValueAsTree() return if (node.isTextual) { - QueryParam.Simple(node.asText()) - } else { - QueryParam.Complex( + QueryParam.StrParam(node.asText()) + } else if (node.isNumber) { + QueryParam.NumberParam(node.numberValue()) + } 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") } } } -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") @@ -116,108 +115,22 @@ object Entities { 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}") + fun search(ctx: Context) { + val sql = ctx.bodyAsClass() 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()) + .where() + .eq("entityName", entity) .apply { - sql.params.forEachIndexed { index, entry -> - setParameter(index + 1, entry.getValue()) + sql.forEach { (t, u) -> + + if (!SafeStringDeserializer.isSafe(t)) { + throw IllegalArgumentException() + } + eq("data->>'$t'", u.getValue()) } } .findList() @@ -225,6 +138,7 @@ object Entities { } + fun view(it: Context) { database.save( AuditLog().apply { @@ -317,18 +231,7 @@ object Entities { } } } - 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 - } } } @@ -371,5 +274,7 @@ object Entities { } } +typealias SearchParams = Map + data class SequenceNumber(val number: String) diff --git a/src/main/kotlin/com/restapi/domain/models.kt b/src/main/kotlin/com/restapi/domain/models.kt index f450160..4b810c1 100644 --- a/src/main/kotlin/com/restapi/domain/models.kt +++ b/src/main/kotlin/com/restapi/domain/models.kt @@ -179,18 +179,6 @@ enum class JobType { SCRIPT, DB } -@Entity -@Index(unique = true, name = "sql_unique_id", columnNames = ["entity_name", "sql_id", "tenant_id"]) -open class SqlModel : BaseTenantModel() { - - @JsonDeserialize(using = SafeStringDeserializer::class) - var sqlId: String = "" - - var entityName: String = "" - - @Column(columnDefinition = "text") - var sql: String = "" -} @Entity open class JobModel : BaseTenantModel() { @@ -242,12 +230,17 @@ open class AnonSession : BaseTenantModel() { } class SafeStringDeserializer : JsonDeserializer() { - private val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$") + + companion object { + private val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$") + + fun isSafe(s: String) = regex.matches(s) + } override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): String { val text = p.text - if (!regex.matches(text)) throw IllegalArgumentException() + if (!isSafe(text)) throw IllegalArgumentException() return text } }