more optional validations

This commit is contained in:
gowthaman.b 2023-11-11 13:10:25 +05:30
parent ea14212337
commit 31388bae59
9 changed files with 441 additions and 68 deletions

View File

@ -1,22 +1,55 @@
package com.restapi
import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.domain.EntityModel
import io.javalin.http.Context
import io.javalin.http.Handler
import io.javalin.http.HttpStatus
import io.javalin.security.AccessManager
import io.javalin.security.RouteRole
import org.slf4j.LoggerFactory
import com.restapi.domain.Session.currentRoles
import com.restapi.domain.Session.database
class AppAccessManager : AccessManager {
private val logger = LoggerFactory.getLogger("Access")
private fun loadEntityActionRole(entity: String?, action: String?): List<String> {
if (entity == null || action == null) return emptyList()
return database.find(EntityModel::class.java)
.where()
.eq("name", entity)
.findOne()?.actions
?.filter { it.equals(action, ignoreCase = true) }
?.map { "role_${entity}_$it" } ?: emptyList()
}
override fun manage(handler: Handler, ctx: Context, routeRoles: Set<RouteRole>) {
logger.warn("access {}, {}", ctx.pathParamMap(), routeRoles)
val pathParamMap = ctx.pathParamMap()
logger.warn("access {}, {}", pathParamMap, routeRoles)
val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$")
if(ctx.pathParamMap().values.count { !regex.matches(it) } > 0){
ctx.status(HttpStatus.FORBIDDEN).result("invalid request")
if (pathParamMap.values.count { !regex.matches(it) } > 0) {
ctx.status(HttpStatus.FORBIDDEN).result("invalid request")
} else {
handler.handle(ctx)
val entity = pathParamMap["entity"]
val action = pathParamMap["action"]
val allowedRoles = routeRoles.map { it as Role }.flatMap {
when (it) {
Role.DbOps -> listOf("ROLE_DB_OPS")
Role.Entity -> loadEntityActionRole(entity, action)
is Role.Standard -> listOf("ROLE_${entity}_${it.action}")
}.map(String::uppercase)
}
val isAllowed = currentRoles().count { allowedRoles.contains(it) } > 0
if (isAllowed || !appConfig.enforceRoleRestriction() || allowedRoles.isEmpty()) {
//if role is allowed, or enforcement is turned off or no roles are explicitly allowed
handler.handle(ctx)
} else {
ctx.status(HttpStatus.UNAUTHORIZED).result("unauthorized request")
}
}
}
}

View File

@ -8,11 +8,9 @@ import com.restapi.config.Auth.getAuthEndpoint
import com.restapi.config.Auth.parseAuthToken
import com.restapi.controllers.Entities
import com.restapi.domain.DataNotFoundException
import com.restapi.domain.Session.database
import com.restapi.domain.Session.objectMapper
import com.restapi.domain.Session.redis
import com.restapi.domain.Session.setAuthorizedUser
import io.ebean.CallableSql
import io.ebean.DuplicateKeyException
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.*
@ -20,7 +18,6 @@ import io.javalin.http.*
import io.javalin.http.util.NaiveRateLimit
import io.javalin.http.util.RateLimitUtil
import io.javalin.json.JavalinJackson
import io.javalin.security.AccessManager
import io.javalin.security.RouteRole
import org.slf4j.LoggerFactory
import java.net.URI
@ -99,11 +96,15 @@ fun main(args: Array<String>) {
}
before("/api/*") { ctx ->
//validate, auth token
NaiveRateLimit.requestPerTimeUnit(ctx, appConfig.rateLimit().getOrDefault(30), TimeUnit.MINUTES) // throws if rate limit is exceeded
NaiveRateLimit.requestPerTimeUnit(
ctx,
appConfig.rateLimit().getOrDefault(30),
TimeUnit.MINUTES
)
val authToken = ctx.header("Authorization")?.replace("Bearer ", "")
val authToken = ctx.header("Authorization")
?.replace("Bearer ", "")
?.replace("Bearer: ", "")
?.trim() ?: throw UnauthorizedResponse()
@ -111,21 +112,29 @@ fun main(args: Array<String>) {
setAuthorizedUser(parseAuthToken(authToken = authToken))
}
val adminRole = Role.Standard(Action.ADMIN)
val viewRole = Role.Standard(Action.VIEW)
val createRole = Role.Standard(Action.CREATE)
val updateRole = Role.Standard(Action.UPDATE)
val approveOrRejectRole = Role.Standard(Action.APPROVE)
path("/api") {
post("/execute/{name}", Entities::executeStoredProcedure, Roles(Role.DbOps))
post("/execute/{name}", Entities::executeStoredProcedure, Roles(adminRole, Role.DbOps))
post("/script/{name}", Entities::executeScript, Roles(adminRole, Role.DbOps))
get("/{entity}/{id}", Entities::view, Roles(Role.Standard(Action.VIEW)))
post("/{entity}/query/{id}", Entities::sqlQueryId, Roles(Role.Standard(Action.VIEW)))
post("/{entity}/query", Entities::sqlQueryRaw, Roles(Role.Standard(Action.VIEW)))
post("/{entity}", Entities::create, Roles(Role.Standard(Action.CREATE)))
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}", Entities::create, Roles(adminRole, createRole))
put("/{entity}/approve/{id}", Entities::approve, Roles(Role.Standard(Action.APPROVE)))
put("/{entity}/reject/{id}", Entities::reject, Roles(Role.Standard(Action.APPROVE)))
put("/{entity}/{action}/{id}", Entities::action, Roles(Role.Entity))
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(Role.Standard(Action.UPDATE)))
patch("/{entity}/{id}", Entities::patch, Roles(Role.Standard(Action.UPDATE)))
delete("/{entity}/{id}", Entities::delete, Roles(Role.Standard(Action.DELETE)))
put("/{entity}/{id}", Entities::update, Roles(adminRole, updateRole))
patch("/{entity}/{id}", Entities::patch, Roles(adminRole, updateRole))
delete("/{entity}/{id}", Entities::delete, Roles(adminRole, Role.Standard(Action.DELETE)))
}
@ -163,7 +172,7 @@ fun main(args: Array<String>) {
enum class Action {
CREATE, VIEW, UPDATE, DELETE, APPROVE
CREATE, VIEW, UPDATE, DELETE, APPROVE, ADMIN
}
sealed class Role {

View File

@ -66,6 +66,13 @@ interface AppConfig {
@Key("app.network.rate_limit")
fun rateLimit(): Optional<Int>
@Key("app.security.enforce_role_restriction")
@Default("true")
fun enforceRoleRestriction(): Boolean
@Key("app.scripts.path")
fun scriptsPath(): String
companion object {
val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java)
}

View File

@ -1,16 +1,23 @@
package com.restapi.controllers
import com.restapi.domain.ApprovalStatus
import com.restapi.domain.DataModel
import com.restapi.domain.EntityModel
import com.restapi.domain.Session
import com.restapi.domain.Session.database
import com.restapi.domain.Session.findByEntityAndId
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.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
data class PatchValue(val key: String, val value: Any)
data class Query(
@ -46,6 +53,14 @@ object Entities {
fun approve(ctx: Context) {}
fun reject(ctx: Context) {}
fun executeScript(ctx: Context) {
val name = ctx.pathParam("name")
val params = ctx.bodyAsClass<Map<String, Any>>()
ctx.json(
Scripting.execute(name, params)
)
}
fun executeStoredProcedure(ctx: Context) {
val name = ctx.pathParam("name")
val params = ctx.bodyAsClass<Map<String, Any>>()
@ -58,7 +73,12 @@ object Entities {
cs.setParameter(index + 1, entry.value)
}
cs.setParameter(params.entries.size + 1, Session.currentTenant())
database.execute(cs)
val done = database.execute(cs)
ctx.json(
mapOf(
"done" to done
)
)
}
fun sqlQueryRaw(ctx: Context) {
@ -77,7 +97,7 @@ object Entities {
}
fun sqlQueryId(ctx: Context) {
fun sqlQueryById(ctx: Context) {
val sql = ctx.bodyAsClass<Query>()
val query = database.findByEntityAndId(ctx.pathParam("entity"), ctx.pathParam("id"))
@ -104,16 +124,112 @@ object Entities {
}
fun create(ctx: Context) {
val entity = ctx.pathParam("entity")
val seqCreated = Session.creatSeq(entity)
logger.debug("sequence created for $entity? = $seqCreated")
database.save(
ctx.bodyAsClass<DataModel>().apply {
this.entityName = entity
if (this.uniqueIdentifier.isEmpty()) {
this.uniqueIdentifier = Session.nextUniqId(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<DataModel>().apply {
this.entityName = entity
if (this.uniqueIdentifier.isEmpty()) {
this.uniqueIdentifier = Session.nextUniqId(entity)
}
)
}
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.approvalLevels > 0) {
this.approvalStatus = ApprovalStatus.PENDING
this.requiredApprovalLevels = setupEntity.approvalLevels
}
}
})
}
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
}
}

View File

@ -47,6 +47,7 @@ object Session {
fun currentUser() = currentUser.get().userName
fun currentTenant() = currentUser.get().tenant
fun currentRoles() = currentUser.get().roles
fun Database.findByEntityAndId(entity: String, id: String): DataModel {
return find(DataModel::class.java)

View File

@ -20,6 +20,10 @@ import javax.persistence.*
data class Comments(val text: String = "", val by: String = "", val at: LocalDateTime = LocalDateTime.now())
enum class ApprovalStatus {
PENDING, APPROVED, REJECTED
}
@MappedSuperclass
abstract class BaseModel : Model() {
@Id
@ -51,6 +55,13 @@ abstract class BaseModel : Model() {
var deletedBy: String? = null
var currentApprovalLevel: Int = 0
var requiredApprovalLevels: Int = 0
@Enumerated(EnumType.STRING)
var approvalStatus: ApprovalStatus = ApprovalStatus.PENDING
@DbArray
var tags: MutableList<String> = arrayListOf()
@ -70,11 +81,15 @@ open class TenantModel : BaseModel() {
}
enum class AuditType {
CREATE, UPDATE, DELETE, VIEW
CREATE, UPDATE, DELETE, VIEW, APPROVE, REJECT
}
@Entity
@Index(columnNames = ["audit_type", "entity", "unique_identifier", "tenant_id", "created_by"])
open class AuditLog : BaseModel() {
@Enumerated(EnumType.STRING)
var auditType: AuditType = AuditType.CREATE
var entity: String = ""
var uniqueIdentifier: String = ""
@ -108,6 +123,10 @@ open class EntityModel : BaseModel() {
@DbArray
var allowedFields: List<String> = emptyList()
//enforce field types, if this is present, only fields that are present is validated
@DbJsonB
var allowedFieldTypes: Map<String, String> = hashMapOf()
//when an entity is saved/updated audit logs will be populated, when this is empty, all fields are logged
@DbArray
var auditLogFields: List<String> = emptyList()
@ -116,7 +135,7 @@ open class EntityModel : BaseModel() {
var preferences: MutableMap<String, Any> = hashMapOf()
//if '0' then its auto saved, no approval steps are required, for further steps,
//a user needs to have ROLE_ENTITY_APPROVE_LEVEL1, ROLE_ENTITY_APPROVE_LEVEL2 roles
//a user needs to have ROLE_ENTITY_APPROVE_LEVEL1, ROLE_ENTITY_APPROVE_LEVEL2 roles for further approvals
var approvalLevels: Int = 0
}

View File

@ -1,43 +1,17 @@
package com.restapi.integ
import com.restapi.config.AppConfig.Companion.appConfig
import java.io.File
import javax.script.Invocable
import javax.script.ScriptEngineManager
object Scripting {
const val a = "1"
@JvmStatic
fun main(args: Array<String>) {
k()
}
fun k(){
fun execute(name: String, params: Map<String, Any>): Any {
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
val res1 = engine.eval("""
fun fn(x: Int) = x + 2
val obj = object {
fun fn1(x: Int) = x + 3
}
obj""".trimIndent())
println(res1)
val invocator = engine as? Invocable
println(invocator)
try {
println(invocator!!.invokeFunction("fn1", 3))
} catch (e: NoSuchMethodException) {
println(e)
}
val res2 = invocator!!.invokeFunction("fn", 3)
println(res2)
val res3 = invocator.invokeMethod(res1, "fn1", 3)
println(res3)
engine.eval(File(appConfig.scriptsPath(), "$name.kts").reader())
val invocator = engine as Invocable
return invocator.invokeFunction("execute", params)
}
}

View File

@ -1,13 +1,42 @@
-- apply changes
create table data_model (
create table audit_log (
sys_pk bigint generated by default as identity not null,
deleted_on timestamp,
current_approval_level integer not null,
required_approval_levels integer not null,
deleted boolean default false not null,
version integer not null,
created_at timestamp not null,
modified_at timestamp not null,
tenant_id varchar(255) not null,
deleted_by varchar(255),
approval_status varchar(8) not null,
tags varchar[] not null,
comments jsonb not null,
audit_type varchar(7) not null,
entity varchar(255) not null,
unique_identifier varchar(255) not null,
data jsonb not null,
changes jsonb not null,
created_by varchar(255) not null,
modified_by varchar(255) not null,
constraint ck_audit_log_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')),
constraint ck_audit_log_audit_type check ( audit_type in ('CREATE','UPDATE','DELETE','VIEW','APPROVE','REJECT')),
constraint pk_audit_log primary key (sys_pk)
);
create table data_model (
sys_pk bigint generated by default as identity not null,
deleted_on timestamp,
current_approval_level integer not null,
required_approval_levels integer not null,
deleted boolean default false not null,
version integer not null,
created_at timestamp not null,
modified_at timestamp not null,
tenant_id varchar(255) not null,
deleted_by varchar(255),
approval_status varchar(8) not null,
tags varchar[] not null,
comments jsonb not null,
unique_identifier varchar(255) not null,
@ -15,9 +44,95 @@ create table data_model (
data jsonb not null,
created_by varchar(255) not null,
modified_by varchar(255) not null,
constraint ck_data_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')),
constraint entity_unique_id unique (entity_name,unique_identifier,tenant_id),
constraint pk_data_model primary key (sys_pk)
);
create table entity_model (
sys_pk bigint generated by default as identity not null,
deleted_on timestamp,
current_approval_level integer not null,
required_approval_levels integer not null,
approval_levels integer not null,
deleted boolean default false not null,
version integer not null,
created_at timestamp not null,
modified_at timestamp not null,
tenant_id varchar(255) not null,
deleted_by varchar(255),
approval_status varchar(8) not null,
tags varchar[] not null,
comments jsonb not null,
name varchar(255) not null,
pre_save_script varchar(255) not null,
post_save_script varchar(255) not null,
actions varchar[] not null,
allowed_fields varchar[] not null,
allowed_field_types jsonb not null,
audit_log_fields varchar[] not null,
preferences jsonb not null,
created_by varchar(255) not null,
modified_by varchar(255) not null,
constraint ck_entity_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')),
constraint uq_entity_model_name unique (name),
constraint pk_entity_model primary key (sys_pk)
);
create table job_model (
sys_pk bigint generated by default as identity not null,
deleted_on timestamp,
current_approval_level integer not null,
required_approval_levels integer not null,
deleted boolean default false not null,
version integer not null,
created_at timestamp not null,
modified_at timestamp not null,
tenant_id varchar(255) not null,
deleted_by varchar(255),
approval_status varchar(8) not null,
tags varchar[] not null,
comments jsonb not null,
job_name varchar(255) not null,
job_type varchar(6) not null,
job_path varchar(255) not null,
tenants varchar[] not null,
job_frequency_type varchar(8) not null,
frequency varchar(255) not null,
created_by varchar(255) not null,
modified_by varchar(255) not null,
constraint ck_job_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')),
constraint ck_job_model_job_type check ( job_type in ('SCRIPT','DB')),
constraint ck_job_model_job_frequency_type check ( job_frequency_type in ('SPECIFIC','EVERY','CRON')),
constraint uq_job_model_job_name unique (job_name),
constraint pk_job_model primary key (sys_pk)
);
create table tenant_model (
sys_pk bigint generated by default as identity not null,
deleted_on timestamp,
current_approval_level integer not null,
required_approval_levels integer not null,
deleted boolean default false not null,
version integer not null,
created_at timestamp not null,
modified_at timestamp not null,
tenant_id varchar(255) not null,
deleted_by varchar(255),
approval_status varchar(8) not null,
tags varchar[] not null,
comments jsonb not null,
name varchar(255) not null,
domain varchar(255) not null,
preferences jsonb not null,
created_by varchar(255) not null,
modified_by varchar(255) not null,
constraint ck_tenant_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')),
constraint pk_tenant_model primary key (sys_pk)
);
-- foreign keys and indices
create index if not exists ix_audit_log_audit_type_entity_unique_identifier_tenant_i_1 on audit_log (audit_type,entity,unique_identifier,tenant_id,created_by);
create index audit_log_values_idx on audit_log using GIN (data) ;
create index audit_log_changes_idx on audit_log using GIN (changes) ;
create index data_jsonb_idx on data_model using GIN (data) ;

View File

@ -1,11 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
<changeSet type="apply">
<createTable name="audit_log" pkName="pk_audit_log">
<column name="sys_pk" type="bigint" primaryKey="true"/>
<column name="tenant_id" type="varchar" notnull="true"/>
<column name="deleted_on" type="localdatetime"/>
<column name="deleted_by" type="varchar"/>
<column name="current_approval_level" type="integer" notnull="true"/>
<column name="required_approval_levels" type="integer" notnull="true"/>
<column name="approval_status" type="varchar(8)" notnull="true" checkConstraint="check ( approval_status in ('PENDING','APPROVED','REJECTED'))" checkConstraintName="ck_audit_log_approval_status"/>
<column name="tags" type="varchar[]" notnull="true"/>
<column name="comments" type="jsonb" notnull="true"/>
<column name="audit_type" type="varchar(7)" notnull="true" checkConstraint="check ( audit_type in ('CREATE','UPDATE','DELETE','VIEW','APPROVE','REJECT'))" checkConstraintName="ck_audit_log_audit_type"/>
<column name="entity" type="varchar" notnull="true"/>
<column name="unique_identifier" type="varchar" notnull="true"/>
<column name="data" type="jsonb" notnull="true"/>
<column name="changes" type="jsonb" 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="varchar" notnull="true"/>
<column name="modified_by" type="varchar" notnull="true"/>
</createTable>
<createTable name="data_model" pkName="pk_data_model">
<column name="sys_pk" type="bigint" primaryKey="true"/>
<column name="tenant_id" type="varchar" notnull="true"/>
<column name="deleted_on" type="localdatetime"/>
<column name="deleted_by" type="varchar"/>
<column name="current_approval_level" type="integer" notnull="true"/>
<column name="required_approval_levels" type="integer" notnull="true"/>
<column name="approval_status" type="varchar(8)" notnull="true" checkConstraint="check ( approval_status in ('PENDING','APPROVED','REJECTED'))" checkConstraintName="ck_data_model_approval_status"/>
<column name="tags" type="varchar[]" notnull="true"/>
<column name="comments" type="jsonb" notnull="true"/>
<column name="unique_identifier" type="varchar" notnull="true"/>
@ -19,6 +44,80 @@
<column name="modified_by" type="varchar" notnull="true"/>
<uniqueConstraint name="entity_unique_id" columnNames="entity_name,unique_identifier,tenant_id" oneToOne="false" nullableColumns=""/>
</createTable>
<createTable name="entity_model" pkName="pk_entity_model">
<column name="sys_pk" type="bigint" primaryKey="true"/>
<column name="tenant_id" type="varchar" notnull="true"/>
<column name="deleted_on" type="localdatetime"/>
<column name="deleted_by" type="varchar"/>
<column name="current_approval_level" type="integer" notnull="true"/>
<column name="required_approval_levels" type="integer" notnull="true"/>
<column name="approval_status" type="varchar(8)" notnull="true" checkConstraint="check ( approval_status in ('PENDING','APPROVED','REJECTED'))" checkConstraintName="ck_entity_model_approval_status"/>
<column name="tags" type="varchar[]" notnull="true"/>
<column name="comments" type="jsonb" notnull="true"/>
<column name="name" type="varchar" notnull="true"/>
<column name="pre_save_script" type="varchar" notnull="true"/>
<column name="post_save_script" type="varchar" notnull="true"/>
<column name="actions" type="varchar[]" notnull="true"/>
<column name="allowed_fields" type="varchar[]" notnull="true"/>
<column name="allowed_field_types" type="jsonb" notnull="true"/>
<column name="audit_log_fields" type="varchar[]" notnull="true"/>
<column name="preferences" type="jsonb" notnull="true"/>
<column name="approval_levels" type="integer" 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="varchar" notnull="true"/>
<column name="modified_by" type="varchar" notnull="true"/>
<uniqueConstraint name="uq_entity_model_name" columnNames="name" oneToOne="false" nullableColumns=""/>
</createTable>
<createTable name="job_model" pkName="pk_job_model">
<column name="sys_pk" type="bigint" primaryKey="true"/>
<column name="tenant_id" type="varchar" notnull="true"/>
<column name="deleted_on" type="localdatetime"/>
<column name="deleted_by" type="varchar"/>
<column name="current_approval_level" type="integer" notnull="true"/>
<column name="required_approval_levels" type="integer" notnull="true"/>
<column name="approval_status" type="varchar(8)" notnull="true" checkConstraint="check ( approval_status in ('PENDING','APPROVED','REJECTED'))" checkConstraintName="ck_job_model_approval_status"/>
<column name="tags" type="varchar[]" notnull="true"/>
<column name="comments" type="jsonb" notnull="true"/>
<column name="job_name" type="varchar" notnull="true"/>
<column name="job_type" type="varchar(6)" notnull="true" checkConstraint="check ( job_type in ('SCRIPT','DB'))" checkConstraintName="ck_job_model_job_type"/>
<column name="job_path" type="varchar" notnull="true"/>
<column name="tenants" type="varchar[]" notnull="true"/>
<column name="job_frequency_type" type="varchar(8)" notnull="true" checkConstraint="check ( job_frequency_type in ('SPECIFIC','EVERY','CRON'))" checkConstraintName="ck_job_model_job_frequency_type"/>
<column name="frequency" 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="varchar" notnull="true"/>
<column name="modified_by" type="varchar" notnull="true"/>
<uniqueConstraint name="uq_job_model_job_name" columnNames="job_name" oneToOne="false" nullableColumns=""/>
</createTable>
<createTable name="tenant_model" pkName="pk_tenant_model">
<column name="sys_pk" type="bigint" primaryKey="true"/>
<column name="tenant_id" type="varchar" notnull="true"/>
<column name="deleted_on" type="localdatetime"/>
<column name="deleted_by" type="varchar"/>
<column name="current_approval_level" type="integer" notnull="true"/>
<column name="required_approval_levels" type="integer" notnull="true"/>
<column name="approval_status" type="varchar(8)" notnull="true" checkConstraint="check ( approval_status in ('PENDING','APPROVED','REJECTED'))" checkConstraintName="ck_tenant_model_approval_status"/>
<column name="tags" type="varchar[]" notnull="true"/>
<column name="comments" type="jsonb" notnull="true"/>
<column name="name" type="varchar" notnull="true"/>
<column name="domain" type="varchar" notnull="true"/>
<column name="preferences" type="jsonb" 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="varchar" notnull="true"/>
<column name="modified_by" type="varchar" notnull="true"/>
</createTable>
<createIndex indexName="ix_audit_log_audit_type_entity_unique_identifier_tenant_i_1" tableName="audit_log" columns="audit_type,entity,unique_identifier,tenant_id,created_by"/>
<createIndex indexName="ix_audit_log_data" tableName="audit_log" columns="data" definition="create index audit_log_values_idx on audit_log using GIN (data) " platforms="POSTGRES"/>
<createIndex indexName="ix_audit_log_changes" tableName="audit_log" columns="changes" definition="create index audit_log_changes_idx on audit_log using GIN (changes) " platforms="POSTGRES"/>
<createIndex indexName="ix_data_model_data" tableName="data_model" columns="data" definition="create index data_jsonb_idx on data_model using GIN (data) " platforms="POSTGRES"/>
</changeSet>
</migration>