feat(structuredProperties) Add new settings aspect plus graphql changes for structured props (#12052)

This commit is contained in:
Chris Collins 2024-12-11 13:59:14 -05:00 committed by GitHub
parent b091e4615d
commit f1ef4f8e5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 915 additions and 126 deletions

View File

@ -1,7 +1,8 @@
package com.linkedin.datahub.graphql.resolvers.structuredproperties;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_ENTITY_NAME;
import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.common.urn.Urn;
import com.linkedin.data.template.SetMode;
@ -12,20 +13,24 @@ import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateStructuredPropertyInput;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity;
import com.linkedin.datahub.graphql.generated.StructuredPropertySettingsInput;
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertyMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.aspect.patch.builder.StructuredPropertyDefinitionPatchBuilder;
import com.linkedin.metadata.models.StructuredPropertyUtils;
import com.linkedin.metadata.utils.EntityKeyUtils;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.structured.PrimitivePropertyValue;
import com.linkedin.structured.PropertyCardinality;
import com.linkedin.structured.PropertyValue;
import com.linkedin.structured.StructuredPropertyKey;
import com.linkedin.structured.StructuredPropertySettings;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;
@ -54,14 +59,83 @@ public class CreateStructuredPropertyResolver
"Unable to create structured property. Please contact your admin.");
}
final StructuredPropertyKey key = new StructuredPropertyKey();
final String id = input.getId() != null ? input.getId() : UUID.randomUUID().toString();
final String id =
StructuredPropertyUtils.getPropertyId(input.getId(), input.getQualifiedName());
key.setId(id);
final Urn propertyUrn =
EntityKeyUtils.convertEntityKeyToUrn(key, STRUCTURED_PROPERTY_ENTITY_NAME);
if (_entityClient.exists(context.getOperationContext(), propertyUrn)) {
throw new IllegalArgumentException(
"A structured property already exists with this urn");
}
List<MetadataChangeProposal> mcps = new ArrayList<>();
// first, create the property definition itself
mcps.add(createPropertyDefinition(context, propertyUrn, id, input));
// then add the settings aspect if we're adding any settings inputs
if (input.getSettings() != null) {
mcps.add(createPropertySettings(context, propertyUrn, input.getSettings()));
}
_entityClient.batchIngestProposals(context.getOperationContext(), mcps, false);
EntityResponse response =
_entityClient.getV2(
context.getOperationContext(),
STRUCTURED_PROPERTY_ENTITY_NAME,
propertyUrn,
null);
return StructuredPropertyMapper.map(context, response);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e);
}
});
}
private MetadataChangeProposal createPropertySettings(
@Nonnull final QueryContext context,
@Nonnull final Urn propertyUrn,
final StructuredPropertySettingsInput settingsInput)
throws Exception {
StructuredPropertySettings settings = new StructuredPropertySettings();
if (settingsInput.getIsHidden() != null) {
settings.setIsHidden(settingsInput.getIsHidden());
}
if (settingsInput.getShowInSearchFilters() != null) {
settings.setShowInSearchFilters(settingsInput.getShowInSearchFilters());
}
if (settingsInput.getShowInAssetSummary() != null) {
settings.setShowInAssetSummary(settingsInput.getShowInAssetSummary());
}
if (settingsInput.getShowAsAssetBadge() != null) {
settings.setShowAsAssetBadge(settingsInput.getShowAsAssetBadge());
}
if (settingsInput.getShowInColumnsTable() != null) {
settings.setShowInColumnsTable(settingsInput.getShowInColumnsTable());
}
settings.setLastModified(context.getOperationContext().getAuditStamp());
StructuredPropertyUtils.validatePropertySettings(settings, true);
return buildMetadataChangeProposalWithUrn(
propertyUrn, STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME, settings);
}
private MetadataChangeProposal createPropertyDefinition(
@Nonnull final QueryContext context,
@Nonnull final Urn propertyUrn,
@Nonnull final String id,
final CreateStructuredPropertyInput input)
throws Exception {
StructuredPropertyDefinitionPatchBuilder builder =
new StructuredPropertyDefinitionPatchBuilder().urn(propertyUrn);
builder.setQualifiedName(input.getQualifiedName());
builder.setQualifiedName(id);
builder.setValueType(input.getValueType());
input.getEntityTypes().forEach(builder::addEntityType);
if (input.getDisplayName() != null) {
@ -80,27 +154,12 @@ public class CreateStructuredPropertyResolver
buildAllowedValues(input, builder);
}
if (input.getCardinality() != null) {
builder.setCardinality(
PropertyCardinality.valueOf(input.getCardinality().toString()));
builder.setCardinality(PropertyCardinality.valueOf(input.getCardinality().toString()));
}
builder.setCreated(context.getOperationContext().getAuditStamp());
builder.setLastModified(context.getOperationContext().getAuditStamp());
MetadataChangeProposal mcp = builder.build();
_entityClient.ingestProposal(context.getOperationContext(), mcp, false);
EntityResponse response =
_entityClient.getV2(
context.getOperationContext(),
STRUCTURED_PROPERTY_ENTITY_NAME,
propertyUrn,
null);
return StructuredPropertyMapper.map(context, response);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e);
}
});
return builder.build();
}
private void buildTypeQualifier(

View File

@ -6,6 +6,7 @@ import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.DeleteStructuredPropertyInput;
import com.linkedin.entity.client.EntityClient;
@ -42,6 +43,23 @@ public class DeleteStructuredPropertyResolver implements DataFetcher<Completable
"Unable to delete structured property. Please contact your admin.");
}
_entityClient.deleteEntity(context.getOperationContext(), propertyUrn);
// Asynchronously Delete all references to the entity (to return quickly)
GraphQLConcurrencyUtils.supplyAsync(
() -> {
try {
_entityClient.deleteEntityReferences(
context.getOperationContext(), propertyUrn);
} catch (Exception e) {
log.error(
String.format(
"Caught exception while attempting to clear all entity references for Structured Property with urn %s",
propertyUrn),
e);
}
return null;
},
this.getClass().getSimpleName(),
"get");
return true;
} catch (Exception e) {
throw new RuntimeException(

View File

@ -93,7 +93,7 @@ public class RemoveStructuredPropertiesResolver
.getValue()
.data());
return StructuredPropertiesMapper.map(context, structuredProperties);
return StructuredPropertiesMapper.map(context, structuredProperties, assetUrn);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e);

