From bdf3659b41a7fa1563a29bdb9cf74f3e88b438b9 Mon Sep 17 00:00:00 2001 From: Pere Miquel Brull Date: Fri, 22 Aug 2025 12:58:06 +0200 Subject: [PATCH] MINOR - Data Contract fixes and improvements (#23043) * fix change event handling for alerts * contract is deleted when asset is deleted * add support for custom properties * Update generated TypeScript types * handle suite index deletion * validate owner is not coming back if not requested --------- Co-authored-by: github-actions[bot] --- .../native/1.9.3/mysql/schemaChanges.sql | 4 + .../native/1.9.3/postgres/schemaChanges.sql | 4 + .../DataContractValidationApp.java | 14 +- .../service/formatter/util/FormatterUtil.java | 31 ++-- .../service/jdbi3/DataContractRepository.java | 12 +- .../resources/data/DataContractMapper.java | 1 + .../resources/data/DataContractResource.java | 7 +- .../service/resources/EntityResourceTest.java | 4 +- .../data/DataContractResourceTest.java | 165 +++++++++++++----- .../schema/api/data/createDataContract.json | 4 + .../json/schema/entity/data/dataContract.json | 4 + .../generated/api/data/createDataContract.ts | 4 + .../src/generated/entity/data/dataContract.ts | 4 + 13 files changed, 192 insertions(+), 66 deletions(-) create mode 100644 bootstrap/sql/migrations/native/1.9.3/mysql/schemaChanges.sql create mode 100644 bootstrap/sql/migrations/native/1.9.3/postgres/schemaChanges.sql diff --git a/bootstrap/sql/migrations/native/1.9.3/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.9.3/mysql/schemaChanges.sql new file mode 100644 index 00000000000..5bc43fa1b1b --- /dev/null +++ b/bootstrap/sql/migrations/native/1.9.3/mysql/schemaChanges.sql @@ -0,0 +1,4 @@ +-- Update the relation between table and dataContract to 0 (CONTAINS) +UPDATE entity_relationship +SET relation = 0 +WHERE fromEntity = 'table' AND toEntity = 'dataContract' AND relation = 10; \ No newline at end of file diff --git a/bootstrap/sql/migrations/native/1.9.3/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.9.3/postgres/schemaChanges.sql new file mode 100644 index 00000000000..5bc43fa1b1b --- /dev/null +++ b/bootstrap/sql/migrations/native/1.9.3/postgres/schemaChanges.sql @@ -0,0 +1,4 @@ +-- Update the relation between table and dataContract to 0 (CONTAINS) +UPDATE entity_relationship +SET relation = 0 +WHERE fromEntity = 'table' AND toEntity = 'dataContract' AND relation = 10; \ No newline at end of file diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/dataContracts/DataContractValidationApp.java b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/dataContracts/DataContractValidationApp.java index 3d7e503d4e9..f54ca9ffced 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/dataContracts/DataContractValidationApp.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/dataContracts/DataContractValidationApp.java @@ -1,6 +1,7 @@ package org.openmetadata.service.apps.bundles.dataContracts; import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; +import static org.openmetadata.service.Entity.ADMIN_USER_NAME; import static org.openmetadata.service.apps.scheduler.OmAppJobListener.APP_RUN_STATS; import java.util.HashMap; @@ -14,14 +15,18 @@ import org.openmetadata.schema.entity.data.DataContract; import org.openmetadata.schema.entity.datacontract.DataContractResult; import org.openmetadata.schema.system.Stats; import org.openmetadata.schema.system.StepStats; +import org.openmetadata.schema.type.ChangeEvent; +import org.openmetadata.schema.type.EventType; import org.openmetadata.schema.utils.JsonUtils; import org.openmetadata.service.Entity; import org.openmetadata.service.apps.AbstractNativeApplication; +import org.openmetadata.service.formatter.util.FormatterUtil; import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jdbi3.DataContractRepository; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.search.SearchRepository; import org.openmetadata.service.util.EntityUtil; +import org.openmetadata.service.util.RestUtil; import org.openmetadata.service.util.ResultList; import org.quartz.JobExecutionContext; @@ -70,8 +75,15 @@ public class DataContractValidationApp extends AbstractNativeApplication { for (DataContract dataContract : contractBatch) { try { LOG.debug("Validating data contract: {}", dataContract.getFullyQualifiedName()); - DataContractResult validationResult = repository.validateContract(dataContract); + RestUtil.PutResponse validationResponse = + repository.validateContract(dataContract); + DataContractResult validationResult = validationResponse.getEntity(); + ChangeEvent changeEvent = + FormatterUtil.getDataContractResultEvent( + validationResult, ADMIN_USER_NAME, EventType.ENTITY_UPDATED); + changeEvent.setEntity(JsonUtils.pojoToMaskedJson(dataContract)); + Entity.getCollectionDAO().changeEventDAO().insert(JsonUtils.pojoToJson(changeEvent)); LOG.debug( "Validation completed for {}: Status = {}", dataContract.getFullyQualifiedName(), diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/formatter/util/FormatterUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/formatter/util/FormatterUtil.java index b9ef8bd23bb..bfc340b0462 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/formatter/util/FormatterUtil.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/formatter/util/FormatterUtil.java @@ -346,24 +346,27 @@ public class FormatterUtil { if (entityTimeSeries instanceof DataContractResult) { DataContractResult result = JsonUtils.readOrConvertValue(entityTimeSeries, DataContractResult.class); - DataContract contract = - Entity.getEntityByName( - Entity.DATA_CONTRACT, result.getDataContractFQN(), "", Include.ALL); - ChangeEvent changeEvent = - getChangeEvent(updateBy, eventType, contract.getEntityReference().getType(), contract); - - return changeEvent - .withChangeDescription( - new ChangeDescription() - .withFieldsUpdated( - List.of( - new FieldChange().withName(DATA_CONTRACT_RESULT).withNewValue(result)))) - .withEntity(contract) - .withEntityFullyQualifiedName(contract.getFullyQualifiedName()); + return getDataContractResultEvent(result, updateBy, eventType); } return null; } + public static ChangeEvent getDataContractResultEvent( + DataContractResult result, String updateBy, EventType eventType) { + DataContract contract = + Entity.getEntityByName(Entity.DATA_CONTRACT, result.getDataContractFQN(), "", Include.ALL); + ChangeEvent changeEvent = + getChangeEvent(updateBy, eventType, contract.getEntityReference().getType(), contract); + + return changeEvent + .withChangeDescription( + new ChangeDescription() + .withFieldsUpdated( + List.of(new FieldChange().withName(DATA_CONTRACT_RESULT).withNewValue(result)))) + .withEntity(contract) + .withEntityFullyQualifiedName(contract.getFullyQualifiedName()); + } + private static ChangeEvent getChangeEventForThread( String updateBy, EventType eventType, String entityType, Thread thread) { return new ChangeEvent() diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java index 1312305a581..07ffd772182 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java @@ -82,9 +82,9 @@ import org.openmetadata.service.util.RestUtil; public class DataContractRepository extends EntityRepository { private static final String DATA_CONTRACT_UPDATE_FIELDS = - "entity,owners,reviewers,status,schema,qualityExpectations,contractUpdates,semantics,latestResult"; + "entity,owners,reviewers,status,schema,qualityExpectations,contractUpdates,semantics,latestResult,extension"; private static final String DATA_CONTRACT_PATCH_FIELDS = - "entity,owners,reviewers,status,schema,qualityExpectations,contractUpdates,semantics,latestResult"; + "entity,owners,reviewers,status,schema,qualityExpectations,contractUpdates,semantics,latestResult,extension"; public static final String RESULT_EXTENSION = "dataContract.dataContractResult"; public static final String RESULT_SCHEMA = "dataContractResult"; @@ -390,6 +390,7 @@ public class DataContractRepository extends EntityRepository { (TestSuiteRepository) Entity.getEntityRepository(Entity.TEST_SUITE); TestSuite testSuite = getOrCreateTestSuite(dataContract); testSuiteRepository.deleteLogicalTestSuite(ADMIN_USER_NAME, testSuite, true); + testSuiteRepository.deleteFromSearch(testSuite, true); } private TestSuite getOrCreateTestSuite(DataContract dataContract) { @@ -475,7 +476,7 @@ public class DataContractRepository extends EntityRepository { } } - public DataContractResult validateContract(DataContract dataContract) { + public RestUtil.PutResponse validateContract(DataContract dataContract) { // Check if there's a running validation and abort it before starting a new one abortRunningValidation(dataContract); @@ -520,8 +521,7 @@ public class DataContractRepository extends EntityRepository { } // Add the result to the data contract and update the time series - addContractResult(dataContract, result); - return result; + return addContractResult(dataContract, result); } public void deployAndTriggerDQValidation(DataContract dataContract) { @@ -878,7 +878,7 @@ public class DataContractRepository extends EntityRepository { dataContract.getId(), dataContract.getEntity().getType(), Entity.DATA_CONTRACT, - Relationship.HAS); + Relationship.CONTAINS); storeOwners(dataContract, dataContract.getOwners()); storeReviewers(dataContract, dataContract.getReviewers()); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractMapper.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractMapper.java index 7339634f25f..b7a76375fb1 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractMapper.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractMapper.java @@ -43,6 +43,7 @@ public class DataContractMapper { .withEffectiveFrom(create.getEffectiveFrom()) .withEffectiveUntil(create.getEffectiveUntil()) .withSourceUrl(create.getSourceUrl()) + .withExtension(create.getExtension()) .withUpdatedBy(user) .withUpdatedAt(System.currentTimeMillis()); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java index 56f23e33ca1..137ea2ff029 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java @@ -73,6 +73,7 @@ import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContext; import org.openmetadata.service.util.EntityUtil.Fields; +import org.openmetadata.service.util.RestUtil; import org.openmetadata.service.util.ResultList; @Slf4j @@ -85,7 +86,7 @@ import org.openmetadata.service.util.ResultList; @Collection(name = "dataContracts") public class DataContractResource extends EntityResource { public static final String COLLECTION_PATH = "v1/dataContracts/"; - static final String FIELDS = "owners,reviewers"; + static final String FIELDS = "owners,reviewers,extension"; @Override public DataContract addHref(UriInfo uriInfo, DataContract dataContract) { @@ -872,8 +873,8 @@ public class DataContractResource extends EntityResource(Entity.DATA_CONTRACT, id, null); authorizer.authorize(securityContext, operationContext, resourceContext); - DataContractResult result = repository.validateContract(dataContract); - return Response.ok(result).build(); + RestUtil.PutResponse result = repository.validateContract(dataContract); + return result.toResponse(); } // Add runId and dataContractFQN to the result if not incoming diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index 80c74d1daa1..43bcb845ea8 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -3578,7 +3578,7 @@ public abstract class EntityResourceTest { Response response = SecurityUtil.addHeaders(target, TEST_AUTH_HEADERS).put(Entity.json(create)); - TestUtils.readResponse(response, DataContract.class, Status.OK.getStatusCode()); + TestUtils.readResponse(response, DataContract.class, OK.getStatusCode()); }, Status.FORBIDDEN, "Principal: CatalogPrincipal{name='test'} operations [EditAll] not allowed"); @@ -1869,7 +1869,7 @@ public class DataContractResourceTest extends EntityResourceTest getDataContract(created.getId(), null)); @@ -1888,12 +1888,37 @@ public class DataContractResourceTest extends EntityResourceTest { Response response = SecurityUtil.addHeaders(target, TEST_AUTH_HEADERS).delete(); - TestUtils.readResponse(response, DataContract.class, Status.OK.getStatusCode()); + TestUtils.readResponse(response, DataContract.class, OK.getStatusCode()); }, Status.FORBIDDEN, "Principal: CatalogPrincipal{name='test'} operations [Delete] not allowed"); } + @Test + @Execution(ExecutionMode.CONCURRENT) + void testDataContractIsDeletedWhenTableIsDeleted(TestInfo test) throws IOException { + // Create a table + Table table = createUniqueTable(test.getDisplayName()); + + // Create a data contract for the table + CreateDataContract create = createDataContractRequest(test.getDisplayName(), table); + DataContract dataContract = createDataContract(create); + + // Verify the data contract was created + assertNotNull(dataContract); + assertEquals(table.getId(), dataContract.getEntity().getId()); + + // Verify we can get the data contract before deletion + DataContract retrieved = getDataContract(dataContract.getId(), null); + assertNotNull(retrieved); + + // Delete the table + deleteTable(table.getId()); + + // Verify that the data contract is also deleted (should throw HttpResponseException) + assertThrows(HttpResponseException.class, () -> getDataContract(dataContract.getId(), null)); + } + @Test @Execution(ExecutionMode.CONCURRENT) void testAllUsersCanReadDataContracts(TestInfo test) throws IOException { @@ -1905,7 +1930,7 @@ public class DataContractResourceTest extends EntityResourceTest authHeaders = SecurityUtil.authHeaders("admin@open-metadata.org"); WebTarget userTarget = getResource("users").path("name").path("admin"); Response response = SecurityUtil.addHeaders(userTarget, authHeaders).get(); - User adminUser = TestUtils.readResponse(response, User.class, Status.OK.getStatusCode()); + User adminUser = TestUtils.readResponse(response, User.class, OK.getStatusCode()); // Create user references for reviewers using the full entity reference List reviewers = new ArrayList<>(); @@ -1967,7 +1992,7 @@ public class DataContractResourceTest extends EntityResourceTest authHeaders = SecurityUtil.authHeaders("admin@open-metadata.org"); WebTarget userTarget = getResource("users").path("name").path("admin"); Response response = SecurityUtil.addHeaders(userTarget, authHeaders).get(); - User adminUser = TestUtils.readResponse(response, User.class, Status.OK.getStatusCode()); + User adminUser = TestUtils.readResponse(response, User.class, OK.getStatusCode()); // Get the full data contract with all fields created = getDataContract(created.getId(), "reviewers"); @@ -1994,7 +2019,7 @@ public class DataContractResourceTest extends EntityResourceTest authHeaders = SecurityUtil.authHeaders("admin@open-metadata.org"); WebTarget userTarget = getResource("users").path("name").path("admin"); Response response = SecurityUtil.addHeaders(userTarget, authHeaders).get(); - User adminUser = TestUtils.readResponse(response, User.class, Status.OK.getStatusCode()); + User adminUser = TestUtils.readResponse(response, User.class, OK.getStatusCode()); // Create with reviewers List initialReviewers = new ArrayList<>(); @@ -2029,11 +2054,11 @@ public class DataContractResourceTest extends EntityResourceTest authHeaders = SecurityUtil.authHeaders("admin@open-metadata.org"); WebTarget userTarget = getResource("users").path("name").path("admin"); Response response = SecurityUtil.addHeaders(userTarget, authHeaders).get(); - User adminUser = TestUtils.readResponse(response, User.class, Status.OK.getStatusCode()); + User adminUser = TestUtils.readResponse(response, User.class, OK.getStatusCode()); userTarget = getResource("users").path("name").path("test"); response = SecurityUtil.addHeaders(userTarget, authHeaders).get(); - User testUser = TestUtils.readResponse(response, User.class, Status.OK.getStatusCode()); + User testUser = TestUtils.readResponse(response, User.class, OK.getStatusCode()); // Create with one reviewer List initialReviewers = new ArrayList<>(); @@ -2084,7 +2109,7 @@ public class DataContractResourceTest extends EntityResourceTest results = JsonUtils.readValue( jsonResponse, @@ -2195,7 +2219,7 @@ public class DataContractResourceTest extends EntityResourceTest results = JsonUtils.readValue( jsonResponse, @@ -2244,12 +2267,11 @@ public class DataContractResourceTest extends EntityResourceTest results2 = JsonUtils.readValue( jsonResponse2, @@ -2566,7 +2588,7 @@ public class DataContractResourceTest extends EntityResourceTest allResults = JsonUtils.readValue( jsonResponse, @@ -3077,8 +3098,7 @@ public class DataContractResourceTest extends EntityResourceTest allResults = JsonUtils.readValue( jsonResponse, @@ -3173,6 +3193,18 @@ public class DataContractResourceTest extends EntityResourceTest searchQueryParams = new HashMap<>(); + searchQueryParams.put("fullyQualifiedName", testSuite.getFullyQualifiedName()); + searchQueryParams.put("fields", "tests"); + ResultList testSuitesInSearchIndex = + testSuiteResourceTest.listEntitiesFromSearch(searchQueryParams, 10, 0, ADMIN_AUTH_HEADERS); + + assertNotNull(testSuitesInSearchIndex); + assertFalse(testSuitesInSearchIndex.getData().isEmpty()); + assertEquals(1, testSuitesInSearchIndex.getData().size()); + assertEquals(testSuite.getId(), testSuitesInSearchIndex.getData().get(0).getId()); + // Verify both test cases exist and are accessible before deletion TestCase retrievedTestCase1 = testCaseResourceTest.getEntity(testCase1.getId(), "*", ADMIN_AUTH_HEADERS); @@ -3193,6 +3225,13 @@ public class DataContractResourceTest extends EntityResourceTest testSuiteResourceTest.getEntityByName(expectedTestSuiteName, "*", ADMIN_AUTH_HEADERS)); + // Verify test suite is no longer in search index + ResultList testSuitesInSearchIndexAfterDeletion = + testSuiteResourceTest.listEntitiesFromSearch(searchQueryParams, 10, 0, ADMIN_AUTH_HEADERS); + + assertNotNull(testSuitesInSearchIndexAfterDeletion); + assertTrue(testSuitesInSearchIndexAfterDeletion.getData().isEmpty()); + // CRITICAL ASSERTION: Verify the test cases are NOT deleted - they should still exist // independently TestCase testCase1AfterDeletion = @@ -3275,4 +3314,50 @@ public class DataContractResourceTest extends EntityResourceTest owners = new ArrayList<>(); + owners.add(USER1_REF); + + CreateDataContract create = + createDataContractRequest(test.getDisplayName(), table).withOwners(owners); + + DataContract created = createDataContract(create); + + // Verify owner was set during creation + assertNotNull(created.getOwners()); + assertEquals(1, created.getOwners().size()); + + // Test 1: Get without specifying fields - should not include owner information + DataContract retrievedWithoutFields = getDataContract(created.getId(), null); + assertNotNull(retrievedWithoutFields); + assertEquals(created.getId(), retrievedWithoutFields.getId()); + assertNull(retrievedWithoutFields.getOwners()); + + // Test 2: Get with "owners" in fields - should include owner information + DataContract retrievedWithOwners = getDataContract(created.getId(), "owners"); + assertNotNull(retrievedWithOwners); + assertEquals(created.getId(), retrievedWithOwners.getId()); + assertNotNull(retrievedWithOwners.getOwners()); + assertEquals(1, retrievedWithOwners.getOwners().size()); + assertEquals(USER1_REF.getId(), retrievedWithOwners.getOwners().get(0).getId()); + + // Test 3: Get with multiple fields including owners - should include owner information + DataContract retrievedWithMultipleFields = getDataContract(created.getId(), "owners,reviewers"); + assertNotNull(retrievedWithMultipleFields); + assertEquals(created.getId(), retrievedWithMultipleFields.getId()); + assertNotNull(retrievedWithMultipleFields.getOwners()); + assertEquals(1, retrievedWithMultipleFields.getOwners().size()); + + // Test 4: Get with fields that exclude owners - should not include owner information + DataContract retrievedWithoutOwnerField = getDataContract(created.getId(), "reviewers"); + assertNotNull(retrievedWithoutOwnerField); + assertEquals(created.getId(), retrievedWithoutOwnerField.getId()); + assertNull(retrievedWithoutOwnerField.getOwners()); + } } diff --git a/openmetadata-spec/src/main/resources/json/schema/api/data/createDataContract.json b/openmetadata-spec/src/main/resources/json/schema/api/data/createDataContract.json index 190bf4a68c1..3df237445c9 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/data/createDataContract.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/data/createDataContract.json @@ -76,6 +76,10 @@ "sourceUrl": { "description": "Source URL of the data contract.", "$ref": "../../type/basic.json#/definitions/sourceUrl" + }, + "extension": { + "description": "Entity extension data with custom attributes added to the entity.", + "$ref": "../../type/basic.json#/definitions/entityExtension" } }, "required": [ diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json b/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json index 9747b9b8a58..12290ab6ba6 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/data/dataContract.json @@ -197,6 +197,10 @@ } }, "additionalProperties": false + }, + "extension": { + "description": "Entity extension data with custom attributes added to the entity.", + "$ref": "../../type/basic.json#/definitions/entityExtension" } }, "required": [ diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/data/createDataContract.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/data/createDataContract.ts index 496c6c2a3fe..083dcb06ad0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/data/createDataContract.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/data/createDataContract.ts @@ -34,6 +34,10 @@ export interface CreateDataContract { * Reference to the data entity (table, topic, etc.) this contract applies to. */ entity: EntityReference; + /** + * Entity extension data with custom attributes added to the entity. + */ + extension?: any; /** * Name of the data contract. */ diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts b/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts index 85f3b246079..7fcfa27aa97 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/entity/data/dataContract.ts @@ -46,6 +46,10 @@ export interface DataContract { * Reference to the data entity (table, topic, etc.) this contract applies to. */ entity: EntityReference; + /** + * Entity extension data with custom attributes added to the entity. + */ + extension?: any; /** * Fully qualified name of the data contract. */