simple api model

This commit is contained in:
gowthaman.b
2023-11-10 11:06:14 +05:30
parent 203c2b97a7
commit 01e197b0f8
14 changed files with 364 additions and 232 deletions

View File

@@ -0,0 +1,177 @@
package com.restapi
import com.fasterxml.jackson.databind.JsonMappingException
import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.domain.DataModel
import com.restapi.domain.DataNotFoundException
import com.restapi.domain.Session
import com.restapi.domain.Session.creatSeq
import com.restapi.domain.Session.database
import com.restapi.domain.Session.findByEntityAndId
import com.restapi.domain.Session.nextUniqId
import io.ebean.CallableSql
import io.ebean.DuplicateKeyException
import io.ebean.RawSqlBuilder
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.http.*
import io.javalin.json.JavalinJackson
import org.slf4j.LoggerFactory
import java.time.LocalDateTime
fun main(args: Array<String>) {
val logger = LoggerFactory.getLogger("api")
Javalin
.create { cfg ->
cfg.http.generateEtags = true
if (appConfig.corsEnabled()) {
cfg.plugins.enableCors { container ->
container.add {
it.allowHost(
"http://localhost:5173",
*appConfig.corsHosts().toTypedArray()
)
}
}
}
cfg.http.defaultContentType = ContentType.JSON
cfg.compression.gzipOnly()
cfg.jsonMapper(JavalinJackson(Session.objectMapper))
}
.routes {
before("/*") { ctx ->
//validate, auth token
//allow only alpha, numeric, hypen, underscore, dot in paths
val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$")
ctx.path().split("/").dropWhile { it.isEmpty() }
.forEach {
if (!it.matches(regex)) {
throw IllegalArgumentException()
}
}
}
path("/api") {
post("/execute/{name}") {
val name = it.pathParam("name")
val params = it.bodyAsClass<Map<String, Any>>()
val placeholders = (0..params.entries.size).joinToString(",") { "?" }
val sql = "{call $name($placeholders)}"
val cs: CallableSql = database.createCallableSql(sql)
params.entries.forEachIndexed { index, entry ->
cs.setParameter(index + 1, entry.value)
}
database.execute(cs)
}
get("/{entity}/{id}") {
it.json(
database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
)
}
post("/{entity}/query/{id}") {
val sql = it.bodyAsClass<Query>()
val query = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
val querySql = query.data["sql"] as String? ?: throw NotFoundResponse()
it.json(
database.find(DataModel::class.java)
.setRawSql(
RawSqlBuilder.parse(querySql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u)
}
}
.findList()
)
}
post("/{entity}/query") {
val sql = it.bodyAsClass<Query>()
it.json(
database.find(DataModel::class.java)
.setRawSql(
RawSqlBuilder.parse(sql.sql).create()
).apply {
sql.params.forEach { (t, u) ->
setParameter(t, u)
}
}
.findList()
)
}
post("/{entity}") {
val entity = it.pathParam("entity")
val seqCreated = creatSeq(entity)
logger.debug("sequence created for $entity? = $seqCreated")
database.save(
it.bodyAsClass<DataModel>().apply {
this.entityName = entity
if (this.uniqueIdentifier.isEmpty()) {
this.uniqueIdentifier = nextUniqId(entity)
}
}
)
}
put("/{entity}/{id}") {
val e = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
val newData = it.bodyAsClass<Map<String, Any>>()
e.data.putAll(newData)
e.update()
}
patch("/{entity}/{id}") {
val e = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
val pv = it.bodyAsClass<PatchValue>()
e.data[pv.key] = pv.value;
e.update()
}
delete("/{entity}/{id}") {
val id = it.pathParam("id")
val e = database.findByEntityAndId(it.pathParam("entity"), it.pathParam("id"))
e.deletedBy = Session.currentUser()
e.deletedOn = LocalDateTime.now()
e.update()
e.delete()
}
}
}
.exception(DuplicateKeyException::class.java) { _, ctx ->
ctx.json(
mapOf(
"error" to "Duplicate Data"
)
).status(HttpStatus.CONFLICT)
}
.exception(DataNotFoundException::class.java) { _, ctx ->
ctx.json(
mapOf(
"error" to "Data Not Found"
)
).status(HttpStatus.NOT_FOUND)
}
.exception(IllegalArgumentException::class.java) { _, ctx ->
ctx.json(
mapOf(
"error" to "Incorrect Data"
)
).status(HttpStatus.BAD_REQUEST)
}
.exception(JsonMappingException::class.java) { _, ctx ->
ctx.json(
mapOf(
"error" to "Incorrect Data"
)
).status(HttpStatus.BAD_REQUEST)
}
.start(appConfig.portNumber())
}
data class Query(
val sql: String,
val params: Map<String, Any>
)
data class PatchValue(val key: String, val value: Any)

View File

