Fixes #5345 - PUT and PATCH API to support storing custom properties added through entity extension (#5346)

This commit is contained in:
Suresh Srinivas 2022-06-07 09:02:24 -07:00 committed by GitHub
parent b0d64d1684
commit d42c6d2967
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 28 deletions

View File

@ -306,9 +306,13 @@ public interface CollectionDAO {
@RegisterRowMapper(ExtensionMapper.class) @RegisterRowMapper(ExtensionMapper.class)
@SqlQuery( @SqlQuery(
"SELECT extension, json FROM entity_extension WHERE id = :id AND extension " "SELECT extension, json FROM entity_extension WHERE id = :id AND extension "
+ "LIKE CONCAT (:extensionPrefix, '.%')") + "LIKE CONCAT (:extensionPrefix, '.%') "
+ "ORDER BY extension")
List<ExtensionRecord> getExtensions(@Bind("id") String id, @Bind("extensionPrefix") String extensionPrefix); List<ExtensionRecord> getExtensions(@Bind("id") String id, @Bind("extensionPrefix") String extensionPrefix);
@SqlUpdate("DELETE FROM entity_extension WHERE id = :id AND extension = :extension")
void delete(@Bind("id") String id, @Bind("extension") String extension);
@SqlUpdate("DELETE FROM entity_extension WHERE id = :id") @SqlUpdate("DELETE FROM entity_extension WHERE id = :id")
void deleteAll(@Bind("id") String id); void deleteAll(@Bind("id") String id);
} }

View File

