From afa73894c5f0b4e8a2108f4d85217f14660b9978 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 21 Jan 2026 01:44:44 +0100 Subject: [PATCH] forcing lower_case entity names --- .../scala/code/api/util/ErrorMessages.scala | 1 + .../scala/code/api/v6_0_0/APIMethods600.scala | 68 +++++++++++++------ .../code/api/v6_0_0/DynamicEntityTest.scala | 38 +++++------ 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 572393a37..44fe84e8e 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -75,6 +75,7 @@ object ErrorMessages { val DynamicDataNotFound = "OBP-09015: Dynamic Data not found. Please specify a valid value." val DuplicateQueryParameters = "OBP-09016: Duplicate Query Parameters are not allowed." val DuplicateHeaderKeys = "OBP-09017: Duplicate Header Keys are not allowed." + val InvalidDynamicEntityName = "OBP-09018: Invalid entity_name format. Entity names must be lowercase with underscores (snake_case), e.g. 'customer_preferences'. No uppercase letters or spaces allowed." // General messages (OBP-10XXX) 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 c5dd8a6bf..c9bfb4e1a 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 @@ -4567,7 +4567,7 @@ trait APIMethods600 { dynamic_entities = List( DynamicEntityDefinitionWithCountJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = None, has_personal_entity = true, @@ -4632,7 +4632,7 @@ trait APIMethods600 { dynamic_entities = List( DynamicEntityDefinitionWithCountJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = Some("gh.29.uk"), has_personal_entity = true, @@ -4757,7 +4757,7 @@ trait APIMethods600 { |**Request format:** |```json |{ - | "entity_name": "CustomerPreferences", + | "entity_name": "customer_preferences", | "has_personal_entity": true, | "schema": { | "description": "User preferences", @@ -4770,17 +4770,19 @@ trait APIMethods600 { |} |``` | - |**Important:** Each property MUST include an `example` field with a valid example value. + |**Important:** + |* The `entity_name` must be lowercase with underscores (snake_case), e.g. `customer_preferences`. No uppercase letters or spaces allowed. + |* Each property MUST include an `example` field with a valid example value. | |For more information see ${Glossary.getGlossaryItemLink("Dynamic-Entities")}""", CreateDynamicEntityRequestJsonV600( - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", has_personal_entity = Some(true), schema = net.liftweb.json.parse("""{"description": "User preferences", "required": ["theme"], "properties": {"theme": {"type": "string", "example": "dark"}, "language": {"type": "string", "example": "en"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject] ), DynamicEntityDefinitionJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = None, has_personal_entity = true, @@ -4796,6 +4798,17 @@ trait APIMethods600 { Some(List(canCreateSystemLevelDynamicEntity)) ) + // v6.0.0 entity names must be lowercase with underscores (snake_case) + private val validEntityNamePattern = "^[a-z][a-z0-9_]*$".r.pattern + + private def validateEntityNameV600(entityName: String, callContext: Option[CallContext]): Future[Unit] = { + if (validEntityNamePattern.matcher(entityName).matches()) { + Future.successful(()) + } else { + Future.failed(new RuntimeException(s"$InvalidDynamicEntityName Current value: '$entityName'")) + } + } + lazy val createSystemDynamicEntity: OBPEndpoint = { case "management" :: "system-dynamic-entities" :: Nil JsonPost json -> _ => { cc => implicit val ec = EndpointContext(Some(cc)) @@ -4803,6 +4816,7 @@ trait APIMethods600 { request <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, cc.callContext) { json.extract[CreateDynamicEntityRequestJsonV600] } + _ <- validateEntityNameV600(request.entity_name, cc.callContext) internalJson = JSONFactory600.convertV600RequestToInternal(request) dynamicEntity = DynamicEntityCommons(internalJson, None, cc.userId, None) result <- createDynamicEntityV600(cc, dynamicEntity) @@ -4824,7 +4838,7 @@ trait APIMethods600 { |**Request format:** |```json |{ - | "entity_name": "CustomerPreferences", + | "entity_name": "customer_preferences", | "has_personal_entity": true, | "schema": { | "description": "User preferences", @@ -4837,17 +4851,19 @@ trait APIMethods600 { |} |``` | - |**Important:** Each property MUST include an `example` field with a valid example value. + |**Important:** + |* The `entity_name` must be lowercase with underscores (snake_case), e.g. `customer_preferences`. No uppercase letters or spaces allowed. + |* Each property MUST include an `example` field with a valid example value. | |For more information see ${Glossary.getGlossaryItemLink("Dynamic-Entities")}""", CreateDynamicEntityRequestJsonV600( - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", has_personal_entity = Some(true), schema = net.liftweb.json.parse("""{"description": "User preferences", "required": ["theme"], "properties": {"theme": {"type": "string", "example": "dark"}, "language": {"type": "string", "example": "en"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject] ), DynamicEntityDefinitionJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = Some("gh.29.uk"), has_personal_entity = true, @@ -4871,6 +4887,7 @@ trait APIMethods600 { request <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, cc.callContext) { json.extract[CreateDynamicEntityRequestJsonV600] } + _ <- validateEntityNameV600(request.entity_name, cc.callContext) internalJson = JSONFactory600.convertV600RequestToInternal(request) dynamicEntity = DynamicEntityCommons(internalJson, None, cc.userId, Some(bankId)) result <- createDynamicEntityV600(cc, dynamicEntity) @@ -4892,7 +4909,7 @@ trait APIMethods600 { |**Request format:** |```json |{ - | "entity_name": "CustomerPreferences", + | "entity_name": "customer_preferences", | "has_personal_entity": true, | "schema": { | "description": "User preferences updated", @@ -4906,15 +4923,17 @@ trait APIMethods600 { |} |``` | + |**Important:** The `entity_name` must be lowercase with underscores (snake_case), e.g. `customer_preferences`. No uppercase letters or spaces allowed. + | |For more information see ${Glossary.getGlossaryItemLink("Dynamic-Entities")}""", UpdateDynamicEntityRequestJsonV600( - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", has_personal_entity = Some(true), schema = net.liftweb.json.parse("""{"description": "User preferences updated", "required": ["theme"], "properties": {"theme": {"type": "string", "example": "dark"}, "language": {"type": "string", "example": "en"}, "notifications_enabled": {"type": "boolean", "example": "true"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject] ), DynamicEntityDefinitionJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = None, has_personal_entity = true, @@ -4937,6 +4956,7 @@ trait APIMethods600 { request <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, cc.callContext) { json.extract[UpdateDynamicEntityRequestJsonV600] } + _ <- validateEntityNameV600(request.entity_name, cc.callContext) internalJson = JSONFactory600.convertV600UpdateRequestToInternal(request) dynamicEntity = DynamicEntityCommons(internalJson, Some(dynamicEntityId), cc.userId, None) result <- updateDynamicEntityV600(cc, dynamicEntity) @@ -4958,7 +4978,7 @@ trait APIMethods600 { |**Request format:** |```json |{ - | "entity_name": "CustomerPreferences", + | "entity_name": "customer_preferences", | "has_personal_entity": true, | "schema": { | "description": "User preferences updated", @@ -4972,15 +4992,17 @@ trait APIMethods600 { |} |``` | + |**Important:** The `entity_name` must be lowercase with underscores (snake_case), e.g. `customer_preferences`. No uppercase letters or spaces allowed. + | |For more information see ${Glossary.getGlossaryItemLink("Dynamic-Entities")}""", UpdateDynamicEntityRequestJsonV600( - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", has_personal_entity = Some(true), schema = net.liftweb.json.parse("""{"description": "User preferences updated", "required": ["theme"], "properties": {"theme": {"type": "string", "example": "dark"}, "language": {"type": "string", "example": "en"}, "notifications_enabled": {"type": "boolean", "example": "true"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject] ), DynamicEntityDefinitionJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = Some("gh.29.uk"), has_personal_entity = true, @@ -5004,6 +5026,7 @@ trait APIMethods600 { request <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, cc.callContext) { json.extract[UpdateDynamicEntityRequestJsonV600] } + _ <- validateEntityNameV600(request.entity_name, cc.callContext) internalJson = JSONFactory600.convertV600UpdateRequestToInternal(request) dynamicEntity = DynamicEntityCommons(internalJson, Some(dynamicEntityId), cc.userId, Some(bankId)) result <- updateDynamicEntityV600(cc, dynamicEntity) @@ -5025,7 +5048,7 @@ trait APIMethods600 { |**Request format:** |```json |{ - | "entity_name": "CustomerPreferences", + | "entity_name": "customer_preferences", | "has_personal_entity": true, | "schema": { | "description": "User preferences updated", @@ -5039,15 +5062,17 @@ trait APIMethods600 { |} |``` | + |**Important:** The `entity_name` must be lowercase with underscores (snake_case), e.g. `customer_preferences`. No uppercase letters or spaces allowed. + | |For more information see ${Glossary.getGlossaryItemLink("My-Dynamic-Entities")}""", UpdateDynamicEntityRequestJsonV600( - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", has_personal_entity = Some(true), schema = net.liftweb.json.parse("""{"description": "User preferences updated", "required": ["theme"], "properties": {"theme": {"type": "string", "example": "dark"}, "language": {"type": "string", "example": "en"}, "notifications_enabled": {"type": "boolean", "example": "true"}}}""").asInstanceOf[net.liftweb.json.JsonAST.JObject] ), DynamicEntityDefinitionJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = None, has_personal_entity = true, @@ -5075,6 +5100,7 @@ trait APIMethods600 { request <- NewStyle.function.tryons(s"$InvalidJsonFormat", 400, cc.callContext) { json.extract[UpdateDynamicEntityRequestJsonV600] } + _ <- validateEntityNameV600(request.entity_name, cc.callContext) internalJson = JSONFactory600.convertV600UpdateRequestToInternal(request) dynamicEntity = DynamicEntityCommons(internalJson, Some(dynamicEntityId), cc.userId, existingEntity.get.bankId) result <- updateDynamicEntityV600(cc, dynamicEntity) @@ -6894,7 +6920,7 @@ trait APIMethods600 { dynamic_entities = List( DynamicEntityDefinitionJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = None, has_personal_entity = true, @@ -6949,7 +6975,7 @@ trait APIMethods600 { dynamic_entities = List( DynamicEntityDefinitionJsonV600( dynamic_entity_id = "abc-123-def", - entity_name = "CustomerPreferences", + entity_name = "customer_preferences", user_id = "user-456", bank_id = None, has_personal_entity = true, diff --git a/obp-api/src/test/scala/code/api/v6_0_0/DynamicEntityTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/DynamicEntityTest.scala index f822fe30d..d2dcd0a75 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/DynamicEntityTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/DynamicEntityTest.scala @@ -72,7 +72,7 @@ class DynamicEntityTest extends V600ServerSetup { val rightEntityV600 = parse( """ |{ - | "entity_name": "FooBar", + | "entity_name": "foo_bar", | "has_personal_entity": true, | "schema": { | "description": "description of this entity, can be markdown text.", @@ -100,7 +100,7 @@ class DynamicEntityTest extends V600ServerSetup { val entityWithoutPersonalV600 = parse( """ |{ - | "entity_name": "SharedEntity", + | "entity_name": "shared_entity", | "has_personal_entity": false, | "schema": { | "description": "A shared entity without personal endpoints.", @@ -121,7 +121,7 @@ class DynamicEntityTest extends V600ServerSetup { val wrongRequiredEntityV600 = parse( """ |{ - | "entity_name": "FooBar", + | "entity_name": "foo_bar", | "has_personal_entity": true, | "schema": { | "description": "description of this entity.", @@ -142,7 +142,7 @@ class DynamicEntityTest extends V600ServerSetup { val updatedEntityV600 = parse( """ |{ - | "entity_name": "FooBar", + | "entity_name": "foo_bar", | "has_personal_entity": true, | "schema": { | "description": "Updated description of this entity.", @@ -206,7 +206,7 @@ class DynamicEntityTest extends V600ServerSetup { (responseJson \ "dynamic_entity_id") shouldBe a[JString] And("Response should have snake_case field: entity_name") - (responseJson \ "entity_name").extract[String] should equal("FooBar") + (responseJson \ "entity_name").extract[String] should equal("foo_bar") And("Response should have snake_case field: user_id") (responseJson \ "user_id").extract[String] should equal(resourceUser1.userId) @@ -220,9 +220,9 @@ class DynamicEntityTest extends V600ServerSetup { (schemaField \ "required") shouldBe a[JArray] (schemaField \ "properties") shouldBe a[JObject] - // Verify schema does NOT contain the entity name as a key (old format would have FooBar as key) + // Verify schema does NOT contain the entity name as a key (old format would have foo_bar as key) And("Schema should NOT contain entity name as a dynamic key") - (schemaField \ "FooBar") should equal(JNothing) + (schemaField \ "foo_bar") should equal(JNothing) val dynamicEntityId = (responseJson \ "dynamic_entity_id").extract[String] @@ -245,7 +245,7 @@ class DynamicEntityTest extends V600ServerSetup { val entity = entities.head And("GET response should also use snake_case fields") (entity \ "dynamic_entity_id").extract[String] should equal(dynamicEntityId) - (entity \ "entity_name").extract[String] should equal("FooBar") + (entity \ "entity_name").extract[String] should equal("foo_bar") (entity \ "has_personal_entity").extract[Boolean] should equal(true) And("GET response should include record_count field") @@ -279,7 +279,7 @@ class DynamicEntityTest extends V600ServerSetup { And("Updated response should use snake_case fields") (responseJson \ "dynamic_entity_id").extract[String] should equal(dynamicEntityId) - (responseJson \ "entity_name").extract[String] should equal("FooBar") + (responseJson \ "entity_name").extract[String] should equal("foo_bar") And("Schema should be updated") val schemaField = responseJson \ "schema" @@ -333,7 +333,7 @@ class DynamicEntityTest extends V600ServerSetup { (responseJson \ "bank_id").extract[String] should equal(bankId) And("Response should have entity_name") - (responseJson \ "entity_name").extract[String] should equal("FooBar") + (responseJson \ "entity_name").extract[String] should equal("foo_bar") val dynamicEntityId = (responseJson \ "dynamic_entity_id").extract[String] @@ -352,7 +352,7 @@ class DynamicEntityTest extends V600ServerSetup { val entity = entities.head (entity \ "bank_id").extract[String] should equal(bankId) - (entity \ "entity_name").extract[String] should equal("FooBar") + (entity \ "entity_name").extract[String] should equal("foo_bar") (entity \ "record_count") shouldBe a[JInt] // Cleanup @@ -380,7 +380,7 @@ class DynamicEntityTest extends V600ServerSetup { updateResponse.code should equal(200) And("Updated response should have snake_case fields") - (updateResponse.body \ "entity_name").extract[String] should equal("FooBar") + (updateResponse.body \ "entity_name").extract[String] should equal("foo_bar") (updateResponse.body \ "bank_id").extract[String] should equal(bankId) // Cleanup @@ -425,16 +425,16 @@ class DynamicEntityTest extends V600ServerSetup { entities.size should be >= 1 And("Response should use snake_case fields") - val entity = entities.find(e => (e \ "entity_name").extract[String] == "FooBar").get + val entity = entities.find(e => (e \ "entity_name").extract[String] == "foo_bar").get (entity \ "dynamic_entity_id") shouldBe a[JString] - (entity \ "entity_name").extract[String] should equal("FooBar") + (entity \ "entity_name").extract[String] should equal("foo_bar") (entity \ "user_id").extract[String] should equal(resourceUser1.userId) (entity \ "has_personal_entity").extract[Boolean] should equal(true) And("Schema field should contain only the schema structure") val schemaField = entity \ "schema" (schemaField \ "description") shouldBe a[JString] - (schemaField \ "FooBar") should equal(JNothing) // Should NOT have entity name as key + (schemaField \ "foo_bar") should equal(JNothing) // Should NOT have entity name as key // Test Update My Dynamic Entity When("We update my dynamic entity") @@ -445,7 +445,7 @@ class DynamicEntityTest extends V600ServerSetup { updateResponse.code should equal(200) And("Updated response should use snake_case fields") - (updateResponse.body \ "entity_name").extract[String] should equal("FooBar") + (updateResponse.body \ "entity_name").extract[String] should equal("foo_bar") (updateResponse.body \ "schema" \ "description").extract[String] should equal("Updated description of this entity.") // Cleanup @@ -492,8 +492,8 @@ class DynamicEntityTest extends V600ServerSetup { And("Response should contain only entities with has_personal_entity = true") val entityNames = entities.map(e => (e \ "entity_name").extract[String]) - entityNames should contain("FooBar") - entityNames should not contain("SharedEntity") + entityNames should contain("foo_bar") + entityNames should not contain("shared_entity") And("All returned entities should have has_personal_entity = true") entities.foreach { entity => @@ -535,7 +535,7 @@ class DynamicEntityTest extends V600ServerSetup { (schemaField \ "properties") shouldBe a[JObject] And("Schema should NOT contain the entity name as a nested key (old v4.0.0 format)") - (schemaField \ "FooBar") should equal(JNothing) + (schemaField \ "foo_bar") should equal(JNothing) And("Schema should NOT contain hasPersonalEntity (that's a separate top-level field)") (schemaField \ "hasPersonalEntity") should equal(JNothing)