View File

@ -1,8 +1,8 @@
package com.linkedin.datahub.graphql.resolvers.structuredproperties;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_ENTITY_NAME;
import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
@ -13,18 +13,23 @@ import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity;
import com.linkedin.datahub.graphql.generated.StructuredPropertySettingsInput;
import com.linkedin.datahub.graphql.generated.UpdateStructuredPropertyInput;
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertyMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.aspect.patch.builder.StructuredPropertyDefinitionPatchBuilder;
import com.linkedin.metadata.models.StructuredPropertyUtils;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.structured.PrimitivePropertyValue;
import com.linkedin.structured.PropertyCardinality;
import com.linkedin.structured.PropertyValue;
import com.linkedin.structured.StructuredPropertyDefinition;
import com.linkedin.structured.StructuredPropertySettings;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;
@ -57,36 +62,24 @@ public class UpdateStructuredPropertyResolver
"Unable to update structured property. Please contact your admin.");
}
final Urn propertyUrn = UrnUtils.getUrn(input.getUrn());
StructuredPropertyDefinition existingDefinition =
final EntityResponse entityResponse =
getExistingStructuredProperty(context, propertyUrn);
StructuredPropertyDefinitionPatchBuilder builder =
new StructuredPropertyDefinitionPatchBuilder().urn(propertyUrn);
if (input.getDisplayName() != null) {
builder.setDisplayName(input.getDisplayName());
}
if (input.getDescription() != null) {
builder.setDescription(input.getDescription());
}
if (input.getImmutable() != null) {
builder.setImmutable(input.getImmutable());
}
if (input.getTypeQualifier() != null) {
buildTypeQualifier(input, builder, existingDefinition);
}
if (input.getNewAllowedValues() != null) {
buildAllowedValues(input, builder);
}
if (input.getSetCardinalityAsMultiple() != null) {
builder.setCardinality(PropertyCardinality.MULTIPLE);
}
if (input.getNewEntityTypes() != null) {
input.getNewEntityTypes().forEach(builder::addEntityType);
}
builder.setLastModified(context.getOperationContext().getAuditStamp());
List<MetadataChangeProposal> mcps = new ArrayList<>();
MetadataChangeProposal mcp = builder.build();
_entityClient.ingestProposal(context.getOperationContext(), mcp, false);
// first update the definition aspect if we need to
MetadataChangeProposal definitionMcp =
updateDefinition(input, context, propertyUrn, entityResponse);
if (definitionMcp != null) {
mcps.add(definitionMcp);
}
// then update the settings aspect if we need to
if (input.getSettings() != null) {
mcps.add(updateSettings(context, input.getSettings(), propertyUrn, entityResponse));
}
_entityClient.batchIngestProposals(context.getOperationContext(), mcps, false);
EntityResponse response =
_entityClient.getV2(
@ -102,6 +95,120 @@ public class UpdateStructuredPropertyResolver
});
}
private boolean hasSettingsChanged(
StructuredPropertySettings existingSettings, StructuredPropertySettingsInput settingsInput) {
if (settingsInput.getIsHidden() != null
&& !existingSettings.isIsHidden().equals(settingsInput.getIsHidden())) {
return true;
}
if (settingsInput.getShowInSearchFilters() != null
&& !existingSettings
.isShowInSearchFilters()
.equals(settingsInput.getShowInSearchFilters())) {
return true;
}
if (settingsInput.getShowInAssetSummary() != null
&& !existingSettings.isShowInAssetSummary().equals(settingsInput.getShowInAssetSummary())) {
return true;
}
if (settingsInput.getShowAsAssetBadge() != null
&& !existingSettings.isShowAsAssetBadge().equals(settingsInput.getShowAsAssetBadge())) {
return true;
}
if (settingsInput.getShowInColumnsTable() != null
&& !existingSettings.isShowInColumnsTable().equals(settingsInput.getShowInColumnsTable())) {
return true;
}
return false;
}
private MetadataChangeProposal updateSettings(
@Nonnull final QueryContext context,
@Nonnull final StructuredPropertySettingsInput settingsInput,
@Nonnull final Urn propertyUrn,
@Nonnull final EntityResponse entityResponse)
throws Exception {
StructuredPropertySettings existingSettings =
getExistingStructuredPropertySettings(entityResponse);
// check if settings has changed to determine if we should update the timestamp
boolean hasChanged = hasSettingsChanged(existingSettings, settingsInput);
if (hasChanged) {
existingSettings.setLastModified(context.getOperationContext().getAuditStamp());
}
if (settingsInput.getIsHidden() != null) {
existingSettings.setIsHidden(settingsInput.getIsHidden());
}
if (settingsInput.getShowInSearchFilters() != null) {
existingSettings.setShowInSearchFilters(settingsInput.getShowInSearchFilters());
}
if (settingsInput.getShowInAssetSummary() != null) {
existingSettings.setShowInAssetSummary(settingsInput.getShowInAssetSummary());
}
if (settingsInput.getShowAsAssetBadge() != null) {
existingSettings.setShowAsAssetBadge(settingsInput.getShowAsAssetBadge());
}
if (settingsInput.getShowInColumnsTable() != null) {
existingSettings.setShowInColumnsTable(settingsInput.getShowInColumnsTable());
}
StructuredPropertyUtils.validatePropertySettings(existingSettings, true);
return buildMetadataChangeProposalWithUrn(
propertyUrn, STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME, existingSettings);
}
private MetadataChangeProposal updateDefinition(
@Nonnull final UpdateStructuredPropertyInput input,
@Nonnull final QueryContext context,
@Nonnull final Urn propertyUrn,
@Nonnull final EntityResponse entityResponse)
throws Exception {
StructuredPropertyDefinition existingDefinition =
getExistingStructuredPropertyDefinition(entityResponse);
StructuredPropertyDefinitionPatchBuilder builder =
new StructuredPropertyDefinitionPatchBuilder().urn(propertyUrn);
boolean hasUpdatedDefinition = false;
if (input.getDisplayName() != null) {
builder.setDisplayName(input.getDisplayName());
hasUpdatedDefinition = true;
}
if (input.getDescription() != null) {
builder.setDescription(input.getDescription());
hasUpdatedDefinition = true;
}
if (input.getImmutable() != null) {
builder.setImmutable(input.getImmutable());
hasUpdatedDefinition = true;
}
if (input.getTypeQualifier() != null) {
buildTypeQualifier(input, builder, existingDefinition);
hasUpdatedDefinition = true;
}
if (input.getNewAllowedValues() != null) {
buildAllowedValues(input, builder);
hasUpdatedDefinition = true;
}
if (input.getSetCardinalityAsMultiple() != null
&& input.getSetCardinalityAsMultiple().equals(true)) {
builder.setCardinality(PropertyCardinality.MULTIPLE);
hasUpdatedDefinition = true;
}
if (input.getNewEntityTypes() != null) {
input.getNewEntityTypes().forEach(builder::addEntityType);
hasUpdatedDefinition = true;
}
if (hasUpdatedDefinition) {
builder.setLastModified(context.getOperationContext().getAuditStamp());
return builder.build();
}
return null;
}
private void buildTypeQualifier(
@Nonnull final UpdateStructuredPropertyInput input,
@Nonnull final StructuredPropertyDefinitionPatchBuilder builder,
@ -141,17 +248,40 @@ public class UpdateStructuredPropertyResolver
});
}
private StructuredPropertyDefinition getExistingStructuredProperty(
private EntityResponse getExistingStructuredProperty(
@Nonnull final QueryContext context, @Nonnull final Urn propertyUrn) throws Exception {
EntityResponse response =
_entityClient.getV2(
return _entityClient.getV2(
context.getOperationContext(), STRUCTURED_PROPERTY_ENTITY_NAME, propertyUrn, null);
}
private StructuredPropertyDefinition getExistingStructuredPropertyDefinition(
EntityResponse response) throws Exception {
if (response != null
&& response.getAspects().containsKey(STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME)) {
return new StructuredPropertyDefinition(
response.getAspects().get(STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME).getValue().data());
response
.getAspects()
.get(STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME)
.getValue()
.data()
.copy());
}
return null;
throw new IllegalArgumentException(
"Attempting to update a structured property with no definition aspect.");
}
private StructuredPropertySettings getExistingStructuredPropertySettings(EntityResponse response)
throws Exception {
if (response != null
&& response.getAspects().containsKey(STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME)) {
return new StructuredPropertySettings(
response
.getAspects()
.get(STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME)
.getValue()
.data()
.copy());
}
return new StructuredPropertySettings();
}
}

