Dynamic Entity definition to schema

This commit is contained in:
simonredfern 2026-01-20 23:59:19 +01:00
parent bfa3917ce1
commit 7fdf8faacc
3 changed files with 66 additions and 66 deletions

View File

@ -4571,7 +4571,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
definition = 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],
record_count = 42
)
)
@ -4636,7 +4636,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = Some("gh.29.uk"),
has_personal_entity = true,
definition = 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],
record_count = 42
)
)
@ -4759,7 +4759,7 @@ trait APIMethods600 {
|{
| "entity_name": "CustomerPreferences",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "User preferences",
| "required": ["theme"],
| "properties": {
@ -4776,7 +4776,7 @@ trait APIMethods600 {
CreateDynamicEntityRequestJsonV600(
entity_name = "CustomerPreferences",
has_personal_entity = Some(true),
definition = 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]
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",
@ -4784,7 +4784,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
definition = 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]
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]
),
List(
$UserNotLoggedIn,
@ -4826,7 +4826,7 @@ trait APIMethods600 {
|{
| "entity_name": "CustomerPreferences",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "User preferences",
| "required": ["theme"],
| "properties": {
@ -4843,7 +4843,7 @@ trait APIMethods600 {
CreateDynamicEntityRequestJsonV600(
entity_name = "CustomerPreferences",
has_personal_entity = Some(true),
definition = 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]
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",
@ -4851,7 +4851,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = Some("gh.29.uk"),
has_personal_entity = true,
definition = 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]
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]
),
List(
$BankNotFound,
@ -4894,7 +4894,7 @@ trait APIMethods600 {
|{
| "entity_name": "CustomerPreferences",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "User preferences updated",
| "required": ["theme"],
| "properties": {
@ -4910,7 +4910,7 @@ trait APIMethods600 {
UpdateDynamicEntityRequestJsonV600(
entity_name = "CustomerPreferences",
has_personal_entity = Some(true),
definition = 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]
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",
@ -4918,7 +4918,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
definition = 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]
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]
),
List(
$UserNotLoggedIn,
@ -4960,7 +4960,7 @@ trait APIMethods600 {
|{
| "entity_name": "CustomerPreferences",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "User preferences updated",
| "required": ["theme"],
| "properties": {
@ -4976,7 +4976,7 @@ trait APIMethods600 {
UpdateDynamicEntityRequestJsonV600(
entity_name = "CustomerPreferences",
has_personal_entity = Some(true),
definition = 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]
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",
@ -4984,7 +4984,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = Some("gh.29.uk"),
has_personal_entity = true,
definition = 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]
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]
),
List(
$BankNotFound,
@ -5027,7 +5027,7 @@ trait APIMethods600 {
|{
| "entity_name": "CustomerPreferences",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "User preferences updated",
| "required": ["theme"],
| "properties": {
@ -5043,7 +5043,7 @@ trait APIMethods600 {
UpdateDynamicEntityRequestJsonV600(
entity_name = "CustomerPreferences",
has_personal_entity = Some(true),
definition = 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]
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",
@ -5051,7 +5051,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
definition = 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]
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]
),
List(
$UserNotLoggedIn,
@ -6898,7 +6898,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
definition = 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]
)
)
),
@ -6953,7 +6953,7 @@ trait APIMethods600 {
user_id = "user-456",
bank_id = None,
has_personal_entity = true,
definition = 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]
)
)
),

View File

@ -487,14 +487,14 @@ case class AbacPoliciesJsonV600(
)
// Dynamic Entity definition with fully predictable structure (v6.0.0 format)
// No dynamic keys - entity name is an explicit field, schema is in 'definition'
// No dynamic keys - entity name is an explicit field, schema describes the structure
case class DynamicEntityDefinitionJsonV600(
dynamic_entity_id: String,
entity_name: String,
user_id: String,
bank_id: Option[String],
has_personal_entity: Boolean,
definition: net.liftweb.json.JsonAST.JObject
schema: net.liftweb.json.JsonAST.JObject
)
case class MyDynamicEntitiesJsonV600(
@ -508,7 +508,7 @@ case class DynamicEntityDefinitionWithCountJsonV600(
user_id: String,
bank_id: Option[String],
has_personal_entity: Boolean,
definition: net.liftweb.json.JsonAST.JObject,
schema: net.liftweb.json.JsonAST.JObject,
record_count: Long
)
@ -520,14 +520,14 @@ case class DynamicEntitiesWithCountJsonV600(
case class CreateDynamicEntityRequestJsonV600(
entity_name: String,
has_personal_entity: Option[Boolean], // defaults to true if not provided
definition: net.liftweb.json.JsonAST.JObject
schema: net.liftweb.json.JsonAST.JObject
)
// Request format for updating a dynamic entity (v6.0.0 with snake_case)
case class UpdateDynamicEntityRequestJsonV600(
entity_name: String,
has_personal_entity: Option[Boolean],
definition: net.liftweb.json.JsonAST.JObject
schema: net.liftweb.json.JsonAST.JObject
)
object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
@ -1343,7 +1343,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
* Create v6.0.0 response for GET /my/dynamic-entities
*
* Fully predictable structure with no dynamic keys.
* Entity name is an explicit field, schema is in 'definition'.
* Entity name is an explicit field, schema describes the structure.
*
* Response format:
* {
@ -1354,7 +1354,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
* "user_id": "user-456",
* "bank_id": null,
* "has_personal_entity": true,
* "definition": { ... schema ... }
* "schema": { ... }
* }
* ]
* }
@ -1378,7 +1378,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
)
}
val schema = schemaOption.getOrElse(
val schemaObj = schemaOption.getOrElse(
throw new IllegalStateException(s"Could not extract schema for entity '${entity.entityName}' from metadataJson")
)
@ -1388,7 +1388,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
user_id = entity.userId,
bank_id = entity.bankId,
has_personal_entity = entity.hasPersonalEntity,
definition = schema
schema = schemaObj
)
}
)
@ -1418,7 +1418,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
)
}
val schema = schemaOption.getOrElse(
val schemaObj = schemaOption.getOrElse(
throw new IllegalStateException(s"Could not extract schema for entity '${entity.entityName}' from metadataJson")
)
@ -1428,7 +1428,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
user_id = entity.userId,
bank_id = entity.bankId,
has_personal_entity = entity.hasPersonalEntity,
definition = schema,
schema = schemaObj,
record_count = recordCount
)
}
@ -1442,7 +1442,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
* {
* "entity_name": "CustomerPreferences",
* "has_personal_entity": true,
* "definition": { ... schema ... }
* "schema": { ... }
* }
*
* Output (internal):
@ -1459,7 +1459,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
// Build the internal format: entity name as dynamic key + hasPersonalEntity
JObject(
JField(request.entity_name, request.definition) ::
JField(request.entity_name, request.schema) ::
JField("hasPersonalEntity", JBool(hasPersonalEntity)) ::
Nil
)
@ -1473,7 +1473,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
// Build the internal format: entity name as dynamic key + hasPersonalEntity
JObject(
JField(request.entity_name, request.definition) ::
JField(request.entity_name, request.schema) ::
JField("hasPersonalEntity", JBool(hasPersonalEntity)) ::
Nil
)

