mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-07 15:05:08 +00:00
feat(Tags/Terms): Backend support for tag & term mutations (#4096)
This commit is contained in:
parent
4dba8fe6e7
commit
f2f5443d04
@ -101,6 +101,7 @@ import com.linkedin.datahub.graphql.resolvers.search.AutoCompleteForMultipleReso
|
||||
import com.linkedin.datahub.graphql.resolvers.search.AutoCompleteResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.search.SearchAcrossEntitiesResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.search.SearchResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.tag.SetTagColorResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.type.AspectInterfaceTypeResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.type.EntityInterfaceTypeResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.type.HyperParameterValueTypeResolver;
|
||||
@ -589,6 +590,7 @@ public class GmsGraphQLEngine {
|
||||
builder.type("Mutation", typeWiring -> typeWiring
|
||||
.dataFetcher("updateDataset", new AuthenticatedResolver<>(new MutableTypeResolver<>(datasetType)))
|
||||
.dataFetcher("updateTag", new AuthenticatedResolver<>(new MutableTypeResolver<>(tagType)))
|
||||
.dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService))
|
||||
.dataFetcher("updateChart", new AuthenticatedResolver<>(new MutableTypeResolver<>(chartType)))
|
||||
.dataFetcher("updateDashboard", new AuthenticatedResolver<>(new MutableTypeResolver<>(dashboardType)))
|
||||
.dataFetcher("updateDataJob", new AuthenticatedResolver<>(new MutableTypeResolver<>(dataJobType)))
|
||||
|
@ -10,11 +10,13 @@ import com.linkedin.datahub.graphql.authorization.ConjunctivePrivilegeGroup;
|
||||
import com.linkedin.datahub.graphql.authorization.DisjunctivePrivilegeGroup;
|
||||
import com.linkedin.datahub.graphql.generated.SubResourceType;
|
||||
import com.linkedin.domain.DomainProperties;
|
||||
import com.linkedin.glossary.GlossaryTermInfo;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.authorization.PoliciesConfig;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.schema.EditableSchemaFieldInfo;
|
||||
import com.linkedin.schema.EditableSchemaMetadata;
|
||||
import com.linkedin.tag.TagProperties;
|
||||
import javax.annotation.Nonnull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@ -69,11 +71,49 @@ public class DescriptionUtils {
|
||||
) {
|
||||
DomainProperties domainProperties =
|
||||
(DomainProperties) getAspectFromEntity(
|
||||
resourceUrn.toString(), Constants.DOMAIN_PROPERTIES_ASPECT_NAME, entityService, new DomainProperties());
|
||||
resourceUrn.toString(), Constants.DOMAIN_PROPERTIES_ASPECT_NAME, entityService, null);
|
||||
if (domainProperties == null) {
|
||||
// If there are no properties for the domain already, then we should throw since the properties model also requires a name.
|
||||
throw new IllegalArgumentException("Properties for this Domain do not yet exist!");
|
||||
}
|
||||
domainProperties.setDescription(newDescription);
|
||||
persistAspect(resourceUrn, Constants.DOMAIN_PROPERTIES_ASPECT_NAME, domainProperties, actor, entityService);
|
||||
}
|
||||
|
||||
public static void updateTagDescription(
|
||||
String newDescription,
|
||||
Urn resourceUrn,
|
||||
Urn actor,
|
||||
EntityService entityService
|
||||
) {
|
||||
TagProperties tagProperties =
|
||||
(TagProperties) getAspectFromEntity(
|
||||
resourceUrn.toString(), Constants.TAG_PROPERTIES_ASPECT_NAME, entityService, null);
|
||||
if (tagProperties == null) {
|
||||
// If there are no properties for the tag already, then we should throw since the properties model also requires a name.
|
||||
throw new IllegalArgumentException("Properties for this Tag do not yet exist!");
|
||||
}
|
||||
tagProperties.setDescription(newDescription);
|
||||
persistAspect(resourceUrn, Constants.TAG_PROPERTIES_ASPECT_NAME, tagProperties, actor, entityService);
|
||||
}
|
||||
|
||||
public static void updateGlossaryTermDescription(
|
||||
String newDescription,
|
||||
Urn resourceUrn,
|
||||
Urn actor,
|
||||
EntityService entityService
|
||||
) {
|
||||
GlossaryTermInfo glossaryTermInfo =
|
||||
(GlossaryTermInfo) getAspectFromEntity(
|
||||
resourceUrn.toString(), Constants.GLOSSARY_TERM_INFO_ASPECT_NAME, entityService, null);
|
||||
if (glossaryTermInfo == null) {
|
||||
// If there are no properties for the term already, then we should throw since the properties model also requires a name.
|
||||
throw new IllegalArgumentException("Properties for this Glossary Term do not yet exist!");
|
||||
}
|
||||
glossaryTermInfo.setDefinition(newDescription); // We call description 'definition' for glossary terms. Not great, we know. :(
|
||||
persistAspect(resourceUrn, Constants.GLOSSARY_TERM_INFO_ASPECT_NAME, glossaryTermInfo, actor, entityService);
|
||||
}
|
||||
|
||||
public static Boolean validateFieldDescriptionInput(
|
||||
Urn resourceUrn,
|
||||
String subResource,
|
||||
@ -111,6 +151,16 @@ public class DescriptionUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Boolean validateLabelInput(
|
||||
Urn resourceUrn,
|
||||
EntityService entityService
|
||||
) {
|
||||
if (!entityService.exists(resourceUrn)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to update %s. %s does not exist.", resourceUrn, resourceUrn));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isAuthorizedToUpdateFieldDescription(@Nonnull QueryContext context, Urn targetUrn) {
|
||||
final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of(
|
||||
ALL_PRIVILEGES_GROUP,
|
||||
@ -152,4 +202,18 @@ public class DescriptionUtils {
|
||||
targetUrn.toString(),
|
||||
orPrivilegeGroups);
|
||||
}
|
||||
|
||||
public static boolean isAuthorizedToUpdateDescription(@Nonnull QueryContext context, Urn targetUrn) {
|
||||
final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of(
|
||||
ALL_PRIVILEGES_GROUP,
|
||||
new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_ENTITY_DOCS_PRIVILEGE.getType()))
|
||||
));
|
||||
|
||||
return AuthorizationUtils.isAuthorized(
|
||||
context.getAuthorizer(),
|
||||
context.getActorUrn(),
|
||||
targetUrn.getEntityType(),
|
||||
targetUrn.toString(),
|
||||
orPrivilegeGroups);
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,10 @@ public class UpdateDescriptionResolver implements DataFetcher<CompletableFuture<
|
||||
return updateContainerDescription(targetUrn, input, environment.getContext());
|
||||
case Constants.DOMAIN_ENTITY_NAME:
|
||||
return updateDomainDescription(targetUrn, input, environment.getContext());
|
||||
case Constants.GLOSSARY_TERM_ENTITY_NAME:
|
||||
return updateGlossaryTermDescription(targetUrn, input, environment.getContext());
|
||||
case Constants.TAG_ENTITY_NAME:
|
||||
return updateTagDescription(targetUrn, input, environment.getContext());
|
||||
default:
|
||||
throw new RuntimeException(
|
||||
String.format("Failed to update description. Unsupported resource type %s provided.", targetUrn));
|
||||
@ -85,8 +89,8 @@ public class UpdateDescriptionResolver implements DataFetcher<CompletableFuture<
|
||||
log.error("Failed to perform update against input {}, {}", input.toString(), e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to perform update against input %s", input.toString()), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Boolean> updateDatasetDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) {
|
||||
|
||||
@ -115,4 +119,52 @@ public class UpdateDescriptionResolver implements DataFetcher<CompletableFuture<
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Boolean> updateTagDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
|
||||
if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn)) {
|
||||
throw new AuthorizationException(
|
||||
"Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
}
|
||||
DescriptionUtils.validateLabelInput(targetUrn, _entityService);
|
||||
|
||||
try {
|
||||
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
|
||||
DescriptionUtils.updateTagDescription(
|
||||
input.getDescription(),
|
||||
targetUrn,
|
||||
actor,
|
||||
_entityService);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform update against input {}, {}", input.toString(), e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to perform update against input %s", input.toString()), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Boolean> updateGlossaryTermDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
|
||||
if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn)) {
|
||||
throw new AuthorizationException(
|
||||
"Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
}
|
||||
DescriptionUtils.validateLabelInput(targetUrn, _entityService);
|
||||
|
||||
try {
|
||||
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
|
||||
DescriptionUtils.updateGlossaryTermDescription(
|
||||
input.getDescription(),
|
||||
targetUrn,
|
||||
actor,
|
||||
_entityService);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform update against input {}, {}", input.toString(), e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to perform update against input %s", input.toString()), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.tag;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
|
||||
import com.linkedin.datahub.graphql.authorization.ConjunctivePrivilegeGroup;
|
||||
import com.linkedin.datahub.graphql.authorization.DisjunctivePrivilegeGroup;
|
||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.resolvers.AuthUtils;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.events.metadata.ChangeType;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.authorization.PoliciesConfig;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.utils.GenericAspectUtils;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import com.linkedin.tag.TagProperties;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.annotation.Nonnull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;
|
||||
|
||||
|
||||
/**
|
||||
* Resolver used for updating the Domain associated with a Metadata Asset. Requires the EDIT_DOMAINS privilege for a particular asset.
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class SetTagColorResolver implements DataFetcher<CompletableFuture<Boolean>> {
|
||||
|
||||
private final EntityClient _entityClient;
|
||||
private final EntityService _entityService; // TODO: Remove this when 'exists' added to EntityClient
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
|
||||
|
||||
final QueryContext context = environment.getContext();
|
||||
final Urn tagUrn = Urn.createFromString(environment.getArgument("urn"));
|
||||
final String colorHex = environment.getArgument("colorHex");
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
|
||||
// If user is not authorized, then throw exception.
|
||||
if (!isAuthorizedToSetTagColor(environment.getContext(), tagUrn)) {
|
||||
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
}
|
||||
|
||||
// If tag does not exist, then throw exception.
|
||||
if (!_entityService.exists(tagUrn)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Failed to set Tag %s color. Tag does not exist.", tagUrn));
|
||||
}
|
||||
|
||||
try {
|
||||
TagProperties tagProperties = (TagProperties) getAspectFromEntity(
|
||||
tagUrn.toString(),
|
||||
Constants.TAG_PROPERTIES_ASPECT_NAME,
|
||||
_entityService,
|
||||
null);
|
||||
|
||||
if (tagProperties == null) {
|
||||
throw new IllegalArgumentException("Failed to set tag color. Tag properties does not yet exist!");
|
||||
}
|
||||
|
||||
tagProperties.setColorHex(colorHex);
|
||||
|
||||
// Update the TagProperties aspect.
|
||||
final MetadataChangeProposal proposal = new MetadataChangeProposal();
|
||||
proposal.setEntityUrn(tagUrn);
|
||||
proposal.setEntityType(tagUrn.getEntityType());
|
||||
proposal.setAspectName(Constants.TAG_PROPERTIES_ASPECT_NAME);
|
||||
proposal.setAspect(GenericAspectUtils.serializeAspect(tagProperties));
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
_entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to set color for Tag with urn {}: {}", tagUrn, e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to set color for Tag with urn %s", tagUrn), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isAuthorizedToSetTagColor(@Nonnull QueryContext context, Urn entityUrn) {
|
||||
final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of(
|
||||
AuthUtils.ALL_PRIVILEGES_GROUP,
|
||||
new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_TAG_COLOR_PRIVILEGE.getType()))
|
||||
));
|
||||
|
||||
return AuthorizationUtils.isAuthorized(
|
||||
context.getAuthorizer(),
|
||||
context.getActorUrn(),
|
||||
entityUrn.getEntityType(),
|
||||
entityUrn.toString(),
|
||||
orPrivilegeGroups);
|
||||
}
|
||||
}
|
@ -23,7 +23,11 @@ public class GlossaryTermInfoMapper implements ModelMapper<com.linkedin.glossary
|
||||
public GlossaryTermInfo apply(@Nonnull final com.linkedin.glossary.GlossaryTermInfo glossaryTermInfo) {
|
||||
com.linkedin.datahub.graphql.generated.GlossaryTermInfo glossaryTermInfoResult = new com.linkedin.datahub.graphql.generated.GlossaryTermInfo();
|
||||
glossaryTermInfoResult.setDefinition(glossaryTermInfo.getDefinition());
|
||||
glossaryTermInfoResult.setDescription(glossaryTermInfo.getDefinition());
|
||||
glossaryTermInfoResult.setTermSource(glossaryTermInfo.getTermSource());
|
||||
if (glossaryTermInfo.hasName()) {
|
||||
glossaryTermInfoResult.setName(glossaryTermInfo.getName());
|
||||
}
|
||||
if (glossaryTermInfo.hasSourceRef()) {
|
||||
glossaryTermInfoResult.setSourceRef(glossaryTermInfo.getSourceRef());
|
||||
}
|
||||
|
@ -34,14 +34,20 @@ public class GlossaryTermMapper implements ModelMapper<EntityResponse, GlossaryT
|
||||
final GlossaryTerm result = new GlossaryTerm();
|
||||
result.setUrn(entityResponse.getUrn().toString());
|
||||
result.setType(EntityType.GLOSSARY_TERM);
|
||||
|
||||
entityResponse.getAspects().forEach((name, aspect) -> {
|
||||
DataMap data = aspect.getValue().data();
|
||||
if (GLOSSARY_TERM_KEY_ASPECT_NAME.equals(name)) {
|
||||
final GlossaryTermKey gmsKey = new GlossaryTermKey(data);
|
||||
result.setName(GlossaryTermUtils.getGlossaryTermName(gmsKey.getName()));
|
||||
// Construct the legacy name from the URN itself.
|
||||
final String legacyName = GlossaryTermUtils.getGlossaryTermName(entityResponse.getUrn().getId());
|
||||
result.setName(legacyName);
|
||||
result.setHierarchicalName(gmsKey.getName());
|
||||
} else if (GLOSSARY_TERM_INFO_ASPECT_NAME.equals(name)) {
|
||||
// Set deprecation info field.
|
||||
result.setGlossaryTermInfo(GlossaryTermInfoMapper.map(new GlossaryTermInfo(data)));
|
||||
// Set new properties field.
|
||||
result.setProperties(GlossaryTermPropertiesMapper.map(new GlossaryTermInfo(data)));
|
||||
} else if (OWNERSHIP_ASPECT_NAME.equals(name)) {
|
||||
result.setOwnership(OwnershipMapper.map(new Ownership(data)));
|
||||
} else if (DEPRECATION_ASPECT_NAME.equals(name)) {
|
||||
|
@ -0,0 +1,42 @@
|
||||
package com.linkedin.datahub.graphql.types.glossary.mappers;
|
||||
|
||||
import com.linkedin.datahub.graphql.generated.GlossaryTermProperties;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.StringMapMapper;
|
||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
||||
|
||||
/**
|
||||
* Maps Pegasus {@link RecordTemplate} objects to objects conforming to the GQL schema.
|
||||
*
|
||||
* To be replaced by auto-generated mappers implementations
|
||||
*/
|
||||
public class GlossaryTermPropertiesMapper implements ModelMapper<com.linkedin.glossary.GlossaryTermInfo, GlossaryTermProperties> {
|
||||
|
||||
public static final GlossaryTermPropertiesMapper INSTANCE = new GlossaryTermPropertiesMapper();
|
||||
|
||||
public static GlossaryTermProperties map(@Nonnull final com.linkedin.glossary.GlossaryTermInfo glossaryTermInfo) {
|
||||
return INSTANCE.apply(glossaryTermInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlossaryTermProperties apply(@Nonnull final com.linkedin.glossary.GlossaryTermInfo glossaryTermInfo) {
|
||||
com.linkedin.datahub.graphql.generated.GlossaryTermProperties result = new com.linkedin.datahub.graphql.generated.GlossaryTermProperties();
|
||||
result.setDefinition(glossaryTermInfo.getDefinition());
|
||||
result.setDescription(glossaryTermInfo.getDefinition());
|
||||
result.setTermSource(glossaryTermInfo.getTermSource());
|
||||
if (glossaryTermInfo.hasName()) {
|
||||
result.setName(glossaryTermInfo.getName());
|
||||
}
|
||||
if (glossaryTermInfo.hasSourceRef()) {
|
||||
result.setSourceRef(glossaryTermInfo.getSourceRef());
|
||||
}
|
||||
if (glossaryTermInfo.hasSourceUrl()) {
|
||||
result.setSourceUrl(glossaryTermInfo.getSourceUrl().toString());
|
||||
}
|
||||
if (glossaryTermInfo.hasCustomProperties()) {
|
||||
result.setCustomProperties(StringMapMapper.map(glossaryTermInfo.getCustomProperties()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package com.linkedin.datahub.graphql.types.tag.mappers;
|
||||
|
||||
import com.datahub.util.ModelUtils;
|
||||
import com.linkedin.common.Ownership;
|
||||
import com.linkedin.data.template.GetMode;
|
||||
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||
import com.linkedin.datahub.graphql.generated.Tag;
|
||||
import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper;
|
||||
@ -33,7 +34,10 @@ public class TagSnapshotMapper implements ModelMapper<TagSnapshot, Tag> {
|
||||
|
||||
ModelUtils.getAspectsFromSnapshot(tag).forEach(aspect -> {
|
||||
if (aspect instanceof TagProperties) {
|
||||
if (TagProperties.class.cast(aspect).hasDescription()) {
|
||||
final TagProperties properties = (TagProperties) aspect;
|
||||
result.setProperties(mapTagProperties(properties));
|
||||
// Set deprecated top-level description field.
|
||||
if (properties.hasDescription()) {
|
||||
result.setDescription(TagProperties.class.cast(aspect).getDescription());
|
||||
}
|
||||
} else if (aspect instanceof Ownership) {
|
||||
@ -42,4 +46,12 @@ public class TagSnapshotMapper implements ModelMapper<TagSnapshot, Tag> {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private com.linkedin.datahub.graphql.generated.TagProperties mapTagProperties(final TagProperties gmsProperties) {
|
||||
final com.linkedin.datahub.graphql.generated.TagProperties result = new com.linkedin.datahub.graphql.generated.TagProperties();
|
||||
result.setName(gmsProperties.getName(GetMode.DEFAULT));
|
||||
result.setDescription(gmsProperties.getDescription(GetMode.DEFAULT));
|
||||
result.setColorHex(gmsProperties.getColorHex(GetMode.DEFAULT));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -155,6 +155,11 @@ type Mutation {
|
||||
"""
|
||||
updateTag(urn: String!, input: TagUpdateInput!): Tag
|
||||
|
||||
"""
|
||||
Set the hex color associated with an existing Tag
|
||||
"""
|
||||
setTagColor(urn: String!, colorHex: String!): Boolean
|
||||
|
||||
"""
|
||||
Create a policy and returns the resulting urn
|
||||
"""
|
||||
@ -793,7 +798,7 @@ type GlossaryTerm implements Entity {
|
||||
urn: String!
|
||||
|
||||
"""
|
||||
Ownership metadata of the dataset
|
||||
Ownership metadata of the glossary term
|
||||
"""
|
||||
ownership: Ownership
|
||||
|
||||
@ -803,9 +808,9 @@ type GlossaryTerm implements Entity {
|
||||
type: EntityType!
|
||||
|
||||
"""
|
||||
Display name of the glossary term
|
||||
Name / id of the glossary term
|
||||
"""
|
||||
name: String!
|
||||
name: String! @deprecated
|
||||
|
||||
"""
|
||||
hierarchicalName of glossary term
|
||||
@ -846,9 +851,19 @@ Information about a glossary term
|
||||
"""
|
||||
type GlossaryTermInfo {
|
||||
"""
|
||||
Definition of the glossary term
|
||||
The name of the Glossary Term
|
||||
"""
|
||||
definition: String!
|
||||
name: String
|
||||
|
||||
"""
|
||||
Description of the glossary term
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Definition of the glossary term. Deprecated - Use 'description' instead.
|
||||
"""
|
||||
definition: String! @deprecated
|
||||
|
||||
"""
|
||||
Term Source of the glossary term
|
||||
@ -881,9 +896,19 @@ Additional read only properties about a Glossary Term
|
||||
"""
|
||||
type GlossaryTermProperties {
|
||||
"""
|
||||
Definition of the glossary term
|
||||
The name of the Glossary Term
|
||||
"""
|
||||
definition: String!
|
||||
name: String
|
||||
|
||||
"""
|
||||
Description of the glossary term
|
||||
"""
|
||||
description: String!
|
||||
|
||||
"""
|
||||
Definition of the glossary term. Deprecated - Use 'description' instead.
|
||||
"""
|
||||
definition: String! @deprecated
|
||||
|
||||
"""
|
||||
Term Source of the glossary term
|
||||
@ -2500,14 +2525,20 @@ type Tag implements Entity {
|
||||
type: EntityType!
|
||||
|
||||
"""
|
||||
The display name of the tag
|
||||
The name / id of the tag. Use properties.name instead.
|
||||
"""
|
||||
name: String!
|
||||
name: String! @deprecated
|
||||
|
||||
"""
|
||||
Additional properties about the Tag
|
||||
"""
|
||||
properties: TagProperties
|
||||
|
||||
"""
|
||||
Additional read write properties about the Tag
|
||||
Deprecated! Use 'properties' field instead.
|
||||
"""
|
||||
editableProperties: EditableTagProperties
|
||||
editableProperties: EditableTagProperties @deprecated
|
||||
|
||||
"""
|
||||
Ownership metadata of the dataset
|
||||
@ -2520,22 +2551,48 @@ type Tag implements Entity {
|
||||
relationships(input: RelationshipsInput!): EntityRelationshipsResult
|
||||
|
||||
"""
|
||||
Deprecated, use editableProperties field instead
|
||||
Description of the tag
|
||||
Deprecated, use properties.description field instead
|
||||
"""
|
||||
description: String @deprecated
|
||||
}
|
||||
|
||||
"""
|
||||
Additional read write Tag properties
|
||||
Deprecated! Replaced by TagProperties.
|
||||
"""
|
||||
type EditableTagProperties {
|
||||
"""
|
||||
A display name for the Tag
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
A description of the Tag
|
||||
"""
|
||||
description: String
|
||||
}
|
||||
|
||||
"""
|
||||
Properties for a DataHub Tag
|
||||
"""
|
||||
type TagProperties {
|
||||
"""
|
||||
A display name for the Tag
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
A description of the Tag
|
||||
"""
|
||||
description: String
|
||||
|
||||
"""
|
||||
An optional RGB hex code for a Tag color, e.g. #FFFFFF
|
||||
"""
|
||||
colorHex: String
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
An edge between a Metadata Entity and a Tag Modeled as a struct to permit
|
||||
additional attributes
|
||||
|
@ -0,0 +1,186 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.tag;
|
||||
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
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.events.metadata.ChangeType;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.utils.GenericAspectUtils;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import com.linkedin.r2.RemoteInvocationException;
|
||||
import com.linkedin.tag.TagProperties;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.HashSet;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
|
||||
public class SetTagColorResolverTest {
|
||||
|
||||
private static final String TEST_ENTITY_URN = "urn:li:tag:test-tag";
|
||||
private static final String TEST_COLOR_HEX = "#FFFFFF";
|
||||
|
||||
@Test
|
||||
public void testGetSuccessExistingProperties() throws Exception {
|
||||
// Create resolver
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
// Test setting the domain
|
||||
final TagProperties oldTagProperties = new TagProperties().setName("Test Tag");
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN)),
|
||||
Mockito.eq(Constants.TAG_PROPERTIES_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(oldTagProperties);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
SetTagColorResolver resolver = new SetTagColorResolver(mockClient, mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_ENTITY_URN);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("colorHex"))).thenReturn(TEST_COLOR_HEX);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
final TagProperties newTagProperties = new TagProperties().setName("Test Tag").setColorHex(TEST_COLOR_HEX);
|
||||
final MetadataChangeProposal proposal = new MetadataChangeProposal();
|
||||
proposal.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN));
|
||||
proposal.setEntityType(Constants.TAG_ENTITY_NAME);
|
||||
proposal.setAspectName(Constants.TAG_PROPERTIES_ASPECT_NAME);
|
||||
proposal.setAspect(GenericAspectUtils.serializeAspect(newTagProperties));
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal),
|
||||
Mockito.any(Authentication.class)
|
||||
);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureNoExistingProperties() throws Exception {
|
||||
// Create resolver
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
// Test setting the domain
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN)),
|
||||
Mockito.eq(Constants.TAG_PROPERTIES_ASPECT_NAME),
|
||||
Mockito.eq(0)))
|
||||
.thenReturn(null);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
SetTagColorResolver resolver = new SetTagColorResolver(mockClient, mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_ENTITY_URN);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("colorHex"))).thenReturn(TEST_COLOR_HEX);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(Authentication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureTagDoesNotExist() throws Exception {
|
||||
// Create resolver
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
|
||||
// Test setting the domain
|
||||
final TagProperties oldTagProperties = new TagProperties().setName("Test Tag");
|
||||
final EnvelopedAspect oldTagPropertiesAspect = new EnvelopedAspect()
|
||||
.setName(Constants.TAG_PROPERTIES_ASPECT_NAME)
|
||||
.setValue(new Aspect(oldTagProperties.data()));
|
||||
Mockito.when(mockClient.batchGetV2(
|
||||
Mockito.eq(Constants.TAG_ENTITY_NAME),
|
||||
Mockito.eq(new HashSet<>(ImmutableSet.of(Urn.createFromString(TEST_ENTITY_URN)))),
|
||||
Mockito.eq(ImmutableSet.of(Constants.TAG_PROPERTIES_ASPECT_NAME)),
|
||||
Mockito.any(Authentication.class)))
|
||||
.thenReturn(ImmutableMap.of(Urn.createFromString(TEST_ENTITY_URN),
|
||||
new EntityResponse()
|
||||
.setEntityName(Constants.TAG_ENTITY_NAME)
|
||||
.setUrn(Urn.createFromString(TEST_ENTITY_URN))
|
||||
.setAspects(new EnvelopedAspectMap(ImmutableMap.of(
|
||||
Constants.TAG_PROPERTIES_ASPECT_NAME,
|
||||
oldTagPropertiesAspect)))));
|
||||
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(false);
|
||||
|
||||
SetTagColorResolver resolver = new SetTagColorResolver(mockClient, mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_ENTITY_URN);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("colorHex"))).thenReturn(TEST_COLOR_HEX);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
Mockito.verify(mockClient, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(Authentication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnauthorized() throws Exception {
|
||||
// Create resolver
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
SetTagColorResolver resolver = new SetTagColorResolver(mockClient, mockService);
|
||||
|
||||
// Execute resolver
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_ENTITY_URN);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("colorHex"))).thenReturn(TEST_COLOR_HEX);
|
||||
QueryContext mockContext = getMockDenyContext();
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
Mockito.verify(mockClient, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(Authentication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEntityClientException() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
Mockito.doThrow(RemoteInvocationException.class).when(mockClient).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(Authentication.class));
|
||||
SetTagColorResolver resolver = new SetTagColorResolver(mockClient, Mockito.mock(EntityService.class));
|
||||
|
||||
// Execute resolver
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("urn"))).thenReturn(TEST_ENTITY_URN);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("colorHex"))).thenReturn(TEST_COLOR_HEX);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
}
|
||||
}
|
@ -336,7 +336,9 @@ export const dataset3 = {
|
||||
urn: 'urn:li:glossaryTerm:sample-glossary-term',
|
||||
name: 'sample-glossary-term',
|
||||
hierarchicalName: 'example.sample-glossary-term',
|
||||
glossaryTermInfo: {
|
||||
properties: {
|
||||
name: 'sample-glossary-term',
|
||||
description: 'sample definition',
|
||||
definition: 'sample definition',
|
||||
termSource: 'sample term source',
|
||||
},
|
||||
@ -704,6 +706,16 @@ const glossaryTerm1 = {
|
||||
},
|
||||
},
|
||||
glossaryTermInfo: {
|
||||
name: 'Another glossary term',
|
||||
description: 'New glossary term',
|
||||
definition: 'New glossary term',
|
||||
termSource: 'termSource',
|
||||
sourceRef: 'sourceRef',
|
||||
sourceURI: 'sourceURI',
|
||||
},
|
||||
properties: {
|
||||
name: 'Another glossary term',
|
||||
description: 'New glossary term',
|
||||
definition: 'New glossary term',
|
||||
termSource: 'termSource',
|
||||
sourceRef: 'sourceRef',
|
||||
@ -718,6 +730,8 @@ const glossaryTerm2 = {
|
||||
hierarchicalName: 'example.glossaryterm1',
|
||||
ownership: null,
|
||||
glossaryTermInfo: {
|
||||
name: 'glossaryterm1',
|
||||
description: 'is A relation glossary term 1',
|
||||
definition: 'is A relation glossary term 1',
|
||||
termSource: 'INTERNAL',
|
||||
sourceRef: 'TERM_SOURCE_SAXO',
|
||||
@ -732,6 +746,23 @@ const glossaryTerm2 = {
|
||||
],
|
||||
__typename: 'GlossaryTermInfo',
|
||||
},
|
||||
properties: {
|
||||
name: 'glossaryterm1',
|
||||
description: 'is A relation glossary term 1',
|
||||
definition: 'is A relation glossary term 1',
|
||||
termSource: 'INTERNAL',
|
||||
sourceRef: 'TERM_SOURCE_SAXO',
|
||||
sourceUrl: '',
|
||||
rawSchema: 'sample proto schema',
|
||||
customProperties: [
|
||||
{
|
||||
key: 'keyProperty',
|
||||
value: 'valueProperty',
|
||||
__typename: 'StringMapEntry',
|
||||
},
|
||||
],
|
||||
__typename: 'GlossaryTermProperties',
|
||||
},
|
||||
isRealtedTerms: {
|
||||
start: 0,
|
||||
count: 0,
|
||||
@ -770,6 +801,8 @@ const glossaryTerm3 = {
|
||||
hierarchicalName: 'example.glossaryterm2',
|
||||
ownership: null,
|
||||
glossaryTermInfo: {
|
||||
name: 'glossaryterm2',
|
||||
description: 'has A relation glossary term 2',
|
||||
definition: 'has A relation glossary term 2',
|
||||
termSource: 'INTERNAL',
|
||||
sourceRef: 'TERM_SOURCE_SAXO',
|
||||
@ -784,6 +817,23 @@ const glossaryTerm3 = {
|
||||
],
|
||||
__typename: 'GlossaryTermInfo',
|
||||
},
|
||||
properties: {
|
||||
name: 'glossaryterm2',
|
||||
description: 'has A relation glossary term 2',
|
||||
definition: 'has A relation glossary term 2',
|
||||
termSource: 'INTERNAL',
|
||||
sourceRef: 'TERM_SOURCE_SAXO',
|
||||
sourceUrl: '',
|
||||
rawSchema: 'sample proto schema',
|
||||
customProperties: [
|
||||
{
|
||||
key: 'keyProperty',
|
||||
value: 'valueProperty',
|
||||
__typename: 'StringMapEntry',
|
||||
},
|
||||
],
|
||||
__typename: 'GlossaryTermProperties',
|
||||
},
|
||||
glossaryRelatedTerms: {
|
||||
isRelatedTerms: null,
|
||||
hasRelatedTerms: [
|
||||
|
@ -124,7 +124,9 @@ export const sampleSchemaWithTags: Schema = {
|
||||
urn: 'urn:li:glossaryTerm:sample-glossary-term',
|
||||
name: 'sample-glossary-term',
|
||||
hierarchicalName: 'example.sample-glossary-term',
|
||||
glossaryTermInfo: {
|
||||
properties: {
|
||||
name: 'sample-glossary-term',
|
||||
description: 'sample definition',
|
||||
definition: 'sample definition',
|
||||
termSource: 'sample term source',
|
||||
},
|
||||
@ -246,7 +248,9 @@ export const sampleSchemaWithPkFk: SchemaMetadata = {
|
||||
urn: 'urn:li:glossaryTerm:sample-glossary-term',
|
||||
name: 'sample-glossary-term',
|
||||
hierarchicalName: 'example.sample-glossary-term',
|
||||
glossaryTermInfo: {
|
||||
properties: {
|
||||
name: 'sample-glossary-term',
|
||||
description: 'sample definition',
|
||||
definition: 'sample definition',
|
||||
termSource: 'sample term source',
|
||||
},
|
||||
|
@ -45,9 +45,6 @@ export const SetDomainModal = ({ visible, onClose, refetch }: Props) => {
|
||||
const domainSearchResults = domainSearchData?.search?.searchResults || [];
|
||||
const [setDomainMutation] = useSetDomainMutation();
|
||||
|
||||
console.log('Re rendering');
|
||||
console.log(domainSearchResults);
|
||||
|
||||
const inputEl = useRef(null);
|
||||
|
||||
const onOk = async () => {
|
||||
@ -156,7 +153,6 @@ export const SetDomainModal = ({ visible, onClose, refetch }: Props) => {
|
||||
tagRender={(tagProps) => <Tag>{tagProps.value}</Tag>}
|
||||
>
|
||||
{domainSearchResults.map((result) => {
|
||||
console.log(result);
|
||||
return (
|
||||
<Select.Option value={result.entity.urn}>{renderSearchResult(result)}</Select.Option>
|
||||
);
|
||||
|
@ -185,6 +185,7 @@ fragment entityPreview on Entity {
|
||||
}
|
||||
... on GlossaryTerm {
|
||||
name
|
||||
hierarchicalName
|
||||
glossaryTermInfo {
|
||||
definition
|
||||
termSource
|
||||
|
@ -13,7 +13,17 @@ import com.linkedin.common.CustomProperties
|
||||
record GlossaryTermInfo includes CustomProperties {
|
||||
|
||||
/**
|
||||
* Definition of business term
|
||||
* Display name of the term
|
||||
*/
|
||||
@Searchable = {
|
||||
"fieldType": "TEXT_PARTIAL",
|
||||
"enableAutocomplete": true,
|
||||
"boostScore": 10.0
|
||||
}
|
||||
name: optional string
|
||||
|
||||
/**
|
||||
* Definition of business term.
|
||||
*/
|
||||
@Searchable = {}
|
||||
definition: string
|
||||
|
@ -9,12 +9,14 @@ import com.linkedin.common.Urn
|
||||
"name": "glossaryTermKey"
|
||||
}
|
||||
record GlossaryTermKey {
|
||||
|
||||
/**
|
||||
* The term name, which serves as a unique id
|
||||
*/
|
||||
@Searchable = {
|
||||
"fieldType": "TEXT_PARTIAL",
|
||||
"enableAutocomplete": true
|
||||
"enableAutocomplete": true,
|
||||
"fieldName": "id"
|
||||
}
|
||||
name: string
|
||||
|
||||
|
||||
}
|
@ -8,12 +8,13 @@ namespace com.linkedin.metadata.key
|
||||
}
|
||||
record TagKey {
|
||||
/**
|
||||
* The unique tag name
|
||||
* The tag name, which serves as a unique id
|
||||
*/
|
||||
@Searchable = {
|
||||
"fieldType": "TEXT_PARTIAL",
|
||||
"enableAutocomplete": true,
|
||||
"boostScore": 10.0
|
||||
"boostScore": 10.0,
|
||||
"fieldName": "id"
|
||||
}
|
||||
name: string
|
||||
}
|
@ -7,14 +7,24 @@ namespace com.linkedin.tag
|
||||
"name": "tagProperties"
|
||||
}
|
||||
record TagProperties {
|
||||
|
||||
/**
|
||||
* Name of the tag
|
||||
* Display name of the tag
|
||||
*/
|
||||
@Searchable = {
|
||||
"fieldType": "TEXT_PARTIAL",
|
||||
"enableAutocomplete": true,
|
||||
"boostScore": 10.0
|
||||
}
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Documentation of the tag
|
||||
*/
|
||||
@Searchable = {}
|
||||
description: optional string
|
||||
|
||||
/**
|
||||
* The color associated with the Tag in Hex. For example #FFFFFF.
|
||||
*/
|
||||
colorHex: optional string
|
||||
}
|
||||
|
@ -66,11 +66,18 @@ entities:
|
||||
- browsePaths # unclear if this will be used
|
||||
- status
|
||||
- domains
|
||||
- name: tag
|
||||
keyAspect: tagKey
|
||||
aspects:
|
||||
- tagProperties
|
||||
- ownership
|
||||
- deprecation
|
||||
- name: glossaryTerm
|
||||
keyAspect: glossaryTermKey
|
||||
aspects:
|
||||
- glossaryTermInfo
|
||||
- schemaMetadata
|
||||
- ownership
|
||||
- deprecation
|
||||
- name: domain
|
||||
doc: A data domain within an organization.
|
||||
|
@ -1524,9 +1524,19 @@
|
||||
"doc" : "Properties associated with a GlossaryTerm",
|
||||
"include" : [ "com.linkedin.common.CustomProperties" ],
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "Display name of the term",
|
||||
"optional" : true,
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
}, {
|
||||
"name" : "definition",
|
||||
"type" : "string",
|
||||
"doc" : "Definition of business term",
|
||||
"doc" : "Definition of business term.",
|
||||
"Searchable" : { }
|
||||
}, {
|
||||
"name" : "parentNode",
|
||||
@ -2504,8 +2514,10 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "The term name, which serves as a unique id",
|
||||
"Searchable" : {
|
||||
"enableAutocomplete" : true,
|
||||
"fieldName" : "id",
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
} ],
|
||||
@ -3141,10 +3153,11 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "The unique tag name",
|
||||
"doc" : "The tag name, which serves as a unique id",
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldName" : "id",
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
} ],
|
||||
@ -3159,11 +3172,22 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "Name of the tag"
|
||||
"doc" : "Display name of the tag",
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
}, {
|
||||
"name" : "description",
|
||||
"type" : "string",
|
||||
"doc" : "Documentation of the tag",
|
||||
"optional" : true,
|
||||
"Searchable" : { }
|
||||
}, {
|
||||
"name" : "colorHex",
|
||||
"type" : "string",
|
||||
"doc" : "The color associated with the Tag in Hex. For example #FFFFFF.",
|
||||
"optional" : true
|
||||
} ],
|
||||
"Aspect" : {
|
||||
|
@ -4064,10 +4064,11 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "The unique tag name",
|
||||
"doc" : "The tag name, which serves as a unique id",
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldName" : "id",
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
} ],
|
||||
@ -4082,11 +4083,22 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "Name of the tag"
|
||||
"doc" : "Display name of the tag",
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
}, {
|
||||
"name" : "description",
|
||||
"type" : "string",
|
||||
"doc" : "Documentation of the tag",
|
||||
"optional" : true,
|
||||
"Searchable" : { }
|
||||
}, {
|
||||
"name" : "colorHex",
|
||||
"type" : "string",
|
||||
"doc" : "The color associated with the Tag in Hex. For example #FFFFFF.",
|
||||
"optional" : true
|
||||
} ],
|
||||
"Aspect" : {
|
||||
@ -4126,8 +4138,10 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "The term name, which serves as a unique id",
|
||||
"Searchable" : {
|
||||
"enableAutocomplete" : true,
|
||||
"fieldName" : "id",
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
} ],
|
||||
@ -4141,9 +4155,19 @@
|
||||
"doc" : "Properties associated with a GlossaryTerm",
|
||||
"include" : [ "com.linkedin.common.CustomProperties" ],
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "Display name of the term",
|
||||
"optional" : true,
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
}, {
|
||||
"name" : "definition",
|
||||
"type" : "string",
|
||||
"doc" : "Definition of business term",
|
||||
"doc" : "Definition of business term.",
|
||||
"Searchable" : { }
|
||||
}, {
|
||||
"name" : "parentNode",
|
||||
|
@ -1281,9 +1281,19 @@
|
||||
"doc" : "Properties associated with a GlossaryTerm",
|
||||
"include" : [ "com.linkedin.common.CustomProperties" ],
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "Display name of the term",
|
||||
"optional" : true,
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
}, {
|
||||
"name" : "definition",
|
||||
"type" : "string",
|
||||
"doc" : "Definition of business term",
|
||||
"doc" : "Definition of business term.",
|
||||
"Searchable" : { }
|
||||
}, {
|
||||
"name" : "parentNode",
|
||||
@ -2261,8 +2271,10 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "The term name, which serves as a unique id",
|
||||
"Searchable" : {
|
||||
"enableAutocomplete" : true,
|
||||
"fieldName" : "id",
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
} ],
|
||||
@ -2898,10 +2910,11 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "The unique tag name",
|
||||
"doc" : "The tag name, which serves as a unique id",
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldName" : "id",
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
} ],
|
||||
@ -2916,11 +2929,22 @@
|
||||
"fields" : [ {
|
||||
"name" : "name",
|
||||
"type" : "string",
|
||||
"doc" : "Name of the tag"
|
||||
"doc" : "Display name of the tag",
|
||||
"Searchable" : {
|
||||
"boostScore" : 10.0,
|
||||
"enableAutocomplete" : true,
|
||||
"fieldType" : "TEXT_PARTIAL"
|
||||
}
|
||||
}, {
|
||||
"name" : "description",
|
||||
"type" : "string",
|
||||
"doc" : "Documentation of the tag",
|
||||
"optional" : true,
|
||||
"Searchable" : { }
|
||||
}, {
|
||||
"name" : "colorHex",
|
||||
"type" : "string",
|
||||
"doc" : "The color associated with the Tag in Hex. For example #FFFFFF.",
|
||||
"optional" : true
|
||||
} ],
|
||||
"Aspect" : {
|
||||
|
@ -29,6 +29,8 @@ public class Constants {
|
||||
public static final String INGESTION_SOURCE_ENTITY_NAME = "dataHubIngestionSource";
|
||||
public static final String SECRETS_ENTITY_NAME = "dataHubSecret";
|
||||
public static final String EXECUTION_REQUEST_ENTITY_NAME = "dataHubExecutionRequest";
|
||||
public static final String TAG_ENTITY_NAME = "tag";
|
||||
|
||||
|
||||
/**
|
||||
* Aspects
|
||||
@ -98,6 +100,9 @@ public class Constants {
|
||||
public static final String GLOSSARY_TERM_INFO_ASPECT_NAME = "glossaryTermInfo";
|
||||
public static final String GLOSSARY_RELATED_TERM_ASPECT_NAME = "glossaryRelatedTerms";
|
||||
|
||||
// Tag
|
||||
public static final String TAG_PROPERTIES_ASPECT_NAME = "tagProperties";
|
||||
|
||||
// Domain
|
||||
public static final String DOMAIN_KEY_ASPECT_NAME = "domainKey";
|
||||
public static final String DOMAIN_PROPERTIES_ASPECT_NAME = "domainProperties";
|
||||
|
@ -147,6 +147,12 @@ public class PoliciesConfig {
|
||||
"The ability to edit the column (field) descriptions associated with a dataset schema."
|
||||
);
|
||||
|
||||
// Tag Privileges
|
||||
public static final Privilege EDIT_TAG_COLOR_PRIVILEGE = Privilege.of(
|
||||
"EDIT_TAG_COLOR",
|
||||
"Edit Tag Color",
|
||||
"The ability to change the color of a Tag.");
|
||||
|
||||
public static final ResourcePrivileges DATASET_PRIVILEGES = ResourcePrivileges.of(
|
||||
"dataset",
|
||||
"Datasets",
|
||||
@ -194,7 +200,7 @@ public class PoliciesConfig {
|
||||
"tag",
|
||||
"Tags",
|
||||
"Tags indexed by DataHub",
|
||||
ImmutableList.of(EDIT_ENTITY_OWNERS_PRIVILEGE, EDIT_ENTITY_PRIVILEGE)
|
||||
ImmutableList.of(EDIT_ENTITY_OWNERS_PRIVILEGE, EDIT_TAG_COLOR_PRIVILEGE, EDIT_ENTITY_DOCS_PRIVILEGE, EDIT_ENTITY_PRIVILEGE)
|
||||
);
|
||||
|
||||
// Container Privileges
|
||||
|
Loading…
x
Reference in New Issue
Block a user