Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ case class DynamicEntityInfo(definition: String, entityName: String, bankId: Opt

val subEntities: List[DynamicEntityInfo] = Nil

val idName = StringUtils.uncapitalize(entityName) + "Id"
val idName = StringHelpers.snakify(entityName) + "_id"

val listName = StringHelpers.snakify(entityName).replaceFirst("[-_]*$", "_list")

Expand Down Expand Up @@ -581,11 +581,16 @@ case class DynamicEntityInfo(definition: String, entityName: String, bankId: Opt
(singleName -> (JObject(JField(idName, JString(ExampleValue.idExample.value)) :: getSingleExampleWithoutId.obj)))
}

def getExampleList: JObject = if (bankId.isDefined){
val objectList: JObject = (listName -> JArray(List(getSingleExample)))
bankIdJObject merge objectList
} else{
(listName -> JArray(List(getSingleExample)))
def getExampleList: JObject = {
// Create the list item without the singleName wrapper - the actual API response
// returns a flat list of objects, not wrapped in entity name
val listItem: JObject = JObject(JField(idName, JString(ExampleValue.idExample.value)) :: getSingleExampleWithoutId.obj)
if (bankId.isDefined) {
val objectList: JObject = (listName -> JArray(List(listItem)))
bankIdJObject merge objectList
} else {
(listName -> JArray(List(listItem)))
}
}

val canCreateRole: ApiRole = DynamicEntityInfo.canCreateRole(entityName, bankId)
Expand Down
3 changes: 3 additions & 0 deletions obp-api/src/main/scala/code/api/util/ApiRole.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 21 additions & 0 deletions obp-api/src/main/scala/code/api/util/Glossary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ object Glossary extends MdcLoggable {
|
|Dynamic Entities can be found under the **More** list of API Versions. Look for versions starting with `OBPdynamic-entity` or similar in the version selector.
|
|To programmatically discover all Dynamic Entity endpoints, use: `GET /resource-docs/API_VERSION/obp?content=dynamic`
|
|For more information about Dynamic Entities see ${getGlossaryItemLink("Dynamic-Entities")}
|
|### Creating Favorites
Expand Down Expand Up @@ -3316,6 +3318,25 @@ object Glossary extends MdcLoggable {
|* PUT /management/system-dynamic-entities/DYNAMIC_ENTITY_ID - Update entity definition
|* DELETE /management/system-dynamic-entities/DYNAMIC_ENTITY_ID - Delete entity (and all its data)
|
|**Discovering Dynamic Entity Endpoints (for application developers):**
|
|Once Dynamic Entities are created, their auto-generated CRUD endpoints are documented in the Resource Docs API. To programmatically discover all available Dynamic Entity endpoints, use:
|
|```
|GET /resource-docs/API_VERSION/obp?content=dynamic
|```
|
|For example: `GET /resource-docs/v5.1.0/obp?content=dynamic`
|
|This returns documentation for all dynamic endpoints (both Dynamic Entities and Dynamic Endpoints) including:
|
|* Endpoint paths and HTTP methods
|* Request and response schemas with examples
|* Required roles and authentication
|* Field descriptions and types
|
|You can also get this documentation in OpenAPI/Swagger format for code generation and API client tooling.
|
|**Required roles to manage Dynamic Entities:**
|
|* CanCreateSystemLevelDynamicEntity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,11 @@ object Migration extends MdcLoggable {
if (performWrite) {
logFunc(ct)
val st = conn.createStatement
st.execute(ct)
st.close
try {
st.execute(ct)
} finally {
st.close()
}
}
ct
}
Expand Down
16 changes: 16 additions & 0 deletions obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2221,6 +2221,14 @@ trait APIMethods400 extends MdcLoggable {
|
|FYI Dynamic Entities and Dynamic Endpoints are listed in the Resource Doc endpoints by adding content=dynamic to the path. They are cached differently to static endpoints.
|
|**Discovering the generated endpoints:**
|
|After creating a Dynamic Entity, OBP automatically generates CRUD endpoints. To discover these endpoints programmatically, use:
|
|`GET /resource-docs/API_VERSION/obp?content=dynamic`
|
|This returns documentation for all dynamic endpoints including paths, schemas, and required roles.
|
|For more information about Dynamic Entities see ${Glossary
.getGlossaryItemLink("Dynamic-Entities")}
|
Expand Down Expand Up @@ -2430,6 +2438,14 @@ trait APIMethods400 extends MdcLoggable {
|
|FYI Dynamic Entities and Dynamic Endpoints are listed in the Resource Doc endpoints by adding content=dynamic to the path. They are cached differently to static endpoints.
|
|**Discovering the generated endpoints:**
|
|After creating a Dynamic Entity, OBP automatically generates CRUD endpoints. To discover these endpoints programmatically, use:
|
|`GET /resource-docs/API_VERSION/obp?content=dynamic`
|
|This returns documentation for all dynamic endpoints including paths, schemas, and required roles.
|
|For more information about Dynamic Entities see ${Glossary
.getGlossaryItemLink("Dynamic-Entities")}
|
Expand Down
80 changes: 77 additions & 3 deletions obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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")
)
))
)
)
),
Expand Down Expand Up @@ -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")
)
))
)
)
),
Expand Down
67 changes: 65 additions & 2 deletions obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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
*
Expand All @@ -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 =>
Expand All @@ -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)
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ object StoredProcedureUtils extends MdcLoggable{
val sql = s"{ CALL $procedureName(?, ?) }"

val callableStatement = conn.prepareCall(sql)
callableStatement.setString(1, procedureParam)

callableStatement.registerOutParameter(2, java.sql.Types.LONGVARCHAR)
// callableStatement.setString(2, "") // MS sql server must comment this line, other DB need check.
callableStatement.executeUpdate()
callableStatement.getString(2)
try {
callableStatement.setString(1, procedureParam)
callableStatement.registerOutParameter(2, java.sql.Types.LONGVARCHAR)
// callableStatement.setString(2, "") // MS sql server must comment this line, other DB need check.
callableStatement.executeUpdate()
callableStatement.getString(2)
} finally {
callableStatement.close()
}
}
logger.debug(s"${StoredProcedureConnector_vDec2019.toString} inBoundJson: $procedureName = $responseJson" )
Connector.extractAdapterResponse[T](responseJson, Empty)
Expand Down