View File

@ -74,7 +74,7 @@ class DynamicEntityTest extends V600ServerSetup {
|{
| "entity_name": "FooBar",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "description of this entity, can be markdown text.",
| "required": [
| "name"
@ -102,7 +102,7 @@ class DynamicEntityTest extends V600ServerSetup {
|{
| "entity_name": "SharedEntity",
| "has_personal_entity": false,
| "definition": {
| "schema": {
| "description": "A shared entity without personal endpoints.",
| "required": [
| "title"
@ -123,7 +123,7 @@ class DynamicEntityTest extends V600ServerSetup {
|{
| "entity_name": "FooBar",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "description of this entity.",
| "required": [
| "name_wrong"
@ -144,7 +144,7 @@ class DynamicEntityTest extends V600ServerSetup {
|{
| "entity_name": "FooBar",
| "has_personal_entity": true,
| "definition": {
| "schema": {
| "description": "Updated description of this entity.",
| "required": [
| "name"
@ -214,15 +214,15 @@ class DynamicEntityTest extends V600ServerSetup {
And("Response should have snake_case field: has_personal_entity")
(responseJson \ "has_personal_entity").extract[Boolean] should equal(true)
And("Response should have definition field with just the schema (no entity name wrapper)")
val definition = responseJson \ "definition"
(definition \ "description") shouldBe a[JString]
(definition \ "required") shouldBe a[JArray]
(definition \ "properties") shouldBe a[JObject]
And("Response should have schema field with just the schema (no entity name wrapper)")
val schemaField = responseJson \ "schema"
(schemaField \ "description") shouldBe a[JString]
(schemaField \ "required") shouldBe a[JArray]
(schemaField \ "properties") shouldBe a[JObject]
// Verify definition does NOT contain the entity name as a key (old format would have FooBar as key)
And("Definition should NOT contain entity name as a dynamic key")
(definition \ "FooBar") should equal(JNothing)
// Verify schema does NOT contain the entity name as a key (old format would have FooBar as key)
And("Schema should NOT contain entity name as a dynamic key")
(schemaField \ "FooBar") should equal(JNothing)
val dynamicEntityId = (responseJson \ "dynamic_entity_id").extract[String]
@ -281,9 +281,9 @@ class DynamicEntityTest extends V600ServerSetup {
(responseJson \ "dynamic_entity_id").extract[String] should equal(dynamicEntityId)
(responseJson \ "entity_name").extract[String] should equal("FooBar")
And("Definition should be updated")
val definition = responseJson \ "definition"
(definition \ "description").extract[String] should equal("Updated description of this entity.")
And("Schema should be updated")
val schemaField = responseJson \ "schema"
(schemaField \ "description").extract[String] should equal("Updated description of this entity.")
// Cleanup
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteSystemLevelDynamicEntity.toString)
@ -431,10 +431,10 @@ class DynamicEntityTest extends V600ServerSetup {
(entity \ "user_id").extract[String] should equal(resourceUser1.userId)
(entity \ "has_personal_entity").extract[Boolean] should equal(true)
And("Definition should contain only the schema")
val definition = entity \ "definition"
(definition \ "description") shouldBe a[JString]
(definition \ "FooBar") should equal(JNothing) // Should NOT have entity name as key
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
// Test Update My Dynamic Entity
When("We update my dynamic entity")
@ -446,7 +446,7 @@ class DynamicEntityTest extends V600ServerSetup {
And("Updated response should use snake_case fields")
(updateResponse.body \ "entity_name").extract[String] should equal("FooBar")
(updateResponse.body \ "definition" \ "description").extract[String] should equal("Updated description of this entity.")
(updateResponse.body \ "schema" \ "description").extract[String] should equal("Updated description of this entity.")
// Cleanup
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteSystemLevelDynamicEntity.toString)
@ -504,7 +504,7 @@ class DynamicEntityTest extends V600ServerSetup {
entities.foreach { entity =>
(entity \ "dynamic_entity_id") shouldBe a[JString]
(entity \ "entity_name") shouldBe a[JString]
(entity \ "definition") shouldBe a[JObject]
(entity \ "schema") shouldBe a[JObject]
}
// Cleanup
@ -517,9 +517,9 @@ class DynamicEntityTest extends V600ServerSetup {
}
feature("v6.0.0 Dynamic Entity definition field validation") {
feature("v6.0.0 Dynamic Entity schema field validation") {
scenario("Verify definition contains only schema, not entity name wrapper", ApiEndpoint1, VersionOfApi) {
scenario("Verify schema contains only schema structure, not entity name wrapper", ApiEndpoint1, VersionOfApi) {
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateSystemLevelDynamicEntity.toString)
val createRequest = (v6_0_0_Request / "management" / "system-dynamic-entities").POST <@(user1)
@ -527,19 +527,19 @@ class DynamicEntityTest extends V600ServerSetup {
createResponse.code should equal(201)
val dynamicEntityId = (createResponse.body \ "dynamic_entity_id").extract[String]
val definition = createResponse.body \ "definition"
val schemaField = createResponse.body \ "schema"
Then("Definition should contain schema fields directly")
(definition \ "description") shouldBe a[JString]
(definition \ "required") shouldBe a[JArray]
(definition \ "properties") shouldBe a[JObject]
Then("Schema should contain schema fields directly")
(schemaField \ "description") shouldBe a[JString]
(schemaField \ "required") shouldBe a[JArray]
(schemaField \ "properties") shouldBe a[JObject]
And("Definition should NOT contain the entity name as a nested key (old v4.0.0 format)")
(definition \ "FooBar") should equal(JNothing)
And("Schema should NOT contain the entity name as a nested key (old v4.0.0 format)")
(schemaField \ "FooBar") should equal(JNothing)
And("Definition should NOT contain hasPersonalEntity (that's a separate top-level field)")
(definition \ "hasPersonalEntity") should equal(JNothing)
(definition \ "has_personal_entity") should equal(JNothing)
And("Schema should NOT contain hasPersonalEntity (that's a separate top-level field)")
(schemaField \ "hasPersonalEntity") should equal(JNothing)
(schemaField \ "has_personal_entity") should equal(JNothing)
// Cleanup
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanDeleteSystemLevelDynamicEntity.toString)