View File

@ -103,7 +103,7 @@ public class UpsertStructuredPropertiesResolver
_entityClient.ingestProposal(
context.getOperationContext(), structuredPropertiesProposal, false);
return StructuredPropertiesMapper.map(context, structuredProperties);
return StructuredPropertiesMapper.map(context, structuredProperties, assetUrn);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e);

View File

@ -142,7 +142,8 @@ public class ChartMapper implements ModelMapper<EntityResponse, Chart> {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((chart, dataMap) ->
chart.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -161,7 +161,9 @@ public class ContainerMapper {
if (envelopedStructuredProps != null) {
result.setStructuredProperties(
StructuredPropertiesMapper.map(
context, new StructuredProperties(envelopedStructuredProps.getValue().data())));
context,
new StructuredProperties(envelopedStructuredProps.getValue().data()),
entityUrn));
}
final EnvelopedAspect envelopedForms = aspects.get(FORMS_ASPECT_NAME);

View File

@ -59,7 +59,8 @@ public class CorpGroupMapper implements ModelMapper<EntityResponse, CorpGroup> {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -88,7 +88,8 @@ public class CorpUserMapper {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -142,7 +142,8 @@ public class DashboardMapper implements ModelMapper<EntityResponse, Dashboard> {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((dashboard, dataMap) ->
dashboard.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -114,7 +114,8 @@ public class DataFlowMapper implements ModelMapper<EntityResponse, DataFlow> {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -135,7 +135,8 @@ public class DataJobMapper implements ModelMapper<EntityResponse, DataJob> {
result.setSubTypes(SubTypesMapper.map(context, new SubTypes(data)));
} else if (STRUCTURED_PROPERTIES_ASPECT_NAME.equals(name)) {
result.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(data)));
StructuredPropertiesMapper.map(
context, new StructuredProperties(data), entityUrn));
} else if (FORMS_ASPECT_NAME.equals(name)) {
result.setForms(FormsMapper.map(new Forms(data), entityUrn.toString()));
}

View File

@ -92,7 +92,8 @@ public class DataProductMapper implements ModelMapper<EntityResponse, DataProduc
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -173,7 +173,8 @@ public class DatasetMapper implements ModelMapper<EntityResponse, Dataset> {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((dataset, dataMap) ->

View File

@ -71,7 +71,9 @@ public class DomainMapper {
if (envelopedStructuredProps != null) {
result.setStructuredProperties(
StructuredPropertiesMapper.map(
context, new StructuredProperties(envelopedStructuredProps.getValue().data())));
context,
new StructuredProperties(envelopedStructuredProps.getValue().data()),
entityUrn));
}
final EnvelopedAspect envelopedForms = aspects.get(FORMS_ASPECT_NAME);

View File

@ -59,7 +59,8 @@ public class GlossaryNodeMapper implements ModelMapper<EntityResponse, GlossaryN
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -90,7 +90,8 @@ public class GlossaryTermMapper implements ModelMapper<EntityResponse, GlossaryT
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -115,7 +115,8 @@ public class MLFeatureMapper implements ModelMapper<EntityResponse, MLFeature> {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((mlFeature, dataMap) ->
mlFeature.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -117,7 +117,8 @@ public class MLFeatureTableMapper implements ModelMapper<EntityResponse, MLFeatu
STRUCTURED_PROPERTIES_ASPECT_NAME,
((mlFeatureTable, dataMap) ->
mlFeatureTable.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -112,7 +112,8 @@ public class MLModelGroupMapper implements ModelMapper<EntityResponse, MLModelGr
STRUCTURED_PROPERTIES_ASPECT_NAME,
((mlModelGroup, dataMap) ->
mlModelGroup.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -174,7 +174,8 @@ public class MLModelMapper implements ModelMapper<EntityResponse, MLModel> {
STRUCTURED_PROPERTIES_ASPECT_NAME,
((dataset, dataMap) ->
dataset.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -112,7 +112,8 @@ public class MLPrimaryKeyMapper implements ModelMapper<EntityResponse, MLPrimary
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->

View File

@ -41,7 +41,8 @@ public class SchemaFieldMapper implements ModelMapper<EntityResponse, SchemaFiel
STRUCTURED_PROPERTIES_ASPECT_NAME,
((schemaField, dataMap) ->
schemaField.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap)))));
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
BUSINESS_ATTRIBUTE_ASPECT,
(((schemaField, dataMap) ->

View File

@ -25,23 +25,29 @@ public class StructuredPropertiesMapper {
public static final StructuredPropertiesMapper INSTANCE = new StructuredPropertiesMapper();
public static com.linkedin.datahub.graphql.generated.StructuredProperties map(
@Nullable QueryContext context, @Nonnull final StructuredProperties structuredProperties) {
return INSTANCE.apply(context, structuredProperties);
@Nullable QueryContext context,
@Nonnull final StructuredProperties structuredProperties,
@Nonnull final Urn entityUrn) {
return INSTANCE.apply(context, structuredProperties, entityUrn);
}
public com.linkedin.datahub.graphql.generated.StructuredProperties apply(
@Nullable QueryContext context, @Nonnull final StructuredProperties structuredProperties) {
@Nullable QueryContext context,
@Nonnull final StructuredProperties structuredProperties,
@Nonnull final Urn entityUrn) {
com.linkedin.datahub.graphql.generated.StructuredProperties result =
new com.linkedin.datahub.graphql.generated.StructuredProperties();
result.setProperties(
structuredProperties.getProperties().stream()
.map(p -> mapStructuredProperty(context, p))
.map(p -> mapStructuredProperty(context, p, entityUrn))
.collect(Collectors.toList()));
return result;
}
private StructuredPropertiesEntry mapStructuredProperty(
@Nullable QueryContext context, StructuredPropertyValueAssignment valueAssignment) {
@Nullable QueryContext context,
StructuredPropertyValueAssignment valueAssignment,
@Nonnull final Urn entityUrn) {
StructuredPropertiesEntry entry = new StructuredPropertiesEntry();
entry.setStructuredProperty(createStructuredPropertyEntity(valueAssignment));
final List<PropertyValue> values = new ArrayList<>();
@ -58,6 +64,7 @@ public class StructuredPropertiesMapper {
});
entry.setValues(values);
entry.setValueEntities(entities);
entry.setAssociatedUrn(entityUrn.toString());
return entry;
}

View File

@ -17,6 +17,7 @@ import com.linkedin.datahub.graphql.generated.PropertyCardinality;
import com.linkedin.datahub.graphql.generated.StringValue;
import com.linkedin.datahub.graphql.generated.StructuredPropertyDefinition;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity;
import com.linkedin.datahub.graphql.generated.StructuredPropertySettings;
import com.linkedin.datahub.graphql.generated.TypeQualifier;
import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper;
import com.linkedin.datahub.graphql.types.mappers.MapperUtils;
@ -55,6 +56,8 @@ public class StructuredPropertyMapper
MappingHelper<StructuredPropertyEntity> mappingHelper = new MappingHelper<>(aspectMap, result);
mappingHelper.mapToResult(
STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME, (this::mapStructuredPropertyDefinition));
mappingHelper.mapToResult(
STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME, (this::mapStructuredPropertySettings));
return mappingHelper.getResult();
}
@ -112,6 +115,21 @@ public class StructuredPropertyMapper
return allowedValues;
}
private void mapStructuredPropertySettings(
@Nonnull StructuredPropertyEntity extendedProperty, @Nonnull DataMap dataMap) {
com.linkedin.structured.StructuredPropertySettings gmsSettings =
new com.linkedin.structured.StructuredPropertySettings(dataMap);
StructuredPropertySettings settings = new StructuredPropertySettings();
settings.setIsHidden(gmsSettings.isIsHidden());
settings.setShowInSearchFilters(gmsSettings.isShowInSearchFilters());
settings.setShowInAssetSummary(gmsSettings.isShowInAssetSummary());
settings.setShowAsAssetBadge(gmsSettings.isShowAsAssetBadge());
settings.setShowInColumnsTable(gmsSettings.isShowInColumnsTable());
extendedProperty.setSettings(settings);
}
private DataTypeEntity createDataTypeEntity(final Urn dataTypeUrn) {
final DataTypeEntity dataType = new DataTypeEntity();
dataType.setUrn(dataTypeUrn.toString());

View File

@ -27,7 +27,8 @@ public class StructuredPropertyType
implements com.linkedin.datahub.graphql.types.EntityType<StructuredPropertyEntity, String> {
public static final Set<String> ASPECTS_TO_FETCH =
ImmutableSet.of(STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME);
ImmutableSet.of(
STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME, STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME);
private final EntityClient _entityClient;
@Override

View File

@ -49,6 +49,11 @@ type StructuredPropertyEntity implements Entity {
"""
definition: StructuredPropertyDefinition!
"""
Definition of this structured property including its name
"""
settings: StructuredPropertySettings
"""
Granular API for querying edges extending from this entity
"""
@ -117,6 +122,36 @@ type StructuredPropertyDefinition {
lastModified: ResolvedAuditStamp
}
"""
Settings specific to a structured property entity
"""
type StructuredPropertySettings {
"""
Whether or not this asset should be hidden in the main application
"""
isHidden: Boolean!
"""
Whether or not this asset should be displayed as a search filter
"""
showInSearchFilters: Boolean!
"""
Whether or not this asset should be displayed in the asset sidebar
"""
showInAssetSummary: Boolean!
"""
Whether or not this asset should be displayed as an asset badge on other asset's headers
"""
showAsAssetBadge: Boolean!
"""
Whether or not this asset should be displayed as a column in the schema field table in a Dataset's "Columns" tab.
"""
showInColumnsTable: Boolean!
}
"""
An entry for an allowed value for a structured property
"""
@ -202,6 +237,11 @@ type StructuredPropertiesEntry {
The optional entities associated with the values if the values are entity urns
"""
valueEntities: [Entity]
"""
The urn of the entity this property came from for tracking purposes e.g. when sibling nodes are merged together
"""
associatedUrn: String!
}
"""
@ -330,8 +370,9 @@ input CreateStructuredPropertyInput {
"""
The unique fully qualified name of this structured property, dot delimited.
This will be required to match the ID of this structured property.
"""
qualifiedName: String!
qualifiedName: String
"""
The optional display name for this property
@ -375,6 +416,11 @@ input CreateStructuredPropertyInput {
For example: ["urn:li:entityType:datahub.dataset"]
"""
entityTypes: [String!]!
"""
Settings for this structured property
"""
settings: StructuredPropertySettingsInput
}
"""
@ -455,6 +501,11 @@ input UpdateStructuredPropertyInput {
For backwards compatibility, this is append only.
"""
newEntityTypes: [String!]
"""
Settings for this structured property
"""
settings: StructuredPropertySettingsInput
}
"""
@ -477,3 +528,34 @@ input DeleteStructuredPropertyInput {
"""
urn: String!
}
"""
Settings for a structured property
"""
input StructuredPropertySettingsInput {
"""
Whether or not this asset should be hidden in the main application
"""
isHidden: Boolean
"""
Whether or not this asset should be displayed as a search filter
"""
showInSearchFilters: Boolean
"""
Whether or not this asset should be displayed in the asset sidebar
"""
showInAssetSummary: Boolean
"""
Whether or not this asset should be displayed as an asset badge on other asset's headers
"""
showAsAssetBadge: Boolean
"""
Whether or not this asset should be displayed as a column in the schema field table in a Dataset's "Columns" tab.
"""
showInColumnsTable: Boolean
}

View File

@ -10,11 +10,11 @@ import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.CreateStructuredPropertyInput;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity;
import com.linkedin.datahub.graphql.generated.StructuredPropertySettingsInput;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.r2.RemoteInvocationException;
import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
@ -36,7 +36,8 @@ public class CreateStructuredPropertyResolverTest {
null,
null,
null,
new ArrayList<>());
new ArrayList<>(),
null);
@Test
public void testGetSuccess() throws Exception {
@ -56,7 +57,40 @@ public class CreateStructuredPropertyResolverTest {
// Validate that we called ingest
Mockito.verify(mockEntityClient, Mockito.times(1))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false));
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
public void testGetMismatchIdAndQualifiedName() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(true);
CreateStructuredPropertyResolver resolver =
new CreateStructuredPropertyResolver(mockEntityClient);
CreateStructuredPropertyInput testInput =
new CreateStructuredPropertyInput(
"mismatched",
"io.acryl.test",
"Display Name",
"description",
true,
null,
null,
null,
null,
new ArrayList<>(),
null);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
// Validate ingest is not called
Mockito.verify(mockEntityClient, Mockito.times(0))
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
@ -75,7 +109,7 @@ public class CreateStructuredPropertyResolverTest {
// Validate that we did NOT call ingest
Mockito.verify(mockEntityClient, Mockito.times(0))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false));
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
@ -94,7 +128,83 @@ public class CreateStructuredPropertyResolverTest {
// Validate that ingest was called, but that caused a failure
Mockito.verify(mockEntityClient, Mockito.times(1))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false));
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
public void testGetInvalidSettingsInput() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(true);
CreateStructuredPropertyResolver resolver =
new CreateStructuredPropertyResolver(mockEntityClient);
// if isHidden is true, other fields should not be true
StructuredPropertySettingsInput settingsInput = new StructuredPropertySettingsInput();
settingsInput.setIsHidden(true);
settingsInput.setShowAsAssetBadge(true);
CreateStructuredPropertyInput testInput =
new CreateStructuredPropertyInput(
null,
"io.acryl.test",
"Display Name",
"description",
true,
null,
null,
null,
null,
new ArrayList<>(),
settingsInput);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
// Validate ingest is not called
Mockito.verify(mockEntityClient, Mockito.times(0))
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
public void testGetSuccessWithSettings() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(true);
CreateStructuredPropertyResolver resolver =
new CreateStructuredPropertyResolver(mockEntityClient);
StructuredPropertySettingsInput settingsInput = new StructuredPropertySettingsInput();
settingsInput.setShowAsAssetBadge(true);
CreateStructuredPropertyInput testInput =
new CreateStructuredPropertyInput(
null,
"io.acryl.test",
"Display Name",
"description",
true,
null,
null,
null,
null,
new ArrayList<>(),
settingsInput);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
StructuredPropertyEntity prop = resolver.get(mockEnv).get();
assertEquals(prop.getUrn(), TEST_STRUCTURED_PROPERTY_URN);
// Validate that we called ingest
Mockito.verify(mockEntityClient, Mockito.times(1))
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
private EntityClient initMockEntityClient(boolean shouldSucceed) throws Exception {

View File

@ -0,0 +1,91 @@
package com.linkedin.datahub.graphql.resolvers.structuredproperties;
import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext;
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext;
import static org.mockito.ArgumentMatchers.any;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.DeleteStructuredPropertyInput;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.r2.RemoteInvocationException;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletionException;
import org.mockito.Mockito;
import org.testng.annotations.Test;
public class DeleteStructuredPropertyResolverTest {
private static final String TEST_PROP_URN = "urn:li:structuredProperty:test";
private static final DeleteStructuredPropertyInput TEST_INPUT =
new DeleteStructuredPropertyInput(TEST_PROP_URN);
@Test
public void testGetSuccess() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(true);
DeleteStructuredPropertyResolver resolver =
new DeleteStructuredPropertyResolver(mockEntityClient);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
Boolean success = resolver.get(mockEnv).get();
assertTrue(success);
// Validate that we called delete
Mockito.verify(mockEntityClient, Mockito.times(1))
.deleteEntity(any(), Mockito.eq(UrnUtils.getUrn(TEST_PROP_URN)));
}
@Test
public void testGetUnauthorized() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(true);
DeleteStructuredPropertyResolver resolver =
new DeleteStructuredPropertyResolver(mockEntityClient);
// Execute resolver
QueryContext mockContext = getMockDenyContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
// Validate that we did NOT call delete
Mockito.verify(mockEntityClient, Mockito.times(0))
.deleteEntity(any(), Mockito.eq(UrnUtils.getUrn(TEST_PROP_URN)));
}
@Test
public void testGetFailure() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(false);
DeleteStructuredPropertyResolver resolver =
new DeleteStructuredPropertyResolver(mockEntityClient);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
// Validate that deleteEntity was called, but since it's the thing that failed it was called
// once still
Mockito.verify(mockEntityClient, Mockito.times(1))
.deleteEntity(any(), Mockito.eq(UrnUtils.getUrn(TEST_PROP_URN)));
}
private EntityClient initMockEntityClient(boolean shouldSucceed) throws Exception {
EntityClient client = Mockito.mock(EntityClient.class);
if (!shouldSucceed) {
Mockito.doThrow(new RemoteInvocationException()).when(client).deleteEntity(any(), any());
}
return client;
}
}

View File

@ -0,0 +1,42 @@
package com.linkedin.datahub.graphql.resolvers.structuredproperties;
import static org.testng.Assert.*;
import com.linkedin.metadata.models.StructuredPropertyUtils;
import java.util.UUID;
import org.testng.annotations.Test;
public class StructuredPropertyUtilsTest {
@Test
public void testGetIdMismatchedInput() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> StructuredPropertyUtils.getPropertyId("test1", "test2"));
}
@Test
public void testGetIdConsistentInput() throws Exception {
assertEquals(StructuredPropertyUtils.getPropertyId("test1", "test1"), "test1");
}
@Test
public void testGetIdNullQualifiedName() throws Exception {
assertEquals(StructuredPropertyUtils.getPropertyId("test1", null), "test1");
}
@Test
public void testGetIdNullId() throws Exception {
assertEquals(StructuredPropertyUtils.getPropertyId(null, "test1"), "test1");
}
@Test
public void testGetIdNullForBoth() throws Exception {
try {
String id = StructuredPropertyUtils.getPropertyId(null, null);
UUID.fromString(id);
} catch (Exception e) {
fail("ID produced is not a UUID");
}
}
}

View File

@ -2,20 +2,25 @@ package com.linkedin.datahub.graphql.resolvers.structuredproperties;
import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext;
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME;
import static org.mockito.ArgumentMatchers.any;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertThrows;
import com.linkedin.common.UrnArray;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity;
import com.linkedin.datahub.graphql.generated.StructuredPropertySettingsInput;
import com.linkedin.datahub.graphql.generated.UpdateStructuredPropertyInput;
import com.linkedin.entity.Aspect;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspect;
import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.r2.RemoteInvocationException;
import com.linkedin.structured.StructuredPropertyDefinition;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletionException;
import org.mockito.Mockito;
@ -33,6 +38,7 @@ public class UpdateStructuredPropertyResolverTest {
null,
null,
null,
null,
null);
@Test
@ -53,7 +59,7 @@ public class UpdateStructuredPropertyResolverTest {
// Validate that we called ingest
Mockito.verify(mockEntityClient, Mockito.times(1))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false));
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
@ -72,7 +78,7 @@ public class UpdateStructuredPropertyResolverTest {
// Validate that we did NOT call ingest
Mockito.verify(mockEntityClient, Mockito.times(0))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false));
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
@ -91,7 +97,80 @@ public class UpdateStructuredPropertyResolverTest {
// Validate that ingest was not called since there was a get failure before ingesting
Mockito.verify(mockEntityClient, Mockito.times(0))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false));
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
public void testGetInvalidSettingsInput() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(true);
UpdateStructuredPropertyResolver resolver =
new UpdateStructuredPropertyResolver(mockEntityClient);
// if isHidden is true, other fields should not be true
StructuredPropertySettingsInput settingsInput = new StructuredPropertySettingsInput();
settingsInput.setIsHidden(true);
settingsInput.setShowInSearchFilters(true);
final UpdateStructuredPropertyInput testInput =
new UpdateStructuredPropertyInput(
TEST_STRUCTURED_PROPERTY_URN,
"New Display Name",
"new description",
true,
null,
null,
null,
null,
settingsInput);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
// Validate that ingest was not called since there was a get failure before ingesting
Mockito.verify(mockEntityClient, Mockito.times(0))
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
@Test
public void testGetValidSettingsInput() throws Exception {
EntityClient mockEntityClient = initMockEntityClient(true);
UpdateStructuredPropertyResolver resolver =
new UpdateStructuredPropertyResolver(mockEntityClient);
// if isHidden is true, other fields should not be true
StructuredPropertySettingsInput settingsInput = new StructuredPropertySettingsInput();
settingsInput.setIsHidden(true);
final UpdateStructuredPropertyInput testInput =
new UpdateStructuredPropertyInput(
TEST_STRUCTURED_PROPERTY_URN,
"New Display Name",
"new description",
true,
null,
null,
null,
null,
settingsInput);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput);
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
StructuredPropertyEntity prop = resolver.get(mockEnv).get();
assertEquals(prop.getUrn(), TEST_STRUCTURED_PROPERTY_URN);
// Validate that we called ingest
Mockito.verify(mockEntityClient, Mockito.times(1))
.batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
}
private EntityClient initMockEntityClient(boolean shouldSucceed) throws Exception {
@ -99,7 +178,11 @@ public class UpdateStructuredPropertyResolverTest {
EntityResponse response = new EntityResponse();
response.setEntityName(Constants.STRUCTURED_PROPERTY_ENTITY_NAME);
response.setUrn(UrnUtils.getUrn(TEST_STRUCTURED_PROPERTY_URN));
response.setAspects(new EnvelopedAspectMap());
final EnvelopedAspectMap aspectMap = new EnvelopedAspectMap();
aspectMap.put(
STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME,
new EnvelopedAspect().setValue(new Aspect(createDefinition().data())));
response.setAspects(aspectMap);
if (shouldSucceed) {
Mockito.when(
client.getV2(
@ -120,4 +203,13 @@ public class UpdateStructuredPropertyResolverTest {
return client;
}
private StructuredPropertyDefinition createDefinition() {
StructuredPropertyDefinition definition = new StructuredPropertyDefinition();
definition.setDisplayName("test");
definition.setQualifiedName("test");
definition.setValueType(UrnUtils.getUrn("urn:li:dataType:datahub.string"));
definition.setEntityTypes(new UrnArray());
return definition;
}
}

View File

@ -20,6 +20,7 @@ import com.linkedin.metadata.query.filter.Filter;
import com.linkedin.structured.PrimitivePropertyValue;
import com.linkedin.structured.StructuredProperties;
import com.linkedin.structured.StructuredPropertyDefinition;
import com.linkedin.structured.StructuredPropertySettings;
import com.linkedin.structured.StructuredPropertyValueAssignment;
import com.linkedin.structured.StructuredPropertyValueAssignmentArray;
import com.linkedin.util.Pair;
@ -32,6 +33,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -45,6 +47,11 @@ public class StructuredPropertyUtils {
static final Date MIN_DATE = Date.valueOf("1000-01-01");
static final Date MAX_DATE = Date.valueOf("9999-12-31");
public static final String INVALID_SETTINGS_MESSAGE =
"Cannot have property isHidden = true while other display location settings are also true.";
public static final String ONLY_ONE_BADGE =
"Cannot have more than one property set with show as badge. Property urns currently set: ";
public static LogicalValueType getLogicalValueType(
StructuredPropertyDefinition structuredPropertyDefinition) {
return getLogicalValueType(structuredPropertyDefinition.getValueType());
@ -355,4 +362,47 @@ public class StructuredPropertyUtils {
true);
}
}
/*
* We accept both ID and qualifiedName as inputs when creating a structured property. However,
* these two fields should ALWAYS be the same. If they don't provide either, use a UUID for both.
* If they provide both, ensure they are the same otherwise throw. Otherwise, use what is provided.
*/
public static String getPropertyId(
@Nullable final String inputId, @Nullable final String inputQualifiedName) {
if (inputId != null && inputQualifiedName != null && !inputId.equals(inputQualifiedName)) {
throw new IllegalArgumentException(
"Qualified name and the ID of a structured property must match");
}
String id = UUID.randomUUID().toString();
if (inputQualifiedName != null) {
id = inputQualifiedName;
} else if (inputId != null) {
id = inputId;
}
return id;
}
/*
* Ensure that a structured property settings aspect is valid by ensuring that if isHidden is true,
* the other fields concerning display locations are false;
*/
public static boolean validatePropertySettings(
StructuredPropertySettings settings, boolean shouldThrow) {
if (settings.isIsHidden()) {
if (settings.isShowInSearchFilters()
|| settings.isShowInAssetSummary()
|| settings.isShowAsAssetBadge()) {
if (shouldThrow) {
throw new IllegalArgumentException(INVALID_SETTINGS_MESSAGE);
} else {
return false;
}
}
}
return true;
}
}

View File

@ -363,6 +363,8 @@ public class Constants {
// Structured Property
public static final String STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME = "propertyDefinition";
public static final String STRUCTURED_PROPERTY_KEY_ASPECT_NAME = "structuredPropertyKey";
public static final String STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME =
"structuredPropertySettings";
// Form
public static final String FORM_INFO_ASPECT_NAME = "formInfo";

View File

@ -118,11 +118,13 @@ class StructuredProperties(ConfigModel):
@property
def fqn(self) -> str:
assert self.urn is not None
return (
self.qualified_name
or self.id
or Urn.from_string(self.urn).get_entity_id()[0]
)
id = Urn.create_from_string(self.urn).get_entity_id()[0]
if self.qualified_name is not None:
# ensure that qualified name and ID match
assert (
self.qualified_name == id
), "ID in the urn and the qualified_name must match"
return id
@validator("urn", pre=True, always=True)
def urn_must_be_present(cls, v, values):

View File

@ -100,7 +100,7 @@ record StructuredPropertyDefinition {
created: optional AuditStamp
/**
* Created Audit stamp
* Last Modified Audit stamp
*/
@Searchable = {
"/time": {

View File

@ -0,0 +1,64 @@
namespace com.linkedin.structured
import com.linkedin.common.AuditStamp
/**
* Settings specific to a structured property entity
*/
@Aspect = {
"name": "structuredPropertySettings"
}
record StructuredPropertySettings {
/**
* Whether or not this asset should be hidden in the main application
*/
@Searchable = {
"fieldType": "BOOLEAN"
}
isHidden: boolean = false
/**
* Whether or not this asset should be displayed as a search filter
*/
@Searchable = {
"fieldType": "BOOLEAN"
}
showInSearchFilters: boolean = false
/**
* Whether or not this asset should be displayed in the asset sidebar
*/
@Searchable = {
"fieldType": "BOOLEAN"
}
showInAssetSummary: boolean = false
/**
* Whether or not this asset should be displayed as an asset badge on other
* asset's headers
*/
@Searchable = {
"fieldType": "BOOLEAN"
}
showAsAssetBadge: boolean = false
/**
* Whether or not this asset should be displayed as a column in the schema field table
* in a Dataset's "Columns" tab.
*/
@Searchable = {
"fieldType": "BOOLEAN"
}
showInColumnsTable: boolean = false
/**
* Last Modified Audit stamp
*/
@Searchable = {
"/time": {
"fieldName": "lastModifiedSettings",
"fieldType": "DATETIME"
}
}
lastModified: optional AuditStamp
}

View File

@ -602,6 +602,7 @@ entities:
keyAspect: structuredPropertyKey
aspects:
- propertyDefinition
- structuredPropertySettings
- institutionalMemory
- status
- name: form