mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
Merge pull request #2666 from simonredfern/develop
Making dynamic entities more snake_case
This commit is contained in:
commit
8ab316b492
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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")}
|
||||
|
|
||||
|
||||
@ -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")
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user