diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 9e1f404b7..2140d58b6 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -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() diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 6f436a023..1ffe22eef 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -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") + ) + )) ) ) ), diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index fed14e065..97ac5265d 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -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) ) } )