@ -572,7 +572,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
} }
} }
protected void cleanup(EntityInterface entityInterface) { protected void cleanup(EntityInterface entityInterface) throws JsonProcessingException {
String id = entityInterface.getId().toString(); String id = entityInterface.getId().toString();
// Delete all the relationships to other entities // Delete all the relationships to other entities
@ -590,6 +590,9 @@ public abstract class EntityRepository<T extends EntityInterface> {
// Delete all the usage data // Delete all the usage data
daoCollection.usageDAO().delete(id); daoCollection.usageDAO().delete(id);
// Delete the extension data storing custom properties
removeExtension(entityInterface);
// Finally, delete the entity // Finally, delete the entity
dao.delete(id); dao.delete(id);
} }
@ -671,24 +674,39 @@ public abstract class EntityRepository<T extends EntityInterface> {
} }
} }
public void storeExtension(T entity) throws JsonProcessingException { public void storeExtension(EntityInterface entity) throws JsonProcessingException {
JsonNode jsonNode = JsonUtils.valueToTree(entity.getExtension()); JsonNode jsonNode = JsonUtils.valueToTree(entity.getExtension());
Iterator<Entry<String, JsonNode>> customFields = jsonNode.fields(); Iterator<Entry<String, JsonNode>> customFields = jsonNode.fields();
while (customFields.hasNext()) { while (customFields.hasNext()) {
Entry<String, JsonNode> entry = customFields.next(); Entry<String, JsonNode> entry = customFields.next();
String fieldName = entry.getKey(); String fieldName = entry.getKey();
JsonNode value = entry.getValue(); JsonNode value = entry.getValue();
storeCustomField(entity, fieldName, value); storeCustomProperty(entity, fieldName, value);
} }
} }
private void storeCustomField(T entity, String fieldName, JsonNode value) throws JsonProcessingException { public void removeExtension(EntityInterface entity) throws JsonProcessingException {
JsonNode jsonNode = JsonUtils.valueToTree(entity.getExtension());
Iterator<Entry<String, JsonNode>> customFields = jsonNode.fields();
while (customFields.hasNext()) {
Entry<String, JsonNode> entry = customFields.next();
removeCustomProperty(entity, entry.getKey());
}
}
private void storeCustomProperty(EntityInterface entity, String fieldName, JsonNode value)
throws JsonProcessingException {
String fieldFQN = TypeRegistry.getCustomPropertyFQN(entityType, fieldName); String fieldFQN = TypeRegistry.getCustomPropertyFQN(entityType, fieldName);
daoCollection daoCollection
.entityExtensionDAO() .entityExtensionDAO()
.insert(entity.getId().toString(), fieldFQN, "customFieldSchema", JsonUtils.pojoToJson(value)); .insert(entity.getId().toString(), fieldFQN, "customFieldSchema", JsonUtils.pojoToJson(value));
} }
private void removeCustomProperty(EntityInterface entity, String fieldName) {
String fieldFQN = TypeRegistry.getCustomPropertyFQN(entityType, fieldName);
daoCollection.entityExtensionDAO().delete(entity.getId().toString(), fieldFQN);
}
public ObjectNode getExtension(T entity) throws JsonProcessingException { public ObjectNode getExtension(T entity) throws JsonProcessingException {
String fieldFQNPrefix = TypeRegistry.getCustomPropertyFQNPrefix(entityType); String fieldFQNPrefix = TypeRegistry.getCustomPropertyFQNPrefix(entityType);
List<ExtensionRecord> records = List<ExtensionRecord> records =
@ -1046,6 +1064,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
updateDescription(); updateDescription();
updateDisplayName(); updateDisplayName();
updateOwner(); updateOwner();
updateExtension();
updateTags(updated.getFullyQualifiedName(), FIELD_TAGS, original.getTags(), updated.getTags()); updateTags(updated.getFullyQualifiedName(), FIELD_TAGS, original.getTags(), updated.getTags());
entitySpecificUpdate(); entitySpecificUpdate();
} }
@ -1135,6 +1154,12 @@ public abstract class EntityRepository<T extends EntityInterface> {
applyTags(updatedTags, fqn); applyTags(updatedTags, fqn);
} }
private void updateExtension() throws JsonProcessingException {
removeExtension(original);
storeExtension(updated);
// TODO change descriptions for custom attributes
}
public final boolean updateVersion(Double oldVersion) { public final boolean updateVersion(Double oldVersion) {
Double newVersion = oldVersion; Double newVersion = oldVersion;
if (majorVersionChange) { if (majorVersionChange) {

View File

@ -90,9 +90,10 @@ import org.openmetadata.common.utils.CommonUtil;
public class TableRepository extends EntityRepository<Table> { public class TableRepository extends EntityRepository<Table> {
// Table fields that can be patched in a PATCH request // Table fields that can be patched in a PATCH request
static final String TABLE_PATCH_FIELDS = "owner,tags,tableConstraints,tablePartition"; static final String TABLE_PATCH_FIELDS = "owner,tags,tableConstraints,tablePartition,extension";
// Table fields that can be updated in a PUT request // Table fields that can be updated in a PUT request
static final String TABLE_UPDATE_FIELDS = "owner,tags,tableConstraints,tablePartition,dataModel,profileSample"; static final String TABLE_UPDATE_FIELDS =
"owner,tags,tableConstraints,tablePartition,dataModel,profileSample," + "extension";
public static final String FIELD_RELATION_COLUMN_TYPE = "table.columns.column"; public static final String FIELD_RELATION_COLUMN_TYPE = "table.columns.column";
public static final String FIELD_RELATION_TABLE_TYPE = "table"; public static final String FIELD_RELATION_TABLE_TYPE = "table";

View File

@ -1138,25 +1138,25 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
// PUT valid custom field intA to the entity type // PUT valid custom field intA to the entity type
TypeResourceTest typeResourceTest = new TypeResourceTest(); TypeResourceTest typeResourceTest = new TypeResourceTest();
INT_TYPE = typeResourceTest.getEntityByName("integer", "", ADMIN_AUTH_HEADERS); INT_TYPE = typeResourceTest.getEntityByName("integer", "", ADMIN_AUTH_HEADERS);
STRING_TYPE = typeResourceTest.getEntityByName("string", "", ADMIN_AUTH_HEADERS); Type entityType = typeResourceTest.getEntityByName(this.entityType, "customProperties", ADMIN_AUTH_HEADERS);
Type entity = typeResourceTest.getEntityByName(this.entityType, "customProperties", ADMIN_AUTH_HEADERS);
CustomProperty fieldA = CustomProperty fieldA =
new CustomProperty().withName("intA").withDescription("intA").withPropertyType(INT_TYPE.getEntityReference()); new CustomProperty().withName("intA").withDescription("intA").withPropertyType(INT_TYPE.getEntityReference());
entity = typeResourceTest.addAndCheckCustomProperty(entity.getId(), fieldA, OK, ADMIN_AUTH_HEADERS); entityType = typeResourceTest.addAndCheckCustomProperty(entityType.getId(), fieldA, OK, ADMIN_AUTH_HEADERS);
final UUID id = entity.getId(); final UUID id = entityType.getId();
// PATCH valid custom field stringB // PATCH valid custom field stringB
STRING_TYPE = typeResourceTest.getEntityByName("string", "", ADMIN_AUTH_HEADERS);
CustomProperty fieldB = CustomProperty fieldB =
new CustomProperty() new CustomProperty()
.withName("stringB") .withName("stringB")
.withDescription("stringB") .withDescription("stringB")
.withPropertyType(STRING_TYPE.getEntityReference()); .withPropertyType(STRING_TYPE.getEntityReference());
String json = JsonUtils.pojoToJson(entity);
ChangeDescription change = getChangeDescription(entity.getVersion()); String json = JsonUtils.pojoToJson(entityType);
ChangeDescription change = getChangeDescription(entityType.getVersion());
change.getFieldsAdded().add(new FieldChange().withName("customProperties").withNewValue(Arrays.asList(fieldB))); change.getFieldsAdded().add(new FieldChange().withName("customProperties").withNewValue(Arrays.asList(fieldB)));
entity.getCustomProperties().add(fieldB); entityType.getCustomProperties().add(fieldB);
entity = typeResourceTest.patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change); entityType = typeResourceTest.patchEntityAndCheck(entityType, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// PUT invalid custom fields to the entity - custom field has invalid type // PUT invalid custom fields to the entity - custom field has invalid type
Type invalidType = Type invalidType =
@ -1172,35 +1172,65 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
CatalogExceptionMessage.entityNotFound(Entity.TYPE, invalidType.getId())); CatalogExceptionMessage.entityNotFound(Entity.TYPE, invalidType.getId()));
// PATCH invalid custom fields to the entity - custom field has invalid type // PATCH invalid custom fields to the entity - custom field has invalid type
String json1 = JsonUtils.pojoToJson(entity); String json1 = JsonUtils.pojoToJson(entityType);
entity.getCustomProperties().add(fieldInvalid); entityType.getCustomProperties().add(fieldInvalid);
Type finalEntity = entity; Type finalEntity = entityType;
assertResponse( assertResponse(
() -> typeResourceTest.patchEntity(id, json1, finalEntity, ADMIN_AUTH_HEADERS), () -> typeResourceTest.patchEntity(id, json1, finalEntity, ADMIN_AUTH_HEADERS),
NOT_FOUND, NOT_FOUND,
CatalogExceptionMessage.entityNotFound(Entity.TYPE, invalidType.getId())); CatalogExceptionMessage.entityNotFound(Entity.TYPE, invalidType.getId()));
// Now create an entity with custom field // Now POST an entity with extension that includes custom field intA
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
ObjectNode jsonNode = mapper.createObjectNode(); ObjectNode jsonNode = mapper.createObjectNode();
jsonNode.set("intA", mapper.convertValue(1, JsonNode.class)); jsonNode.set("intA", mapper.convertValue(1, JsonNode.class));
jsonNode.set("stringB", mapper.convertValue("string", JsonNode.class));
K create = createRequest(test).withExtension(jsonNode); K create = createRequest(test).withExtension(jsonNode);
createAndCheckEntity(create, ADMIN_AUTH_HEADERS); T entity = createAndCheckEntity(create, ADMIN_AUTH_HEADERS);
// Now set the entity custom field with an unknown field name // PUT and update the entity with extension field intA to a new value
jsonNode.set("stringC", mapper.convertValue("string", JsonNode.class)); // Unknown field // TODO to do change description for stored customProperties
assertResponse( jsonNode.set("intA", mapper.convertValue(2, JsonNode.class));
() -> createEntity(createRequest(test, 1).withExtension(jsonNode), ADMIN_AUTH_HEADERS), create = createRequest(test).withExtension(jsonNode);
BAD_REQUEST, entity = updateEntity(create, Status.OK, ADMIN_AUTH_HEADERS);
CatalogExceptionMessage.unknownCustomField("stringC")); assertEquals(JsonUtils.valueToTree(create.getExtension()), JsonUtils.valueToTree(entity.getExtension()));
// Now set the entity custom field to an invalid value // PATCH and update the entity with extension field stringB
// TODO to do change description for stored customProperties
json = JsonUtils.pojoToJson(entity);
jsonNode.set("stringB", mapper.convertValue("stringB", JsonNode.class));
entity.setExtension(jsonNode);
entity = patchEntity(entity.getId(), json, entity, ADMIN_AUTH_HEADERS);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
// PUT and remove field intA from the the entity extension
// TODO to do change description for stored customProperties
jsonNode.remove("intA");
create = createRequest(test).withExtension(jsonNode);
entity = updateEntity(create, Status.OK, ADMIN_AUTH_HEADERS);
assertEquals(JsonUtils.valueToTree(create.getExtension()), JsonUtils.valueToTree(entity.getExtension()));
// PATCH and remove field stringB from the the entity extension
// TODO to do change description for stored customProperties
json = JsonUtils.pojoToJson(entity);
jsonNode.remove("stringB");
entity.setExtension(jsonNode);
entity = patchEntity(entity.getId(), json, entity, ADMIN_AUTH_HEADERS);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
// Now set the entity custom property to an invalid value
jsonNode.set("intA", mapper.convertValue("stringInsteadOfNumber", JsonNode.class)); // String in integer field jsonNode.set("intA", mapper.convertValue("stringInsteadOfNumber", JsonNode.class)); // String in integer field
assertResponseContains( assertResponseContains(
() -> createEntity(createRequest(test, 1).withExtension(jsonNode), ADMIN_AUTH_HEADERS), () -> createEntity(createRequest(test, 1).withExtension(jsonNode), ADMIN_AUTH_HEADERS),
BAD_REQUEST, BAD_REQUEST,
CatalogExceptionMessage.jsonValidationError("intA", "")); CatalogExceptionMessage.jsonValidationError("intA", ""));
// Now set the entity custom property with an unknown field name
jsonNode.remove("intA");
jsonNode.set("stringC", mapper.convertValue("string", JsonNode.class)); // Unknown field
assertResponse(
() -> createEntity(createRequest(test, 1).withExtension(jsonNode), ADMIN_AUTH_HEADERS),
BAD_REQUEST,
CatalogExceptionMessage.unknownCustomField("stringC"));
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////