simple api model
This commit is contained in:
parent
203c2b97a7
commit
01e197b0f8
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -9,7 +9,7 @@
|
|||||||
<list />
|
<list />
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="semeru-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
21
api.http
21
api.http
@ -9,22 +9,33 @@ Content-Type: application/json
|
|||||||
"uniqueIdentifier": "KA03HD6064"
|
"uniqueIdentifier": "KA03HD6064"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
### create row, with autogenerated identifier
|
||||||
|
POST http://localhost:9001/api/log
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"user": "gowthaman",
|
||||||
|
"action": "logout"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
### get row
|
### get row
|
||||||
GET http://localhost:9001/api/vehicle/1
|
GET http://localhost:9001/api/vehicle/KA03HD6064
|
||||||
|
|
||||||
### query row
|
### query row
|
||||||
POST http://localhost:9001/api/vehicle/query
|
POST http://localhost:9001/api/vehicle/query
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"sql": "select id, tenant_id, deleted_on, deleted_by, deleted, version, created_at, modified_at, created_by, modified_by, data, tags, comments, unique_identifier, entity_name from data_model where data ->> 'number' = :number",
|
"sql": "select sys_pk, tenant_id, deleted_on, deleted_by, deleted, version, created_at, modified_at, created_by, modified_by, data, tags, comments, unique_identifier, entity_name from data_model where data ->> 'number' = :number",
|
||||||
"params": {
|
"params": {
|
||||||
"number": "KA03HD6064"
|
"number": "KA03HD6064"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
### update field
|
### update field
|
||||||
PATCH http://localhost:9001/api/vehicle/1
|
PATCH http://localhost:9001/api/vehicle/KA03HD6064
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -34,7 +45,7 @@ Content-Type: application/json
|
|||||||
|
|
||||||
|
|
||||||
### upate a row
|
### upate a row
|
||||||
PUT http://localhost:9001/api/vehicle/1
|
PUT http://localhost:9001/api/vehicle/KA03HD6064
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -44,4 +55,4 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
|
|
||||||
### delete a row
|
### delete a row
|
||||||
DELETE http://localhost:9001/api/vehicle/1
|
DELETE http://localhost:9001/api/vehicle/KA03HD6064
|
||||||
7
app-sample.properties
Normal file
7
app-sample.properties
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
app.port=9001
|
||||||
|
app.cors.enabled=true
|
||||||
|
app.cors.hosts=www.readymixerp.com,app.readymixerp.com
|
||||||
|
app.db.user=postgres
|
||||||
|
app.db.pass=postgres
|
||||||
|
app.db.url=jdbc:postgresql://192.168.64.6/modules_app
|
||||||
|
app.db.run_migration=true
|
||||||
@ -27,7 +27,8 @@ dependencies {
|
|||||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.+")
|
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.+")
|
||||||
|
|
||||||
implementation("org.slf4j:slf4j-simple:2.0.7")
|
implementation("org.slf4j:slf4j-simple:2.0.7")
|
||||||
|
api ("net.cactusthorn.config:config-core:0.81")
|
||||||
|
kapt("net.cactusthorn.config:config-compiler:0.81")
|
||||||
kapt("io.ebean:kotlin-querybean-generator:13.23.2")
|
kapt("io.ebean:kotlin-querybean-generator:13.23.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,5 +41,5 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("com.readymixerp.MainKt")
|
mainClass.set("com.restapi.MainKt")
|
||||||
}
|
}
|
||||||
@ -1,116 +0,0 @@
|
|||||||
package com.readymixerp
|
|
||||||
|
|
||||||
import com.readymixerp.domain.DataModel
|
|
||||||
import com.readymixerp.domain.Session
|
|
||||||
import com.readymixerp.domain.Session.database
|
|
||||||
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 java.time.LocalDateTime
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Javalin
|
|
||||||
.create { cfg ->
|
|
||||||
cfg.http.generateEtags = true
|
|
||||||
cfg.plugins.enableCors { container ->
|
|
||||||
container.add {
|
|
||||||
it.allowHost(
|
|
||||||
"http://localhost:5173",
|
|
||||||
"https://www.readymixerp.com",
|
|
||||||
"https://app.readymixerp.com"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg.http.defaultContentType = ContentType.JSON
|
|
||||||
cfg.compression.gzipOnly()
|
|
||||||
cfg.jsonMapper(JavalinJackson(Session.objectMapper))
|
|
||||||
}
|
|
||||||
.routes {
|
|
||||||
before("/*") {
|
|
||||||
//validate, auth token
|
|
||||||
}
|
|
||||||
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.find(DataModel::class.java, it.pathParam("id")) ?: throw NotFoundResponse()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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}") {
|
|
||||||
database.save(
|
|
||||||
it.bodyAsClass<DataModel>().apply {
|
|
||||||
this.entityName = it.pathParam("entity")
|
|
||||||
if(this.uniqueIdentifier.isEmpty()) {
|
|
||||||
//todo: set a counter
|
|
||||||
}
|
|
||||||
this.uniqueIdentifier = "${this.entityName}_${this.uniqueIdentifier}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
put("/{entity}/{id}") {
|
|
||||||
val e = database.find(DataModel::class.java, it.pathParam("id")) ?: throw NotFoundResponse()
|
|
||||||
val newData = it.bodyAsClass<Map<String,Any>>()
|
|
||||||
e.data.putAll(newData)
|
|
||||||
e.update()
|
|
||||||
}
|
|
||||||
patch("/{entity}/{id}") {
|
|
||||||
val e = database.find(DataModel::class.java, it.pathParam("id")) ?: throw NotFoundResponse()
|
|
||||||
val pv = it.bodyAsClass<PatchValue>()
|
|
||||||
e.data[pv.key] = pv.value;
|
|
||||||
e.update()
|
|
||||||
}
|
|
||||||
delete("/{entity}/{id}") {
|
|
||||||
val id = it.pathParam("id")
|
|
||||||
val e = database.find(DataModel::class.java, id) ?: throw NotFoundResponse()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
.start(9001)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Query(
|
|
||||||
val sql: String,
|
|
||||||
val params: Map<String, Any>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PatchValue(val key: String, val value: Any)
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
package com.readymixerp.domain
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
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
|
|
||||||
|
|
||||||
data class CurrentUser(
|
|
||||||
val anon: Boolean = true,
|
|
||||||
val userId:Long = 0,
|
|
||||||
val tenantId: Long = 0
|
|
||||||
)
|
|
||||||
object Session {
|
|
||||||
private val currentUser = object: ThreadLocal<CurrentUser>() {
|
|
||||||
override fun initialValue(): CurrentUser {
|
|
||||||
return CurrentUser()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val sc = DatabaseConfig().apply {
|
|
||||||
loadFromProperties()
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
|
||||||
177
src/main/kotlin/com/restapi/Main.kt
Normal file
177
src/main/kotlin/com/restapi/Main.kt
Normal 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)
|
||||||
48
src/main/kotlin/com/restapi/config/AppConfig.kt
Normal file
48
src/main/kotlin/com/restapi/config/AppConfig.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/main/kotlin/com/restapi/domain/db.kt
Normal file
75
src/main/kotlin/com/restapi/domain/db.kt
Normal 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
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
package com.readymixerp.domain
|
package com.restapi.domain
|
||||||
|
|
||||||
import io.ebean.annotation.Platform
|
import io.ebean.annotation.Platform
|
||||||
import io.ebean.dbmigration.DbMigration
|
import io.ebean.dbmigration.DbMigration
|
||||||
|
|
||||||
|
|
||||||
object DBMigration {
|
object DBMigration {
|
||||||
fun create(){
|
private fun create(){
|
||||||
val dbMigration: DbMigration = DbMigration.create()
|
val dbMigration: DbMigration = DbMigration.create()
|
||||||
dbMigration.setPlatform(Platform.POSTGRES)
|
dbMigration.setPlatform(Platform.POSTGRES)
|
||||||
|
|
||||||
@ -1,8 +1,14 @@
|
|||||||
package com.readymixerp.domain
|
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.Model
|
||||||
import io.ebean.annotation.DbArray
|
import io.ebean.annotation.DbArray
|
||||||
import io.ebean.annotation.DbJsonB
|
import io.ebean.annotation.DbJsonB
|
||||||
|
import io.ebean.annotation.Index
|
||||||
|
import io.ebean.annotation.Platform
|
||||||
import io.ebean.annotation.SoftDelete
|
import io.ebean.annotation.SoftDelete
|
||||||
import io.ebean.annotation.TenantId
|
import io.ebean.annotation.TenantId
|
||||||
import io.ebean.annotation.WhenCreated
|
import io.ebean.annotation.WhenCreated
|
||||||
@ -22,7 +28,7 @@ data class Comments(val text: String = "", val by: String = "", val at: LocalDat
|
|||||||
abstract class BaseModel : Model() {
|
abstract class BaseModel : Model() {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
var id: Long = 0
|
var sysPk: Long = 0
|
||||||
|
|
||||||
@SoftDelete
|
@SoftDelete
|
||||||
var deleted: Boolean = false
|
var deleted: Boolean = false
|
||||||
@ -37,20 +43,17 @@ abstract class BaseModel : Model() {
|
|||||||
var modifiedAt: LocalDateTime? = null
|
var modifiedAt: LocalDateTime? = null
|
||||||
|
|
||||||
@TenantId
|
@TenantId
|
||||||
var tenantId: Long = 0L
|
var tenantId: String = ""
|
||||||
|
|
||||||
@WhoCreated
|
@WhoCreated
|
||||||
var createdBy: Long = 0L
|
var createdBy: String = ""
|
||||||
|
|
||||||
@WhoModified
|
@WhoModified
|
||||||
var modifiedBy: Long? = null
|
var modifiedBy: String? = null
|
||||||
|
|
||||||
var deletedOn: LocalDateTime? = null
|
var deletedOn: LocalDateTime? = null
|
||||||
|
|
||||||
var deletedBy: Long? = null
|
var deletedBy: String? = null
|
||||||
|
|
||||||
@DbJsonB
|
|
||||||
var data: MutableMap<String, Any> = hashMapOf()
|
|
||||||
|
|
||||||
@DbArray
|
@DbArray
|
||||||
var tags: MutableList<String> = arrayListOf()
|
var tags: MutableList<String> = arrayListOf()
|
||||||
@ -60,10 +63,29 @@ abstract class BaseModel : Model() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Index(unique = true, name = "entity_unique_id", columnNames = ["entity_name", "unique_identifier", "tenant_id"])
|
||||||
open class DataModel : BaseModel() {
|
open class DataModel : BaseModel() {
|
||||||
|
|
||||||
|
@JsonDeserialize(using = SafeStringDeserializer::class)
|
||||||
var uniqueIdentifier: String = ""
|
var uniqueIdentifier: String = ""
|
||||||
|
|
||||||
|
@JsonDeserialize(using = SafeStringDeserializer::class)
|
||||||
var entityName: String = ""
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,32 +0,0 @@
|
|||||||
-- apply changes
|
|
||||||
create table data_model
|
|
||||||
(
|
|
||||||
id bigint generated by default as identity not null,
|
|
||||||
tenant_id bigint not null,
|
|
||||||
deleted_on timestamp,
|
|
||||||
deleted_by bigint,
|
|
||||||
deleted boolean default false not null,
|
|
||||||
version integer not null,
|
|
||||||
created_at timestamp not null,
|
|
||||||
modified_at timestamp not null,
|
|
||||||
created_by bigint not null,
|
|
||||||
modified_by bigint not null,
|
|
||||||
data jsonb not null,
|
|
||||||
tags varchar[] not null,
|
|
||||||
comments jsonb not null,
|
|
||||||
unique_identifier varchar(255) not null,
|
|
||||||
entity_name varchar(255) not null,
|
|
||||||
constraint pk_data_model primary key (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
create unique index data_model_uniq_identifier on data_model (unique_identifier);
|
|
||||||
create index data_json_jdx on data_model using GIN (data);
|
|
||||||
create index data_json_meta_data on data_model (tenant_id,
|
|
||||||
deleted,
|
|
||||||
version,
|
|
||||||
created_at,
|
|
||||||
modified_at,
|
|
||||||
created_by,
|
|
||||||
modified_by,
|
|
||||||
entity_name
|
|
||||||
);
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
|
|
||||||
<changeSet type="apply">
|
|
||||||
<createTable name="data_model" pkName="pk_data_model">
|
|
||||||
<column name="id" type="bigint" primaryKey="true"/>
|
|
||||||
<column name="tenant_id" type="bigint" notnull="true"/>
|
|
||||||
<column name="deleted_on" type="localdatetime"/>
|
|
||||||
<column name="deleted_by" type="bigint"/>
|
|
||||||
<column name="data" type="jsonb" notnull="true"/>
|
|
||||||
<column name="tags" type="varchar[]" notnull="true"/>
|
|
||||||
<column name="comments" type="jsonb" notnull="true"/>
|
|
||||||
<column name="unique_identifier" type="varchar" notnull="true"/>
|
|
||||||
<column name="entity_name" type="varchar" notnull="true"/>
|
|
||||||
<column name="deleted" type="boolean" defaultValue="false" notnull="true"/>
|
|
||||||
<column name="version" type="integer" notnull="true"/>
|
|
||||||
<column name="created_at" type="localdatetime" notnull="true"/>
|
|
||||||
<column name="modified_at" type="localdatetime" notnull="true"/>
|
|
||||||
<column name="created_by" type="bigint" notnull="true"/>
|
|
||||||
<column name="modified_by" type="bigint" notnull="true"/>
|
|
||||||
</createTable>
|
|
||||||
</changeSet>
|
|
||||||
</migration>
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
entity-packages: com.readymixerp.domain
|
entity-packages: com.restapi.domain
|
||||||
querybean-packages: com.readymixerp.domain
|
querybean-packages: com.restapi.domain
|
||||||
transactional-packages: com.readymixerp
|
transactional-packages: com.restapi
|
||||||
profile-location: true
|
profile-location: true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user