added _links for dynamic entity CRUD endpoints. + adding GET system

database pool endpoint
This commit is contained in:
simonredfern 2026-01-22 18:37:42 +01:00
parent b4856ef2ac
commit 3d4660ec0b
3 changed files with 145 additions and 5 deletions

View File

@ -406,6 +406,9 @@ object ApiRole extends MdcLoggable{
case class CanGetCacheInfo(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetCacheInfo = CanGetCacheInfo()
case class CanGetDatabasePoolInfo(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetDatabasePoolInfo = CanGetDatabasePoolInfo()
case class CanGetCacheNamespaces(requiresBankId: Boolean = false) extends ApiRole
lazy val canGetCacheNamespaces = CanGetCacheNamespaces()

View File

@ -30,7 +30,7 @@ import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
import code.api.v6_0_0.JSONFactory600.{AddUserToGroupResponseJsonV600, DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupEntitlementJsonV600, GroupEntitlementsJsonV600, GroupJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, UserGroupMembershipJsonV600, UserGroupMembershipsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveRateLimitsJsonV600, createCallLimitJsonV600, createRedisCallCountersJson}
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CreateAbacRuleJsonV600, CreateDynamicEntityRequestJsonV600, CurrentConsumerJsonV600, DynamicEntityDefinitionJsonV600, DynamicEntityDefinitionWithCountJsonV600, DynamicEntitiesWithCountJsonV600, ExecuteAbacRuleJsonV600, InMemoryCacheStatusJsonV600, MyDynamicEntitiesJsonV600, RedisCacheStatusJsonV600, UpdateAbacRuleJsonV600, UpdateDynamicEntityRequestJsonV600}
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CreateAbacRuleJsonV600, CreateDynamicEntityRequestJsonV600, CurrentConsumerJsonV600, DynamicEntityDefinitionJsonV600, DynamicEntityDefinitionWithCountJsonV600, DynamicEntitiesWithCountJsonV600, DynamicEntityLinksJsonV600, ExecuteAbacRuleJsonV600, InMemoryCacheStatusJsonV600, MyDynamicEntitiesJsonV600, RedisCacheStatusJsonV600, RelatedLinkJsonV600, UpdateAbacRuleJsonV600, UpdateDynamicEntityRequestJsonV600}
import code.api.v6_0_0.OBPAPI6_0_0
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
import code.metrics.APIMetrics
@ -795,6 +795,62 @@ trait APIMethods600 {
}
}
staticResourceDocs += ResourceDoc(
getDatabasePoolInfo,
implementedInApiVersion,
nameOf(getDatabasePoolInfo),
"GET",
"/system/database/pool",
"Get Database Pool Information",
"""Returns HikariCP connection pool information including:
|
|- Pool name
|- Active connections: currently in use
|- Idle connections: available in pool
|- Total connections: active + idle
|- Threads awaiting connection: requests waiting for a connection
|- Configuration: max pool size, min idle, timeouts
|
|This helps diagnose connection pool issues such as connection leaks or pool exhaustion.
|
|Authentication is Required
|""",
EmptyBody,
DatabasePoolInfoJsonV600(
pool_name = "HikariPool-1",
active_connections = 5,
idle_connections = 3,
total_connections = 8,
threads_awaiting_connection = 0,
maximum_pool_size = 10,
minimum_idle = 2,
connection_timeout_ms = 30000,
idle_timeout_ms = 600000,
max_lifetime_ms = 1800000,
keepalive_time_ms = 0
),
List(
AuthenticatedUserIsRequired,
UserHasMissingRoles,
UnknownError
),
List(apiTagSystem, apiTagApi),
Some(List(canGetDatabasePoolInfo))
)
lazy val getDatabasePoolInfo: OBPEndpoint = {
case "system" :: "database" :: "pool" :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(u), callContext) <- authenticatedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", u.userId, canGetDatabasePoolInfo, callContext)
} yield {
val result = JSONFactory600.createDatabasePoolInfoJsonV600()
(result, HttpCode.`200`(callContext))
}
}
}
lazy val getCurrentConsumer: OBPEndpoint = {
case "consumers" :: "current" :: Nil JsonGet _ => {
cc => {
@ -6924,7 +6980,16 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
schema = net.liftweb.json.parse("""{"description": "User preferences", "required": ["theme"], "properties": {"theme": {"type": "string"}, "language": {"type": "string"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject]
schema = net.liftweb.json.parse("""{"description": "User preferences", "required": ["theme"], "properties": {"theme": {"type": "string"}, "language": {"type": "string"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject],
_links = Some(DynamicEntityLinksJsonV600(
related = List(
RelatedLinkJsonV600("list", "/obp/v6.0.0/my/customer_preferences", "GET"),
RelatedLinkJsonV600("create", "/obp/v6.0.0/my/customer_preferences", "POST"),
RelatedLinkJsonV600("read", "/obp/v6.0.0/my/customer_preferences/CUSTOMER_PREFERENCES_ID", "GET"),
RelatedLinkJsonV600("update", "/obp/v6.0.0/my/customer_preferences/CUSTOMER_PREFERENCES_ID", "PUT"),
RelatedLinkJsonV600("delete", "/obp/v6.0.0/my/customer_preferences/CUSTOMER_PREFERENCES_ID", "DELETE")
)
))
)
)
),
@ -6979,7 +7044,16 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
schema = net.liftweb.json.parse("""{"description": "User preferences", "required": ["theme"], "properties": {"theme": {"type": "string"}, "language": {"type": "string"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject]
schema = net.liftweb.json.parse("""{"description": "User preferences", "required": ["theme"], "properties": {"theme": {"type": "string"}, "language": {"type": "string"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject],
_links = Some(DynamicEntityLinksJsonV600(
related = List(
RelatedLinkJsonV600("list", "/obp/v6.0.0/my/customer_preferences", "GET"),
RelatedLinkJsonV600("create", "/obp/v6.0.0/my/customer_preferences", "POST"),
RelatedLinkJsonV600("read", "/obp/v6.0.0/my/customer_preferences/CUSTOMER_PREFERENCES_ID", "GET"),
RelatedLinkJsonV600("update", "/obp/v6.0.0/my/customer_preferences/CUSTOMER_PREFERENCES_ID", "PUT"),
RelatedLinkJsonV600("delete", "/obp/v6.0.0/my/customer_preferences/CUSTOMER_PREFERENCES_ID", "DELETE")
)
))
)
)
),

View File

@ -305,6 +305,20 @@ case class CacheInfoJsonV600(
redis_available: Boolean
)
case class DatabasePoolInfoJsonV600(
pool_name: String,
active_connections: Int,
idle_connections: Int,
total_connections: Int,
threads_awaiting_connection: Int,
maximum_pool_size: Int,
minimum_idle: Int,
connection_timeout_ms: Long,
idle_timeout_ms: Long,
max_lifetime_ms: Long,
keepalive_time_ms: Long
)
case class PostCustomerJsonV600(
legal_name: String,
customer_number: Option[String] = None,
@ -486,6 +500,12 @@ case class AbacPoliciesJsonV600(
policies: List[AbacPolicyJsonV600]
)
// HATEOAS-style links for dynamic entity discoverability
case class RelatedLinkJsonV600(rel: String, href: String, method: String)
case class DynamicEntityLinksJsonV600(
related: List[RelatedLinkJsonV600]
)
// Dynamic Entity definition with fully predictable structure (v6.0.0 format)
// No dynamic keys - entity name is an explicit field, schema describes the structure
case class DynamicEntityDefinitionJsonV600(
@ -494,7 +514,8 @@ case class DynamicEntityDefinitionJsonV600(
user_id: String,
bank_id: Option[String],
has_personal_entity: Boolean,
schema: net.liftweb.json.JsonAST.JObject
schema: net.liftweb.json.JsonAST.JObject,
_links: Option[DynamicEntityLinksJsonV600] = None
)
case class MyDynamicEntitiesJsonV600(
@ -1339,6 +1360,28 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
)
}
def createDatabasePoolInfoJsonV600(): DatabasePoolInfoJsonV600 = {
import code.api.util.APIUtil
val ds = APIUtil.vendor.HikariDatasource.ds
val config = APIUtil.vendor.HikariDatasource.config
val pool = ds.getHikariPoolMXBean
DatabasePoolInfoJsonV600(
pool_name = ds.getPoolName,
active_connections = if (pool != null) pool.getActiveConnections else -1,
idle_connections = if (pool != null) pool.getIdleConnections else -1,
total_connections = if (pool != null) pool.getTotalConnections else -1,
threads_awaiting_connection = if (pool != null) pool.getThreadsAwaitingConnection else -1,
maximum_pool_size = config.getMaximumPoolSize,
minimum_idle = config.getMinimumIdle,
connection_timeout_ms = config.getConnectionTimeout,
idle_timeout_ms = config.getIdleTimeout,
max_lifetime_ms = config.getMaxLifetime,
keepalive_time_ms = config.getKeepaliveTime
)
}
/**
* Create v6.0.0 response for GET /my/dynamic-entities
*
@ -1362,6 +1405,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
def createMyDynamicEntitiesJson(dynamicEntities: List[code.dynamicEntity.DynamicEntityCommons]): MyDynamicEntitiesJsonV600 = {
import net.liftweb.json.JsonAST._
import net.liftweb.json.parse
import net.liftweb.util.StringHelpers
MyDynamicEntitiesJsonV600(
dynamic_entities = dynamicEntities.map { entity =>
@ -1382,13 +1426,32 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
throw new IllegalStateException(s"Could not extract schema for entity '${entity.entityName}' from metadataJson")
)
// Build HATEOAS-style links for this dynamic entity
val entityName = entity.entityName
val idPlaceholder = StringHelpers.snakify(entityName + "Id").toUpperCase()
val baseUrl = entity.bankId match {
case Some(bankId) => s"/obp/v6.0.0/banks/$bankId/my/$entityName"
case None => s"/obp/v6.0.0/my/$entityName"
}
val links = DynamicEntityLinksJsonV600(
related = List(
RelatedLinkJsonV600("list", baseUrl, "GET"),
RelatedLinkJsonV600("create", baseUrl, "POST"),
RelatedLinkJsonV600("read", s"$baseUrl/$idPlaceholder", "GET"),
RelatedLinkJsonV600("update", s"$baseUrl/$idPlaceholder", "PUT"),
RelatedLinkJsonV600("delete", s"$baseUrl/$idPlaceholder", "DELETE")
)
)
DynamicEntityDefinitionJsonV600(
dynamic_entity_id = entity.dynamicEntityId.getOrElse(""),
entity_name = entity.entityName,
user_id = entity.userId,
bank_id = entity.bankId,
has_personal_entity = entity.hasPersonalEntity,
schema = schemaObj
schema = schemaObj,
_links = Some(links)
)
}
)