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; package com.linkedin.datahub.graphql.resolvers.structuredproperties;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; 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.common.urn.Urn;
import com.linkedin.data.template.SetMode; 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.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateStructuredPropertyInput; import com.linkedin.datahub.graphql.generated.CreateStructuredPropertyInput;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity; 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.datahub.graphql.types.structuredproperty.StructuredPropertyMapper;
import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient; import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.aspect.patch.builder.StructuredPropertyDefinitionPatchBuilder; import com.linkedin.metadata.aspect.patch.builder.StructuredPropertyDefinitionPatchBuilder;
import com.linkedin.metadata.models.StructuredPropertyUtils;
import com.linkedin.metadata.utils.EntityKeyUtils; import com.linkedin.metadata.utils.EntityKeyUtils;
import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.structured.PrimitivePropertyValue; import com.linkedin.structured.PrimitivePropertyValue;
import com.linkedin.structured.PropertyCardinality; import com.linkedin.structured.PropertyCardinality;
import com.linkedin.structured.PropertyValue; import com.linkedin.structured.PropertyValue;
import com.linkedin.structured.StructuredPropertyKey; import com.linkedin.structured.StructuredPropertyKey;
import com.linkedin.structured.StructuredPropertySettings;
import graphql.schema.DataFetcher; import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -54,40 +59,28 @@ public class CreateStructuredPropertyResolver
"Unable to create structured property. Please contact your admin."); "Unable to create structured property. Please contact your admin.");
} }
final StructuredPropertyKey key = new StructuredPropertyKey(); 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); key.setId(id);
final Urn propertyUrn = final Urn propertyUrn =
EntityKeyUtils.convertEntityKeyToUrn(key, STRUCTURED_PROPERTY_ENTITY_NAME); EntityKeyUtils.convertEntityKeyToUrn(key, STRUCTURED_PROPERTY_ENTITY_NAME);
StructuredPropertyDefinitionPatchBuilder builder =
new StructuredPropertyDefinitionPatchBuilder().urn(propertyUrn);
builder.setQualifiedName(input.getQualifiedName()); if (_entityClient.exists(context.getOperationContext(), propertyUrn)) {
builder.setValueType(input.getValueType()); throw new IllegalArgumentException(
input.getEntityTypes().forEach(builder::addEntityType); "A structured property already exists with this urn");
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);
}
if (input.getAllowedValues() != null) {
buildAllowedValues(input, builder);
}
if (input.getCardinality() != null) {
builder.setCardinality(
PropertyCardinality.valueOf(input.getCardinality().toString()));
}
builder.setCreated(context.getOperationContext().getAuditStamp());
builder.setLastModified(context.getOperationContext().getAuditStamp());
MetadataChangeProposal mcp = builder.build(); List<MetadataChangeProposal> mcps = new ArrayList<>();
_entityClient.ingestProposal(context.getOperationContext(), mcp, false);
// 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 = EntityResponse response =
_entityClient.getV2( _entityClient.getV2(
@ -103,6 +96,72 @@ public class CreateStructuredPropertyResolver
}); });
} }
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(id);
builder.setValueType(input.getValueType());
input.getEntityTypes().forEach(builder::addEntityType);
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);
}
if (input.getAllowedValues() != null) {
buildAllowedValues(input, builder);
}
if (input.getCardinality() != null) {
builder.setCardinality(PropertyCardinality.valueOf(input.getCardinality().toString()));
}
builder.setCreated(context.getOperationContext().getAuditStamp());
builder.setLastModified(context.getOperationContext().getAuditStamp());
return builder.build();
}
private void buildTypeQualifier( private void buildTypeQualifier(
@Nonnull final CreateStructuredPropertyInput input, @Nonnull final CreateStructuredPropertyInput input,
@Nonnull final StructuredPropertyDefinitionPatchBuilder builder) { @Nonnull final StructuredPropertyDefinitionPatchBuilder builder) {

View File

@ -6,6 +6,7 @@ import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils; import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; 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.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.DeleteStructuredPropertyInput; import com.linkedin.datahub.graphql.generated.DeleteStructuredPropertyInput;
import com.linkedin.entity.client.EntityClient; 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."); "Unable to delete structured property. Please contact your admin.");
} }
_entityClient.deleteEntity(context.getOperationContext(), propertyUrn); _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; return true;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException( throw new RuntimeException(

View File

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

View File

@ -1,8 +1,8 @@
package com.linkedin.datahub.graphql.resolvers.structuredproperties; package com.linkedin.datahub.graphql.resolvers.structuredproperties;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME; import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_ENTITY_NAME; import static com.linkedin.metadata.Constants.*;
import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils; 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.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity; 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.generated.UpdateStructuredPropertyInput;
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertyMapper; import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertyMapper;
import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient; import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.aspect.patch.builder.StructuredPropertyDefinitionPatchBuilder; import com.linkedin.metadata.aspect.patch.builder.StructuredPropertyDefinitionPatchBuilder;
import com.linkedin.metadata.models.StructuredPropertyUtils;
import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.structured.PrimitivePropertyValue; import com.linkedin.structured.PrimitivePropertyValue;
import com.linkedin.structured.PropertyCardinality; import com.linkedin.structured.PropertyCardinality;
import com.linkedin.structured.PropertyValue; import com.linkedin.structured.PropertyValue;
import com.linkedin.structured.StructuredPropertyDefinition; import com.linkedin.structured.StructuredPropertyDefinition;
import com.linkedin.structured.StructuredPropertySettings;
import graphql.schema.DataFetcher; import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -57,36 +62,24 @@ public class UpdateStructuredPropertyResolver
"Unable to update structured property. Please contact your admin."); "Unable to update structured property. Please contact your admin.");
} }
final Urn propertyUrn = UrnUtils.getUrn(input.getUrn()); final Urn propertyUrn = UrnUtils.getUrn(input.getUrn());
StructuredPropertyDefinition existingDefinition = final EntityResponse entityResponse =
getExistingStructuredProperty(context, propertyUrn); getExistingStructuredProperty(context, propertyUrn);
StructuredPropertyDefinitionPatchBuilder builder =
new StructuredPropertyDefinitionPatchBuilder().urn(propertyUrn);
if (input.getDisplayName() != null) { List<MetadataChangeProposal> mcps = new ArrayList<>();
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());
MetadataChangeProposal mcp = builder.build(); // first update the definition aspect if we need to
_entityClient.ingestProposal(context.getOperationContext(), mcp, false); 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 = EntityResponse response =
_entityClient.getV2( _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( private void buildTypeQualifier(
@Nonnull final UpdateStructuredPropertyInput input, @Nonnull final UpdateStructuredPropertyInput input,
@Nonnull final StructuredPropertyDefinitionPatchBuilder builder, @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 { @Nonnull final QueryContext context, @Nonnull final Urn propertyUrn) throws Exception {
EntityResponse response = return _entityClient.getV2(
_entityClient.getV2( context.getOperationContext(), STRUCTURED_PROPERTY_ENTITY_NAME, propertyUrn, null);
context.getOperationContext(), STRUCTURED_PROPERTY_ENTITY_NAME, propertyUrn, null); }
private StructuredPropertyDefinition getExistingStructuredPropertyDefinition(
EntityResponse response) throws Exception {
if (response != null if (response != null
&& response.getAspects().containsKey(STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME)) { && response.getAspects().containsKey(STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME)) {
return new StructuredPropertyDefinition( 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( _entityClient.ingestProposal(
context.getOperationContext(), structuredPropertiesProposal, false); context.getOperationContext(), structuredPropertiesProposal, false);
return StructuredPropertiesMapper.map(context, structuredProperties); return StructuredPropertiesMapper.map(context, structuredProperties, assetUrn);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException( throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e); 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, STRUCTURED_PROPERTIES_ASPECT_NAME,
((chart, dataMap) -> ((chart, dataMap) ->
chart.setStructuredProperties( chart.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap))))); StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult( mappingHelper.mapToResult(
FORMS_ASPECT_NAME, FORMS_ASPECT_NAME,
((entity, dataMap) -> ((entity, dataMap) ->

View File

@ -161,7 +161,9 @@ public class ContainerMapper {
if (envelopedStructuredProps != null) { if (envelopedStructuredProps != null) {
result.setStructuredProperties( result.setStructuredProperties(
StructuredPropertiesMapper.map( StructuredPropertiesMapper.map(
context, new StructuredProperties(envelopedStructuredProps.getValue().data()))); context,
new StructuredProperties(envelopedStructuredProps.getValue().data()),
entityUrn));
} }
final EnvelopedAspect envelopedForms = aspects.get(FORMS_ASPECT_NAME); 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, STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) -> ((entity, dataMap) ->
entity.setStructuredProperties( entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap))))); StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult( mappingHelper.mapToResult(
FORMS_ASPECT_NAME, FORMS_ASPECT_NAME,
((entity, dataMap) -> ((entity, dataMap) ->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -71,7 +71,9 @@ public class DomainMapper {
if (envelopedStructuredProps != null) { if (envelopedStructuredProps != null) {
result.setStructuredProperties( result.setStructuredProperties(
StructuredPropertiesMapper.map( StructuredPropertiesMapper.map(
context, new StructuredProperties(envelopedStructuredProps.getValue().data()))); context,
new StructuredProperties(envelopedStructuredProps.getValue().data()),
entityUrn));
} }
final EnvelopedAspect envelopedForms = aspects.get(FORMS_ASPECT_NAME); 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, STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) -> ((entity, dataMap) ->
entity.setStructuredProperties( entity.setStructuredProperties(
StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap))))); StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult( mappingHelper.mapToResult(
FORMS_ASPECT_NAME, FORMS_ASPECT_NAME,
((entity, dataMap) -> ((entity, dataMap) ->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,23 +25,29 @@ public class StructuredPropertiesMapper {
public static final StructuredPropertiesMapper INSTANCE = new StructuredPropertiesMapper(); public static final StructuredPropertiesMapper INSTANCE = new StructuredPropertiesMapper();
public static com.linkedin.datahub.graphql.generated.StructuredProperties map( public static com.linkedin.datahub.graphql.generated.StructuredProperties map(
@Nullable QueryContext context, @Nonnull final StructuredProperties structuredProperties) { @Nullable QueryContext context,
return INSTANCE.apply(context, structuredProperties); @Nonnull final StructuredProperties structuredProperties,
@Nonnull final Urn entityUrn) {
return INSTANCE.apply(context, structuredProperties, entityUrn);
} }
public com.linkedin.datahub.graphql.generated.StructuredProperties apply( 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 = com.linkedin.datahub.graphql.generated.StructuredProperties result =
new com.linkedin.datahub.graphql.generated.StructuredProperties(); new com.linkedin.datahub.graphql.generated.StructuredProperties();
result.setProperties( result.setProperties(
structuredProperties.getProperties().stream() structuredProperties.getProperties().stream()
.map(p -> mapStructuredProperty(context, p)) .map(p -> mapStructuredProperty(context, p, entityUrn))
.collect(Collectors.toList())); .collect(Collectors.toList()));
return result; return result;
} }
private StructuredPropertiesEntry mapStructuredProperty( private StructuredPropertiesEntry mapStructuredProperty(
@Nullable QueryContext context, StructuredPropertyValueAssignment valueAssignment) { @Nullable QueryContext context,
StructuredPropertyValueAssignment valueAssignment,
@Nonnull final Urn entityUrn) {
StructuredPropertiesEntry entry = new StructuredPropertiesEntry(); StructuredPropertiesEntry entry = new StructuredPropertiesEntry();
entry.setStructuredProperty(createStructuredPropertyEntity(valueAssignment)); entry.setStructuredProperty(createStructuredPropertyEntity(valueAssignment));
final List<PropertyValue> values = new ArrayList<>(); final List<PropertyValue> values = new ArrayList<>();
@ -58,6 +64,7 @@ public class StructuredPropertiesMapper {
}); });
entry.setValues(values); entry.setValues(values);
entry.setValueEntities(entities); entry.setValueEntities(entities);
entry.setAssociatedUrn(entityUrn.toString());
return entry; 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.StringValue;
import com.linkedin.datahub.graphql.generated.StructuredPropertyDefinition; import com.linkedin.datahub.graphql.generated.StructuredPropertyDefinition;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity; 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.generated.TypeQualifier;
import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper;
import com.linkedin.datahub.graphql.types.mappers.MapperUtils; import com.linkedin.datahub.graphql.types.mappers.MapperUtils;
@ -55,6 +56,8 @@ public class StructuredPropertyMapper
MappingHelper<StructuredPropertyEntity> mappingHelper = new MappingHelper<>(aspectMap, result); MappingHelper<StructuredPropertyEntity> mappingHelper = new MappingHelper<>(aspectMap, result);
mappingHelper.mapToResult( mappingHelper.mapToResult(
STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME, (this::mapStructuredPropertyDefinition)); STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME, (this::mapStructuredPropertyDefinition));
mappingHelper.mapToResult(
STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME, (this::mapStructuredPropertySettings));
return mappingHelper.getResult(); return mappingHelper.getResult();
} }
@ -112,6 +115,21 @@ public class StructuredPropertyMapper
return allowedValues; 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) { private DataTypeEntity createDataTypeEntity(final Urn dataTypeUrn) {
final DataTypeEntity dataType = new DataTypeEntity(); final DataTypeEntity dataType = new DataTypeEntity();
dataType.setUrn(dataTypeUrn.toString()); dataType.setUrn(dataTypeUrn.toString());

View File

@ -27,7 +27,8 @@ public class StructuredPropertyType
implements com.linkedin.datahub.graphql.types.EntityType<StructuredPropertyEntity, String> { implements com.linkedin.datahub.graphql.types.EntityType<StructuredPropertyEntity, String> {
public static final Set<String> ASPECTS_TO_FETCH = 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; private final EntityClient _entityClient;
@Override @Override

View File

@ -49,6 +49,11 @@ type StructuredPropertyEntity implements Entity {
""" """
definition: StructuredPropertyDefinition! definition: StructuredPropertyDefinition!
"""
Definition of this structured property including its name
"""
settings: StructuredPropertySettings
""" """
Granular API for querying edges extending from this entity Granular API for querying edges extending from this entity
""" """
@ -117,6 +122,36 @@ type StructuredPropertyDefinition {
lastModified: ResolvedAuditStamp 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 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 The optional entities associated with the values if the values are entity urns
""" """
valueEntities: [Entity] 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. 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 The optional display name for this property
@ -375,6 +416,11 @@ input CreateStructuredPropertyInput {
For example: ["urn:li:entityType:datahub.dataset"] For example: ["urn:li:entityType:datahub.dataset"]
""" """
entityTypes: [String!]! entityTypes: [String!]!
"""
Settings for this structured property
"""
settings: StructuredPropertySettingsInput
} }
""" """
@ -455,6 +501,11 @@ input UpdateStructuredPropertyInput {
For backwards compatibility, this is append only. For backwards compatibility, this is append only.
""" """
newEntityTypes: [String!] newEntityTypes: [String!]
"""
Settings for this structured property
"""
settings: StructuredPropertySettingsInput
} }
""" """
@ -477,3 +528,34 @@ input DeleteStructuredPropertyInput {
""" """
urn: String! 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.QueryContext;
import com.linkedin.datahub.graphql.generated.CreateStructuredPropertyInput; import com.linkedin.datahub.graphql.generated.CreateStructuredPropertyInput;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity; import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity;
import com.linkedin.datahub.graphql.generated.StructuredPropertySettingsInput;
import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspectMap; import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.entity.client.EntityClient; import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants; import com.linkedin.metadata.Constants;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.r2.RemoteInvocationException; import com.linkedin.r2.RemoteInvocationException;
import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList; import java.util.ArrayList;
@ -36,7 +36,8 @@ public class CreateStructuredPropertyResolverTest {
null, null,
null, null,
null, null,
new ArrayList<>()); new ArrayList<>(),
null);
@Test @Test
public void testGetSuccess() throws Exception { public void testGetSuccess() throws Exception {
@ -56,7 +57,40 @@ public class CreateStructuredPropertyResolverTest {
// Validate that we called ingest // Validate that we called ingest
Mockito.verify(mockEntityClient, Mockito.times(1)) 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 @Test
@ -75,7 +109,7 @@ public class CreateStructuredPropertyResolverTest {
// Validate that we did NOT call ingest // Validate that we did NOT call ingest
Mockito.verify(mockEntityClient, Mockito.times(0)) Mockito.verify(mockEntityClient, Mockito.times(0))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false)); .batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
} }
@Test @Test
@ -94,7 +128,83 @@ public class CreateStructuredPropertyResolverTest {
// Validate that ingest was called, but that caused a failure // Validate that ingest was called, but that caused a failure
Mockito.verify(mockEntityClient, Mockito.times(1)) 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 { 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.getMockAllowContext;
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; 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.mockito.ArgumentMatchers.any;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertThrows;
import com.linkedin.common.UrnArray;
import com.linkedin.common.urn.UrnUtils; import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity; 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.generated.UpdateStructuredPropertyInput;
import com.linkedin.entity.Aspect;
import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspect;
import com.linkedin.entity.EnvelopedAspectMap; import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.entity.client.EntityClient; import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants; import com.linkedin.metadata.Constants;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.r2.RemoteInvocationException; import com.linkedin.r2.RemoteInvocationException;
import com.linkedin.structured.StructuredPropertyDefinition;
import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import org.mockito.Mockito; import org.mockito.Mockito;
@ -33,6 +38,7 @@ public class UpdateStructuredPropertyResolverTest {
null, null,
null, null,
null, null,
null,
null); null);
@Test @Test
@ -53,7 +59,7 @@ public class UpdateStructuredPropertyResolverTest {
// Validate that we called ingest // Validate that we called ingest
Mockito.verify(mockEntityClient, Mockito.times(1)) Mockito.verify(mockEntityClient, Mockito.times(1))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false)); .batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
} }
@Test @Test
@ -72,7 +78,7 @@ public class UpdateStructuredPropertyResolverTest {
// Validate that we did NOT call ingest // Validate that we did NOT call ingest
Mockito.verify(mockEntityClient, Mockito.times(0)) Mockito.verify(mockEntityClient, Mockito.times(0))
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false)); .batchIngestProposals(any(), Mockito.anyList(), Mockito.eq(false));
} }
@Test @Test
@ -91,7 +97,80 @@ public class UpdateStructuredPropertyResolverTest {
// Validate that ingest was not called since there was a get failure before ingesting // Validate that ingest was not called since there was a get failure before ingesting
Mockito.verify(mockEntityClient, Mockito.times(0)) 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 { private EntityClient initMockEntityClient(boolean shouldSucceed) throws Exception {
@ -99,7 +178,11 @@ public class UpdateStructuredPropertyResolverTest {
EntityResponse response = new EntityResponse(); EntityResponse response = new EntityResponse();
response.setEntityName(Constants.STRUCTURED_PROPERTY_ENTITY_NAME); response.setEntityName(Constants.STRUCTURED_PROPERTY_ENTITY_NAME);
response.setUrn(UrnUtils.getUrn(TEST_STRUCTURED_PROPERTY_URN)); 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) { if (shouldSucceed) {
Mockito.when( Mockito.when(
client.getV2( client.getV2(
@ -120,4 +203,13 @@ public class UpdateStructuredPropertyResolverTest {
return client; 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.PrimitivePropertyValue;
import com.linkedin.structured.StructuredProperties; import com.linkedin.structured.StructuredProperties;
import com.linkedin.structured.StructuredPropertyDefinition; import com.linkedin.structured.StructuredPropertyDefinition;
import com.linkedin.structured.StructuredPropertySettings;
import com.linkedin.structured.StructuredPropertyValueAssignment; import com.linkedin.structured.StructuredPropertyValueAssignment;
import com.linkedin.structured.StructuredPropertyValueAssignmentArray; import com.linkedin.structured.StructuredPropertyValueAssignmentArray;
import com.linkedin.util.Pair; import com.linkedin.util.Pair;
@ -32,6 +33,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -45,6 +47,11 @@ public class StructuredPropertyUtils {
static final Date MIN_DATE = Date.valueOf("1000-01-01"); static final Date MIN_DATE = Date.valueOf("1000-01-01");
static final Date MAX_DATE = Date.valueOf("9999-12-31"); 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( public static LogicalValueType getLogicalValueType(
StructuredPropertyDefinition structuredPropertyDefinition) { StructuredPropertyDefinition structuredPropertyDefinition) {
return getLogicalValueType(structuredPropertyDefinition.getValueType()); return getLogicalValueType(structuredPropertyDefinition.getValueType());
@ -355,4 +362,47 @@ public class StructuredPropertyUtils {
true); 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 // Structured Property
public static final String STRUCTURED_PROPERTY_DEFINITION_ASPECT_NAME = "propertyDefinition"; 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_KEY_ASPECT_NAME = "structuredPropertyKey";
public static final String STRUCTURED_PROPERTY_SETTINGS_ASPECT_NAME =
"structuredPropertySettings";
// Form // Form
public static final String FORM_INFO_ASPECT_NAME = "formInfo"; public static final String FORM_INFO_ASPECT_NAME = "formInfo";

View File

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

View File

@ -89,25 +89,25 @@ record StructuredPropertyDefinition {
version: optional string version: optional string
/** /**
* Created Audit stamp * Created Audit stamp
*/ */
@Searchable = { @Searchable = {
"/time": { "/time": {
"fieldName": "createdTime", "fieldName": "createdTime",
"fieldType": "DATETIME" "fieldType": "DATETIME"
} }
} }
created: optional AuditStamp created: optional AuditStamp
/** /**
* Created Audit stamp * Last Modified Audit stamp
*/ */
@Searchable = { @Searchable = {
"/time": { "/time": {
"fieldName": "lastModified", "fieldName": "lastModified",
"fieldType": "DATETIME" "fieldType": "DATETIME"
} }
} }
lastModified: optional AuditStamp lastModified: optional AuditStamp
} }

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 keyAspect: structuredPropertyKey
aspects: aspects:
- propertyDefinition - propertyDefinition
- structuredPropertySettings
- institutionalMemory - institutionalMemory
- status - status
- name: form - name: form