Merge pull request #2666 from simonredfern/develop

Making dynamic entities more snake_case
This commit is contained in:
Simon Redfern 2026-01-26 15:46:40 +01:00 committed by GitHub
commit 8ab316b492
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 207 additions and 19 deletions

View File

@ -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")
@ -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)

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

@ -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
@ -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

View File

@ -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
}

View File

@ -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")}
|
@ -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")}
|

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)
)
}
)

View File

@ -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)