@@ -0,0 +1,48 @@
package com.restapi.config
import net.cactusthorn.config.core.Config
import net.cactusthorn.config.core.Default
import net.cactusthorn.config.core.Key
import net.cactusthorn.config.core.factory.ConfigFactory
import net.cactusthorn.config.core.loader.LoadStrategy
const val INITIAL_ROLES_JSON = """{
"roles": []
}"""
@Config(
sources = [
"file:~/app.properties", "system:env"
],
loadStrategy = LoadStrategy.FIRST_KEYCASEINSENSITIVE
)
interface AppConfig {
@Key("app.cors.enabled")
@Default("false")
fun corsEnabled(): Boolean
@Key("app.port")
@Default("9001")
fun portNumber(): Int
@Key("app.cors.hosts")
@Default("*")
fun corsHosts(): List<String>
@Key("app.db.user")
fun dbUser(): String
@Key("app.db.pass")
fun dbPass(): String
@Key("app.db.url")
fun dbUrl(): String
@Key("app.db.run_migration")
fun dbRunMigration(): Boolean
companion object {
val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java)
}
}

View File

@@ -0,0 +1,75 @@
package com.restapi.domain
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.restapi.config.AppConfig.Companion.appConfig
import io.ebean.Database
import io.ebean.DatabaseFactory
import io.ebean.config.CurrentTenantProvider
import io.ebean.config.CurrentUserProvider
import io.ebean.config.DatabaseConfig
import io.ebean.config.TenantMode
import java.util.*
data class CurrentUser(
val anon: Boolean = true,
val userId: String = "",
val tenantId: String = ""
)
object Session {
private val currentUser = object : ThreadLocal<CurrentUser>() {
override fun initialValue(): CurrentUser {
return CurrentUser()
}
}
private val sc = DatabaseConfig().apply {
loadFromProperties(Properties().apply {
setProperty("datasource.db.username", appConfig.dbUser())
setProperty("datasource.db.password", appConfig.dbPass())
setProperty("datasource.db.url", appConfig.dbUrl())
setProperty("ebean.migration.run", appConfig.dbRunMigration().toString())
})
tenantMode = TenantMode.PARTITION
currentTenantProvider = CurrentTenantProvider { currentUser.get().tenantId }
currentUserProvider = CurrentUserProvider { currentUser.get().userId }
}
val database: Database = DatabaseFactory.create(sc)
val objectMapper = jacksonObjectMapper().apply {
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
findAndRegisterModules()
}
fun currentUser() = currentUser.get().userId
fun Database.findByEntityAndId(entity: String, id: String): DataModel {
return find(DataModel::class.java)
.where()
.eq("uniqueIdentifier", id)
.eq("entityName", entity)
.findOne() ?: throw DataNotFoundException
}
private fun seqName(entity: String) = "sequence_$entity"
fun creatSeq(entity: String): Int {
return database.sqlUpdate("CREATE SEQUENCE IF NOT EXISTS ${seqName(entity)} START 1;").execute()
}
fun nextUniqId(entity: String): String {
val s = database
.sqlQuery("SELECT nextval('${seqName(entity)}');")
.findOne()?.getLong("nextval") ?: throw DataNotFoundException
return String.format("%s-%s", entity, "$s".padStart(10, '0'))
}
}
object DataNotFoundException : Exception() {
private fun readResolve(): Any = DataNotFoundException
}

View File

@@ -0,0 +1,19 @@
package com.restapi.domain
import io.ebean.annotation.Platform
import io.ebean.dbmigration.DbMigration
object DBMigration {
private fun create(){
val dbMigration: DbMigration = DbMigration.create()
dbMigration.setPlatform(Platform.POSTGRES)
dbMigration.generateMigration()
}
@JvmStatic
fun main(args: Array<String>) {
create()
}
}

View File

@@ -0,0 +1,91 @@
package com.restapi.domain
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import io.ebean.Model
import io.ebean.annotation.DbArray
import io.ebean.annotation.DbJsonB
import io.ebean.annotation.Index
import io.ebean.annotation.Platform
import io.ebean.annotation.SoftDelete
import io.ebean.annotation.TenantId
import io.ebean.annotation.WhenCreated
import io.ebean.annotation.WhenModified
import io.ebean.annotation.WhoCreated
import io.ebean.annotation.WhoModified
import java.time.LocalDateTime
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.MappedSuperclass
import javax.persistence.Version
data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now())
@MappedSuperclass
abstract class BaseModel : Model() {
@Id
@GeneratedValue
var sysPk: Long = 0
@SoftDelete
var deleted: Boolean = false
@Version
var version: Int = 0
@WhenCreated
var createdAt: LocalDateTime = LocalDateTime.now()
@WhenModified
var modifiedAt: LocalDateTime? = null
@TenantId
var tenantId: String = ""
@WhoCreated
var createdBy: String = ""
@WhoModified
var modifiedBy: String? = null
var deletedOn: LocalDateTime? = null
var deletedBy: String? = null
@DbArray
var tags: MutableList<String> = arrayListOf()
@DbJsonB
var comments: MutableList<Comments> = arrayListOf()
}
@Entity
@Index(unique = true, name = "entity_unique_id", columnNames = ["entity_name", "unique_identifier", "tenant_id"])
open class DataModel : BaseModel() {
@JsonDeserialize(using = SafeStringDeserializer::class)
var uniqueIdentifier: String = ""
@JsonDeserialize(using = SafeStringDeserializer::class)
var entityName: String = ""
@Index(definition = "create index data_jsonb_idx on data_model using GIN (data) ", platforms = [Platform.POSTGRES])
@DbJsonB
var data: MutableMap<String, Any> = hashMapOf()
}
class SafeStringDeserializer : JsonDeserializer<String>() {
private val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$")
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): String {
val text = p.text
if (!regex.matches(text)) throw IllegalArgumentException()
return text
}
}