From d443b076afac83004df7cfea721677a4c05e5a87 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Mon, 18 Dec 2023 00:04:24 +0530 Subject: [PATCH 01/50] business-attribute: graphql crud resolvers and metadata models --- .../datahub/graphql/GmsGraphQLEngine.java | 63 +- .../graphql/resolvers/EntityTypeMapper.java | 1 + .../datahub/graphql/resolvers/MeResolver.java | 4 +- .../BusinessAttributeAuthorizationUtils.java | 40 + .../CreateBusinessAttributeResolver.java | 103 +++ .../DeleteBusinessAttributeResolver.java | 50 + .../ListBusinessAttributesResolver.java | 23 + .../UpdateBusinessAttributeResolver.java | 124 +++ .../UpdateDeprecationResolver.java | 166 ++-- .../resolvers/mutate/AddTagsResolver.java | 8 +- .../mutate/BatchAddTagsResolver.java | 1 + .../resolvers/mutate/DescriptionUtils.java | 15 + .../mutate/UpdateDescriptionResolver.java | 856 +++++++++--------- .../resolvers/mutate/UpdateNameResolver.java | 369 ++++---- .../mutate/util/BusinessAttributeUtils.java | 95 ++ .../resolvers/mutate/util/LabelUtils.java | 97 +- .../graphql/resolvers/search/SearchUtils.java | 2 + .../BusinessAttributeType.java | 84 ++ .../mappers/BusinessAttributeMapper.java | 67 ++ .../common/mappers/UrnToEntityMapper.java | 6 + .../src/main/resources/app.graphql | 11 + .../src/main/resources/entity.graphql | 174 ++++ datahub-web-react/src/graphql/me.graphql | 3 + .../java/com/linkedin/metadata/Constants.java | 5 + .../BusinessAttributeInfo.pdl | 26 + .../BusinessAttributeKey.pdl | 14 + .../schema/EditableSchemaFieldBase.pdl | 61 ++ .../schema/EditableSchemaFieldInfo.pdl | 54 +- .../src/main/resources/entity-registry.yml | 8 + .../authorization/PoliciesConfig.java | 14 +- 30 files changed, 1805 insertions(+), 739 deletions(-) create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java create mode 100644 metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index b0b26f073876c4..665c6b53850453 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -68,7 +68,6 @@ import com.linkedin.datahub.graphql.generated.ListQueriesResult; import com.linkedin.datahub.graphql.generated.ListTestsResult; import com.linkedin.datahub.graphql.generated.ListViewsResult; -import com.linkedin.datahub.graphql.generated.MatchedField; import com.linkedin.datahub.graphql.generated.MLFeature; import com.linkedin.datahub.graphql.generated.MLFeatureProperties; import com.linkedin.datahub.graphql.generated.MLFeatureTable; @@ -78,6 +77,7 @@ import com.linkedin.datahub.graphql.generated.MLModelProperties; import com.linkedin.datahub.graphql.generated.MLPrimaryKey; import com.linkedin.datahub.graphql.generated.MLPrimaryKeyProperties; +import com.linkedin.datahub.graphql.generated.MatchedField; import com.linkedin.datahub.graphql.generated.Notebook; import com.linkedin.datahub.graphql.generated.Owner; import com.linkedin.datahub.graphql.generated.OwnershipTypeEntity; @@ -105,6 +105,10 @@ import com.linkedin.datahub.graphql.resolvers.browse.BrowsePathsResolver; import com.linkedin.datahub.graphql.resolvers.browse.BrowseResolver; import com.linkedin.datahub.graphql.resolvers.browse.EntityBrowsePathsResolver; +import com.linkedin.datahub.graphql.resolvers.businessattribute.CreateBusinessAttributeResolver; +import com.linkedin.datahub.graphql.resolvers.businessattribute.DeleteBusinessAttributeResolver; +import com.linkedin.datahub.graphql.resolvers.businessattribute.ListBusinessAttributesResolver; +import com.linkedin.datahub.graphql.resolvers.businessattribute.UpdateBusinessAttributeResolver; import com.linkedin.datahub.graphql.resolvers.chart.BrowseV2Resolver; import com.linkedin.datahub.graphql.resolvers.chart.ChartStatsSummaryResolver; import com.linkedin.datahub.graphql.resolvers.config.AppConfigResolver; @@ -267,6 +271,7 @@ import com.linkedin.datahub.graphql.types.aspect.AspectType; import com.linkedin.datahub.graphql.types.assertion.AssertionType; import com.linkedin.datahub.graphql.types.auth.AccessTokenMetadataType; +import com.linkedin.datahub.graphql.types.businessattribute.BusinessAttributeType; import com.linkedin.datahub.graphql.types.chart.ChartType; import com.linkedin.datahub.graphql.types.common.mappers.OperationMapper; import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper; @@ -284,7 +289,6 @@ import com.linkedin.datahub.graphql.types.dataset.VersionedDatasetType; import com.linkedin.datahub.graphql.types.dataset.mappers.DatasetProfileMapper; import com.linkedin.datahub.graphql.types.domain.DomainType; -import com.linkedin.datahub.graphql.types.rolemetadata.RoleType; import com.linkedin.datahub.graphql.types.glossary.GlossaryNodeType; import com.linkedin.datahub.graphql.types.glossary.GlossaryTermType; import com.linkedin.datahub.graphql.types.mlmodel.MLFeatureTableType; @@ -297,6 +301,7 @@ import com.linkedin.datahub.graphql.types.policy.DataHubPolicyType; import com.linkedin.datahub.graphql.types.query.QueryType; import com.linkedin.datahub.graphql.types.role.DataHubRoleType; +import com.linkedin.datahub.graphql.types.rolemetadata.RoleType; import com.linkedin.datahub.graphql.types.schemafield.SchemaFieldType; import com.linkedin.datahub.graphql.types.tag.TagType; import com.linkedin.datahub.graphql.types.test.TestType; @@ -332,6 +337,13 @@ import graphql.schema.DataFetchingEnvironment; import graphql.schema.StaticDataFetcher; import graphql.schema.idl.RuntimeWiring; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.dataloader.BatchLoaderContextProvider; +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; + import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -345,16 +357,24 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.IOUtils; -import org.dataloader.BatchLoaderContextProvider; -import org.dataloader.DataLoader; -import org.dataloader.DataLoaderOptions; -import static com.linkedin.datahub.graphql.Constants.*; -import static com.linkedin.metadata.Constants.*; -import static graphql.scalars.ExtendedScalars.*; +import static com.linkedin.datahub.graphql.Constants.ANALYTICS_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.APP_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.AUTH_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.GMS_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.INGESTION_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.LINEAGE_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.RECOMMENDATIONS_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.SEARCH_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.STEPS_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.TESTS_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.TIMELINE_SCHEMA_FILE; +import static com.linkedin.datahub.graphql.Constants.URNS_FIELD_NAME; +import static com.linkedin.datahub.graphql.Constants.URN_FIELD_NAME; +import static com.linkedin.datahub.graphql.Constants.VERSION_STAMP_FIELD_NAME; +import static com.linkedin.metadata.Constants.DATA_PROCESS_INSTANCE_RUN_EVENT_ASPECT_NAME; +import static com.linkedin.metadata.Constants.OPERATION_EVENT_TIME_FIELD_NAME; +import static graphql.scalars.ExtendedScalars.GraphQLLong; /** @@ -439,6 +459,8 @@ public class GmsGraphQLEngine { private final DataProductType dataProductType; private final OwnershipType ownershipType; + private final BusinessAttributeType businessAttributeType; + /** * A list of GraphQL Plugins that extend the core engine */ @@ -469,6 +491,7 @@ public class GmsGraphQLEngine { */ public final List> browsableTypes; + public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.graphQLPlugins = List.of( @@ -548,6 +571,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.queryType = new QueryType(entityClient); this.dataProductType = new DataProductType(entityClient); this.ownershipType = new OwnershipType(entityClient); + this.businessAttributeType = new BusinessAttributeType(entityClient); // Init Lists this.entityTypes = ImmutableList.of( @@ -582,7 +606,8 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { dataHubViewType, queryType, dataProductType, - ownershipType + ownershipType, + businessAttributeType ); this.loadableTypes = new ArrayList<>(entityTypes); // Extend loadable types with types from the plugins @@ -666,6 +691,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureQueryEntityResolvers(builder); configureOwnershipTypeResolver(builder); configurePluginResolvers(builder); + configureBusinessAttributeResolver(builder); } private void configureOrganisationRoleResolvers(RuntimeWiring.Builder builder) { @@ -865,6 +891,8 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("listDataProductAssets", new ListDataProductAssetsResolver(this.entityClient)) .dataFetcher("listOwnershipTypes", new ListOwnershipTypesResolver(this.entityClient)) .dataFetcher("browseV2", new BrowseV2Resolver(this.entityClient, this.viewService)) + .dataFetcher("businessAttribute", getResolver(businessAttributeType)) + .dataFetcher("listBusinessAttributes", new ListBusinessAttributesResolver(this.entityClient)) ); } @@ -1008,6 +1036,9 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService)) .dataFetcher("updateOwnershipType", new UpdateOwnershipTypeResolver(this.ownershipTypeService)) .dataFetcher("deleteOwnershipType", new DeleteOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher("createBusinessAttribute", new CreateBusinessAttributeResolver(this.entityClient, this.entityService)) + .dataFetcher("updateBusinessAttribute", new UpdateBusinessAttributeResolver(this.entityClient)) + .dataFetcher("deleteBusinessAttribute", new DeleteBusinessAttributeResolver(this.entityClient)) ); } @@ -1848,4 +1879,12 @@ private void configureIngestionSourceResolvers(final RuntimeWiring.Builder build }) )); } + + private void configureBusinessAttributeResolver(final RuntimeWiring.Builder builder) { + builder.type("BusinessAttribute", typeWiring -> typeWiring + .dataFetcher("exists", new EntityExistsResolver(entityService)) + .dataFetcher("privileges", new EntityPrivilegesResolver(entityClient)) + ); + + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java index b0f23e63177e60..1b0c942f15c42c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java @@ -41,6 +41,7 @@ public class EntityTypeMapper { .put(EntityType.TEST, "test") .put(EntityType.DATAHUB_VIEW, Constants.DATAHUB_VIEW_ENTITY_NAME) .put(EntityType.DATA_PRODUCT, Constants.DATA_PRODUCT_ENTITY_NAME) + .put(EntityType.BUSINESS_ATTRIBUTE, Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME) .build(); private static final Map ENTITY_NAME_TO_TYPE = diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java index 02921b453e3154..4fa9018d91b855 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java @@ -10,6 +10,7 @@ import com.linkedin.datahub.graphql.generated.AuthenticatedUser; import com.linkedin.datahub.graphql.generated.CorpUser; import com.linkedin.datahub.graphql.generated.PlatformPrivileges; +import com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils; import com.linkedin.datahub.graphql.types.corpuser.mappers.CorpUserMapper; import com.linkedin.entity.EntityResponse; import com.linkedin.entity.client.EntityClient; @@ -75,7 +76,8 @@ public CompletableFuture get(DataFetchingEnvironment environm platformPrivileges.setManageGlobalViews(AuthorizationUtils.canManageGlobalViews(context)); platformPrivileges.setManageOwnershipTypes(AuthorizationUtils.canManageOwnershipTypes(context)); platformPrivileges.setManageGlobalAnnouncements(AuthorizationUtils.canManageGlobalAnnouncements(context)); - + platformPrivileges.setCreateBusinessAttributes(BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)); + platformPrivileges.setManageBusinessAttributes(BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)); // Construct and return authenticated user object. final AuthenticatedUser authUser = new AuthenticatedUser(); authUser.setCorpUser(corpUser); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java new file mode 100644 index 00000000000000..24e60a5aee7679 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java @@ -0,0 +1,40 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authorization.ConjunctivePrivilegeGroup; +import com.datahub.authorization.DisjunctivePrivilegeGroup; +import com.google.common.collect.ImmutableList; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; +import com.linkedin.metadata.authorization.PoliciesConfig; + +import javax.annotation.Nonnull; + +public class BusinessAttributeAuthorizationUtils { + private BusinessAttributeAuthorizationUtils() { + + } + + public static boolean canCreateBusinessAttribute(@Nonnull QueryContext context) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( + new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())), + new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())) + )); + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + orPrivilegeGroups); + } + + public static boolean canManageBusinessAttribute(@Nonnull QueryContext context) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( + new ConjunctivePrivilegeGroup(ImmutableList.of( + PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())) + )); + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + orPrivilegeGroups); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java new file mode 100644 index 00000000000000..c1cf3443e4365d --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java @@ -0,0 +1,103 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.businessattribute.BusinessAttributeKey; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.template.SetMode; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.exception.DataHubGraphQLErrorCode; +import com.linkedin.datahub.graphql.exception.DataHubGraphQLException; +import com.linkedin.datahub.graphql.generated.CreateBusinessAttributeInput; +import com.linkedin.datahub.graphql.generated.OwnerEntityType; +import com.linkedin.datahub.graphql.generated.OwnershipType; +import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; +import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.utils.EntityKeyUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithKey; +import static com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils.mapOwnershipTypeToEntity; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; + +@Slf4j +@RequiredArgsConstructor +public class CreateBusinessAttributeResolver implements DataFetcher> { + private final EntityClient _entityClient; + private final EntityService _entityService; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + CreateBusinessAttributeInput input = bindArgument(environment.getArgument("input"), CreateBusinessAttributeInput.class); + + return CompletableFuture.supplyAsync(() -> { + if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + + try { + final BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); + businessAttributeKey.setId(UUID.randomUUID().toString()); + + if (_entityClient.exists(EntityKeyUtils.convertEntityKeyToUrn(businessAttributeKey, + BUSINESS_ATTRIBUTE_ENTITY_NAME), + context.getAuthentication())) { + throw new IllegalArgumentException("This Business Attribute already exists!"); + } + + if (BusinessAttributeUtils.hasNameConflict(input.getBusinessAttributeInfo().getName(), context, _entityClient)) { + throw new DataHubGraphQLException( + String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", + input.getBusinessAttributeInfo().getName()), DataHubGraphQLErrorCode.CONFLICT); + } + + // Create the MCP + final MetadataChangeProposal changeProposal = buildMetadataChangeProposalWithKey( + businessAttributeKey, BUSINESS_ATTRIBUTE_ENTITY_NAME, + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + mapBusinessAttributeInfo(input, context) + ); + + // Ingest the MCP + String businessAttributeUrn = _entityClient.ingestProposal(changeProposal, context.getAuthentication()); + OwnershipType ownershipType = OwnershipType.TECHNICAL_OWNER; + if (!_entityService.exists(UrnUtils.getUrn(mapOwnershipTypeToEntity(ownershipType.name())))) { + log.warn("Technical owner does not exist, defaulting to None ownership."); + ownershipType = OwnershipType.NONE; + } + OwnerUtils.addCreatorAsOwner(context, businessAttributeUrn, OwnerEntityType.CORP_USER, ownershipType, _entityService); + return businessAttributeUrn; + + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + log.error("Failed to create Business Attribute with name: {}: {}", input.getBusinessAttributeInfo().getName(), e.getMessage()); + throw new RuntimeException(String.format("Failed to create Business Attribute with name: %s", input.getBusinessAttributeInfo().getName()), e); + } + }); + } + + private BusinessAttributeInfo mapBusinessAttributeInfo(CreateBusinessAttributeInput input, QueryContext context) { + final BusinessAttributeInfo info = new BusinessAttributeInfo(); + info.setFieldPath(input.getBusinessAttributeInfo().getName(), SetMode.DISALLOW_NULL); + info.setName(input.getBusinessAttributeInfo().getName(), SetMode.DISALLOW_NULL); + info.setDescription(input.getBusinessAttributeInfo().getDescription(), SetMode.IGNORE_NULL); + info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(input.getBusinessAttributeInfo().getType()), SetMode.IGNORE_NULL); + info.setCreated(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis())); + return info; + } + +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java new file mode 100644 index 00000000000000..63f274251dac7f --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java @@ -0,0 +1,50 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.entity.client.EntityClient; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CompletableFuture; + +/** + * Resolver responsible for hard deleting a particular Business Attribute + */ +@Slf4j +@RequiredArgsConstructor +public class DeleteBusinessAttributeResolver implements DataFetcher> { + private final EntityClient _entityClient; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + final Urn businessAttributeUrn = UrnUtils.getUrn(environment.getArgument("urn")); + return CompletableFuture.supplyAsync(() -> { + if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + try { + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new IllegalArgumentException("The Business Attribute provided dos not exist"); + } + _entityClient.deleteEntity(businessAttributeUrn, context.getAuthentication()); + CompletableFuture.runAsync(() -> { + try { + _entityClient.deleteEntityReferences(businessAttributeUrn, context.getAuthentication()); + } catch (Exception e) { + log.error(String.format( + "Exception while attempting to clear all entity references for Business Attribute with urn %s", businessAttributeUrn), e); + } + }); + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to delete Business Attribute with urn %s", businessAttributeUrn), e); + } + }); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java new file mode 100644 index 00000000000000..de3a32b7832389 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java @@ -0,0 +1,23 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.linkedin.datahub.graphql.generated.SearchResults; +import com.linkedin.entity.client.EntityClient; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CompletableFuture; + +@Slf4j +@RequiredArgsConstructor +public class ListBusinessAttributesResolver implements DataFetcher> { + private final EntityClient _entityClient; + private static final int DEFAULT_START = 0; + private static final int DEFAULT_COUNT = 10; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + return null; + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java new file mode 100644 index 00000000000000..b15b817682fb65 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java @@ -0,0 +1,124 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authentication.Authentication; +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.exception.DataHubGraphQLErrorCode; +import com.linkedin.datahub.graphql.exception.DataHubGraphQLException; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; +import com.linkedin.datahub.graphql.generated.UpdateBusinessAttributeInput; +import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.AspectUtils; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; + +@Slf4j +@RequiredArgsConstructor +public class UpdateBusinessAttributeResolver implements DataFetcher> { + + private final EntityClient _entityClient; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + QueryContext context = environment.getContext(); + UpdateBusinessAttributeInput input = bindArgument(environment.getArgument("input"), UpdateBusinessAttributeInput.class); + final Urn businessAttributeUrn = UrnUtils.getUrn(environment.getArgument("urn")); + if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + return CompletableFuture.supplyAsync(() -> { + try { + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new IllegalArgumentException("The Business Attribute provided dos not exist"); + } + updateBusinessAttribute(input, businessAttributeUrn, context); + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to update Business Attribute with urn %s", businessAttributeUrn), e); + } + return null; + }); + } + + private Urn updateBusinessAttribute(UpdateBusinessAttributeInput input, Urn businessAttributeUrn, QueryContext context) { + try { + BusinessAttributeInfo businessAttributeInfo = getBusinessAttributeInfo(businessAttributeUrn, context.getAuthentication()); + // 1. Check whether the Business Attribute exists + if (businessAttributeInfo == null) { + throw new IllegalArgumentException( + String.format("Failed to update Business Attribute. Business Attribute with urn %s does not exist.", businessAttributeUrn)); + } + + // 2. Apply changes to existing Business Attribute + if (Objects.nonNull(input.getName())) { + if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { + throw new DataHubGraphQLException( + String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", input.getName()), + DataHubGraphQLErrorCode.CONFLICT); + } + businessAttributeInfo.setName(input.getName()); + businessAttributeInfo.setFieldPath(input.getName()); + } + if (Objects.nonNull(input.getDescription())) { + businessAttributeInfo.setDescription(input.getDescription()); + } + if (Objects.nonNull(input.getType())) { + businessAttributeInfo.setType(BusinessAttributeUtils.mapSchemaFieldDataType(input.getType())); + } + // 3. Write changes to GMS + return UrnUtils.getUrn(_entityClient.ingestProposal( + AspectUtils.buildMetadataChangeProposal( + businessAttributeUrn, Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo), context.getAuthentication() + ) + ); + + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Nullable + public BusinessAttributeInfo getBusinessAttributeInfo(@Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { + Objects.requireNonNull(businessAttributeUrn, "businessAttributeUrn must not be null"); + Objects.requireNonNull(authentication, "authentication must not be null"); + final EntityResponse response = getBusinessAttributeEntityResponse(businessAttributeUrn, authentication); + if (response != null && response.getAspects().containsKey(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME)) { + return new BusinessAttributeInfo(response.getAspects().get(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME).getValue().data()); + } + // No aspect found + return null; + } + + private EntityResponse getBusinessAttributeEntityResponse(@Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { + Objects.requireNonNull(businessAttributeUrn, "business attribute must not be null"); + Objects.requireNonNull(authentication, "authentication must not be null"); + try { + return _entityClient.batchGetV2( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + Set.of(businessAttributeUrn), + Set.of(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME), + authentication).get(businessAttributeUrn); + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to retrieve Business Attribute with urn %s", businessAttributeUrn), e); + } + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolver.java index 75c09d0cf7e437..f3a976577627c7 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/deprecation/UpdateDeprecationResolver.java @@ -18,13 +18,17 @@ import com.linkedin.mxe.MetadataChangeProposal; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; + import java.net.URISyntaxException; import java.util.concurrent.CompletableFuture; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; + import com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils; + import static com.linkedin.metadata.Constants.*; @@ -35,89 +39,89 @@ @RequiredArgsConstructor public class UpdateDeprecationResolver implements DataFetcher> { - private static final String EMPTY_STRING = ""; - private final EntityClient _entityClient; - private final EntityService _entityService; // TODO: Remove this when 'exists' added to EntityClient - - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - - final QueryContext context = environment.getContext(); - final UpdateDeprecationInput input = bindArgument(environment.getArgument("input"), UpdateDeprecationInput.class); - final Urn entityUrn = Urn.createFromString(input.getUrn()); - - return CompletableFuture.supplyAsync(() -> { - - if (!isAuthorizedToUpdateDeprecationForEntity(environment.getContext(), entityUrn)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - validateUpdateDeprecationInput( - entityUrn, - _entityService - ); - try { - Deprecation deprecation = (Deprecation) EntityUtils.getAspectFromEntity( - entityUrn.toString(), - DEPRECATION_ASPECT_NAME, - _entityService, - new Deprecation()); - updateDeprecation(deprecation, input, context); - - // Create the Deprecation aspect - final MetadataChangeProposal proposal = MutationUtils.buildMetadataChangeProposalWithUrn(entityUrn, DEPRECATION_ASPECT_NAME, deprecation); - _entityClient.ingestProposal(proposal, context.getAuthentication(), false); - return true; - } catch (Exception e) { - log.error("Failed to update Deprecation for resource with entity urn {}: {}", entityUrn, e.getMessage()); - throw new RuntimeException(String.format("Failed to update Deprecation for resource with entity urn %s", entityUrn), e); - } - }); - } - - private boolean isAuthorizedToUpdateDeprecationForEntity(final QueryContext context, final Urn entityUrn) { - final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( - AuthUtils.ALL_PRIVILEGES_GROUP, - new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_ENTITY_DEPRECATION_PRIVILEGE.getType())) - )); - - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), - context.getActorUrn(), - entityUrn.getEntityType(), - entityUrn.toString(), - orPrivilegeGroups); - } - - public static Boolean validateUpdateDeprecationInput( - Urn entityUrn, - EntityService entityService - ) { - - if (!entityService.exists(entityUrn)) { - throw new IllegalArgumentException( - String.format("Failed to update deprecation for Entity %s. Entity does not exist.", entityUrn)); + private static final String EMPTY_STRING = ""; + private final EntityClient _entityClient; + private final EntityService _entityService; // TODO: Remove this when 'exists' added to EntityClient + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + + final QueryContext context = environment.getContext(); + final UpdateDeprecationInput input = bindArgument(environment.getArgument("input"), UpdateDeprecationInput.class); + final Urn entityUrn = Urn.createFromString(input.getUrn()); + + return CompletableFuture.supplyAsync(() -> { + + if (!isAuthorizedToUpdateDeprecationForEntity(environment.getContext(), entityUrn)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + validateUpdateDeprecationInput( + entityUrn, + _entityService + ); + try { + Deprecation deprecation = (Deprecation) EntityUtils.getAspectFromEntity( + entityUrn.toString(), + DEPRECATION_ASPECT_NAME, + _entityService, + new Deprecation()); + updateDeprecation(deprecation, input, context); + + // Create the Deprecation aspect + final MetadataChangeProposal proposal = MutationUtils.buildMetadataChangeProposalWithUrn(entityUrn, DEPRECATION_ASPECT_NAME, deprecation); + _entityClient.ingestProposal(proposal, context.getAuthentication(), false); + return true; + } catch (Exception e) { + log.error("Failed to update Deprecation for resource with entity urn {}: {}", entityUrn, e.getMessage()); + throw new RuntimeException(String.format("Failed to update Deprecation for resource with entity urn %s", entityUrn), e); + } + }); } - return true; - } - - private static void updateDeprecation(Deprecation deprecation, UpdateDeprecationInput input, QueryContext context) { - deprecation.setDeprecated(input.getDeprecated()); - deprecation.setDecommissionTime(input.getDecommissionTime(), SetMode.REMOVE_IF_NULL); - if (input.getNote() != null) { - deprecation.setNote(input.getNote()); - } else { - // Note is required field in GMS. Set to empty string if not provided. - deprecation.setNote(EMPTY_STRING); + private boolean isAuthorizedToUpdateDeprecationForEntity(final QueryContext context, final Urn entityUrn) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( + AuthUtils.ALL_PRIVILEGES_GROUP, + new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_ENTITY_DEPRECATION_PRIVILEGE.getType())) + )); + + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + entityUrn.getEntityType(), + entityUrn.toString(), + orPrivilegeGroups); } - try { - deprecation.setActor(Urn.createFromString(context.getActorUrn())); - } catch (URISyntaxException e) { - // Should never happen. - throw new RuntimeException( - String.format("Failed to convert authorized actor into an Urn. actor urn: %s", - context.getActorUrn()), - e); + + public static Boolean validateUpdateDeprecationInput( + Urn entityUrn, + EntityService entityService + ) { + + if (!entityService.exists(entityUrn)) { + throw new IllegalArgumentException( + String.format("Failed to update deprecation for Entity %s. Entity does not exist.", entityUrn)); + } + + return true; + } + + private static void updateDeprecation(Deprecation deprecation, UpdateDeprecationInput input, QueryContext context) { + deprecation.setDeprecated(input.getDeprecated()); + deprecation.setDecommissionTime(input.getDecommissionTime(), SetMode.REMOVE_IF_NULL); + if (input.getNote() != null) { + deprecation.setNote(input.getNote()); + } else { + // Note is required field in GMS. Set to empty string if not provided. + deprecation.setNote(EMPTY_STRING); + } + try { + deprecation.setActor(Urn.createFromString(context.getActorUrn())); + } catch (URISyntaxException e) { + // Should never happen. + throw new RuntimeException( + String.format("Failed to convert authorized actor into an Urn. actor urn: %s", + context.getActorUrn()), + e); + } } - } } \ No newline at end of file diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddTagsResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddTagsResolver.java index 7174f3edffee67..71338e4afbf8c3 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddTagsResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddTagsResolver.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableList; import com.linkedin.common.urn.CorpuserUrn; - import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; @@ -14,13 +13,14 @@ import com.linkedin.metadata.entity.EntityService; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; @Slf4j diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/BatchAddTagsResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/BatchAddTagsResolver.java index 9c5cddb3c50bca..67531a20532742 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/BatchAddTagsResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/BatchAddTagsResolver.java @@ -54,6 +54,7 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw validateInputResources(resources, context); try { + // Then execute the bulk add batchAddTags(tagUrns, resources, context); return true; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java index 59d5d6939c04c8..cd2054e92230e1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableList; +import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.urn.Urn; import com.linkedin.container.EditableContainerProperties; import com.linkedin.datahub.graphql.QueryContext; @@ -363,4 +364,18 @@ public static void updateDataProductDescription( } persistAspect(resourceUrn, Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME, properties, actor, entityService); } + + public static void updateBusinessAttributeDescription( + String newDescription, + Urn resourceUrn, + Urn actor, + EntityService entityService) { + BusinessAttributeInfo businessAttributeInfo = (BusinessAttributeInfo) EntityUtils.getAspectFromEntity( + resourceUrn.toString(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, entityService, new BusinessAttributeInfo()); + if (businessAttributeInfo != null) { + businessAttributeInfo.setDescription(newDescription); + } + persistAspect(resourceUrn, Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo, actor, entityService); + } } + diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java index d6e6e5610da56a..f8b0adf29be8ca 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java @@ -12,435 +12,461 @@ import com.linkedin.metadata.entity.EntityService; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import javax.annotation.Nonnull; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nonnull; - -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; @Slf4j @RequiredArgsConstructor public class UpdateDescriptionResolver implements DataFetcher> { - private final EntityService _entityService; - private final EntityClient _entityClient; - - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - final DescriptionUpdateInput input = bindArgument(environment.getArgument("input"), DescriptionUpdateInput.class); - Urn targetUrn = Urn.createFromString(input.getResourceUrn()); - log.info("Updating description. input: {}", input.toString()); - switch (targetUrn.getEntityType()) { - case Constants.DATASET_ENTITY_NAME: - return updateDatasetSchemaFieldDescription(targetUrn, input, environment.getContext()); - case Constants.CONTAINER_ENTITY_NAME: - 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.GLOSSARY_NODE_ENTITY_NAME: - return updateGlossaryNodeDescription(targetUrn, input, environment.getContext()); - case Constants.TAG_ENTITY_NAME: - return updateTagDescription(targetUrn, input, environment.getContext()); - case Constants.CORP_GROUP_ENTITY_NAME: - return updateCorpGroupDescription(targetUrn, input, environment.getContext()); - case Constants.NOTEBOOK_ENTITY_NAME: - return updateNotebookDescription(targetUrn, input, environment.getContext()); - case Constants.ML_MODEL_ENTITY_NAME: - return updateMlModelDescription(targetUrn, input, environment.getContext()); - case Constants.ML_MODEL_GROUP_ENTITY_NAME: - return updateMlModelGroupDescription(targetUrn, input, environment.getContext()); - case Constants.ML_FEATURE_TABLE_ENTITY_NAME: - return updateMlFeatureTableDescription(targetUrn, input, environment.getContext()); - case Constants.ML_FEATURE_ENTITY_NAME: - return updateMlFeatureDescription(targetUrn, input, environment.getContext()); - case Constants.ML_PRIMARY_KEY_ENTITY_NAME: - return updateMlPrimaryKeyDescription(targetUrn, input, environment.getContext()); - case Constants.DATA_PRODUCT_ENTITY_NAME: - return updateDataProductDescription(targetUrn, input, environment.getContext()); - default: - throw new RuntimeException( - String.format("Failed to update description. Unsupported resource type %s provided.", targetUrn)); + private final EntityService _entityService; + private final EntityClient _entityClient; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final DescriptionUpdateInput input = bindArgument(environment.getArgument("input"), DescriptionUpdateInput.class); + Urn targetUrn = Urn.createFromString(input.getResourceUrn()); + log.info("Updating description. input: {}", input.toString()); + switch (targetUrn.getEntityType()) { + case Constants.DATASET_ENTITY_NAME: + return updateDatasetSchemaFieldDescription(targetUrn, input, environment.getContext()); + case Constants.CONTAINER_ENTITY_NAME: + 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.GLOSSARY_NODE_ENTITY_NAME: + return updateGlossaryNodeDescription(targetUrn, input, environment.getContext()); + case Constants.TAG_ENTITY_NAME: + return updateTagDescription(targetUrn, input, environment.getContext()); + case Constants.CORP_GROUP_ENTITY_NAME: + return updateCorpGroupDescription(targetUrn, input, environment.getContext()); + case Constants.NOTEBOOK_ENTITY_NAME: + return updateNotebookDescription(targetUrn, input, environment.getContext()); + case Constants.ML_MODEL_ENTITY_NAME: + return updateMlModelDescription(targetUrn, input, environment.getContext()); + case Constants.ML_MODEL_GROUP_ENTITY_NAME: + return updateMlModelGroupDescription(targetUrn, input, environment.getContext()); + case Constants.ML_FEATURE_TABLE_ENTITY_NAME: + return updateMlFeatureTableDescription(targetUrn, input, environment.getContext()); + case Constants.ML_FEATURE_ENTITY_NAME: + return updateMlFeatureDescription(targetUrn, input, environment.getContext()); + case Constants.ML_PRIMARY_KEY_ENTITY_NAME: + return updateMlPrimaryKeyDescription(targetUrn, input, environment.getContext()); + case Constants.DATA_PRODUCT_ENTITY_NAME: + return updateDataProductDescription(targetUrn, input, environment.getContext()); + case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME: + return updateBusinessAttributeDescription(targetUrn, input, environment.getContext()); + default: + throw new RuntimeException( + String.format("Failed to update description. Unsupported resource type %s provided.", targetUrn)); + } } - } - - private CompletableFuture updateContainerDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { - return CompletableFuture.supplyAsync(() -> { - - if (!DescriptionUtils.isAuthorizedToUpdateContainerDescription(context, targetUrn)) { - throw new AuthorizationException( - "Unauthorized to perform this action. Please contact your DataHub administrator."); - } - - DescriptionUtils.validateContainerInput(targetUrn, _entityService); - - try { - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - DescriptionUtils.updateContainerDescription( - 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 updateDomainDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { - return CompletableFuture.supplyAsync(() -> { - - if (!DescriptionUtils.isAuthorizedToUpdateDomainDescription(context, targetUrn)) { - throw new AuthorizationException( - "Unauthorized to perform this action. Please contact your DataHub administrator."); - } - DescriptionUtils.validateDomainInput(targetUrn, _entityService); + private CompletableFuture updateContainerDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { + return CompletableFuture.supplyAsync(() -> { + + if (!DescriptionUtils.isAuthorizedToUpdateContainerDescription(context, targetUrn)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + + DescriptionUtils.validateContainerInput(targetUrn, _entityService); + + try { + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + DescriptionUtils.updateContainerDescription( + 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 updateDomainDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { + return CompletableFuture.supplyAsync(() -> { + + if (!DescriptionUtils.isAuthorizedToUpdateDomainDescription(context, targetUrn)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + DescriptionUtils.validateDomainInput(targetUrn, _entityService); + + try { + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + DescriptionUtils.updateDomainDescription( + 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); + } + }); + } + + // If updating schema field description fails, try again on a sibling until there are no more siblings to try. Then throw if necessary. + private Boolean attemptUpdateDatasetSchemaFieldDescription( + @Nonnull final Urn targetUrn, + @Nonnull final DescriptionUpdateInput input, + @Nonnull final QueryContext context, + @Nonnull final HashSet attemptedUrns, + @Nonnull final List siblingUrns + ) { + attemptedUrns.add(targetUrn); try { - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - DescriptionUtils.updateDomainDescription( - input.getDescription(), - targetUrn, - actor, - _entityService); - return true; + DescriptionUtils.validateFieldDescriptionInput(targetUrn, input.getSubResource(), input.getSubResourceType(), + _entityService); + + final Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + DescriptionUtils.updateFieldDescription(input.getDescription(), targetUrn, input.getSubResource(), 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); + final Optional siblingUrn = SiblingsUtils.getNextSiblingUrn(siblingUrns, attemptedUrns); + + if (siblingUrn.isPresent()) { + log.warn("Failed to update description for input {}, trying sibling urn {} now.", input.toString(), siblingUrn.get()); + return attemptUpdateDatasetSchemaFieldDescription(siblingUrn.get(), input, context, attemptedUrns, siblingUrns); + } else { + 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); + } } - }); - } - - // If updating schema field description fails, try again on a sibling until there are no more siblings to try. Then throw if necessary. - private Boolean attemptUpdateDatasetSchemaFieldDescription( - @Nonnull final Urn targetUrn, - @Nonnull final DescriptionUpdateInput input, - @Nonnull final QueryContext context, - @Nonnull final HashSet attemptedUrns, - @Nonnull final List siblingUrns - ) { - attemptedUrns.add(targetUrn); - try { - DescriptionUtils.validateFieldDescriptionInput(targetUrn, input.getSubResource(), input.getSubResourceType(), - _entityService); - - final Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - DescriptionUtils.updateFieldDescription(input.getDescription(), targetUrn, input.getSubResource(), actor, - _entityService); - return true; - } catch (Exception e) { - final Optional siblingUrn = SiblingsUtils.getNextSiblingUrn(siblingUrns, attemptedUrns); - - if (siblingUrn.isPresent()) { - log.warn("Failed to update description for input {}, trying sibling urn {} now.", input.toString(), siblingUrn.get()); - return attemptUpdateDatasetSchemaFieldDescription(siblingUrn.get(), input, context, attemptedUrns, siblingUrns); - } else { - 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 updateDatasetSchemaFieldDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { - - return CompletableFuture.supplyAsync(() -> { - - if (!DescriptionUtils.isAuthorizedToUpdateFieldDescription(context, targetUrn)) { - throw new AuthorizationException( - "Unauthorized to perform this action. Please contact your DataHub administrator."); - } - - if (input.getSubResourceType() == null) { - throw new IllegalArgumentException("Update description without subresource is not currently supported"); - } - - List siblingUrns = SiblingsUtils.getSiblingUrns(targetUrn, _entityService); - - return attemptUpdateDatasetSchemaFieldDescription(targetUrn, input, context, new HashSet<>(), siblingUrns); - }); - } - - private CompletableFuture 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 updateGlossaryTermDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { - return CompletableFuture.supplyAsync(() -> { - final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); - if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn) - && !GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient) - ) { - 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); - } - }); - } - - private CompletableFuture updateGlossaryNodeDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { - return CompletableFuture.supplyAsync(() -> { - final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); - if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn) - && !GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient) - ) { - 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.updateGlossaryNodeDescription( - 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 updateCorpGroupDescription(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.validateCorpGroupInput(targetUrn, _entityService); - - try { - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - DescriptionUtils.updateCorpGroupDescription( - 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 updateNotebookDescription(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.validateNotebookInput(targetUrn, _entityService); - - try { - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - DescriptionUtils.updateNotebookDescription( - 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 updateMlModelDescription(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.updateMlModelDescription( - 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 updateMlModelGroupDescription(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.updateMlModelGroupDescription( - 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 updateMlFeatureDescription(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.updateMlFeatureDescription( - 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 updateMlPrimaryKeyDescription(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.updateMlPrimaryKeyDescription( - 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 updateMlFeatureTableDescription(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.updateMlFeatureTableDescription( - 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 updateDataProductDescription(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.updateDataProductDescription( - 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 updateDatasetSchemaFieldDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { + + return CompletableFuture.supplyAsync(() -> { + + if (!DescriptionUtils.isAuthorizedToUpdateFieldDescription(context, targetUrn)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + + if (input.getSubResourceType() == null) { + throw new IllegalArgumentException("Update description without subresource is not currently supported"); + } + + List siblingUrns = SiblingsUtils.getSiblingUrns(targetUrn, _entityService); + + return attemptUpdateDatasetSchemaFieldDescription(targetUrn, input, context, new HashSet<>(), siblingUrns); + }); + } + + private CompletableFuture 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 updateGlossaryTermDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { + return CompletableFuture.supplyAsync(() -> { + final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); + if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn) + && !GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient) + ) { + 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); + } + }); + } + + private CompletableFuture updateGlossaryNodeDescription(Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { + return CompletableFuture.supplyAsync(() -> { + final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); + if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn) + && !GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient) + ) { + 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.updateGlossaryNodeDescription( + 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 updateCorpGroupDescription(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.validateCorpGroupInput(targetUrn, _entityService); + + try { + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + DescriptionUtils.updateCorpGroupDescription( + 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 updateNotebookDescription(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.validateNotebookInput(targetUrn, _entityService); + + try { + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + DescriptionUtils.updateNotebookDescription( + 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 updateMlModelDescription(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.updateMlModelDescription( + 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 updateMlModelGroupDescription(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.updateMlModelGroupDescription( + 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 updateMlFeatureDescription(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.updateMlFeatureDescription( + 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 updateMlPrimaryKeyDescription(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.updateMlPrimaryKeyDescription( + 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 updateMlFeatureTableDescription(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.updateMlFeatureTableDescription( + 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 updateDataProductDescription(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.updateDataProductDescription( + 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 updateBusinessAttributeDescription(Urn targetUrn, DescriptionUpdateInput input, + QueryContext context) { + return CompletableFuture.supplyAsync(() -> { + //check if user has the rights to update description for business attribute + if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + + //validate label input + DescriptionUtils.validateLabelInput(targetUrn, _entityService); + + try { + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + DescriptionUtils.updateBusinessAttributeDescription(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); + } + }); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java index 0e316ac1296ee0..9bcfc7d5ee55be 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.resolvers.mutate; +import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.urn.CorpuserUrn; import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; @@ -10,6 +11,7 @@ import com.linkedin.datahub.graphql.exception.DataHubGraphQLException; import com.linkedin.datahub.graphql.generated.UpdateNameInput; import com.linkedin.datahub.graphql.resolvers.dataproduct.DataProductAuthorizationUtils; +import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; import com.linkedin.datahub.graphql.resolvers.mutate.util.DomainUtils; import com.linkedin.datahub.graphql.resolvers.mutate.util.GlossaryUtils; import com.linkedin.dataproduct.DataProductProperties; @@ -36,183 +38,216 @@ @RequiredArgsConstructor public class UpdateNameResolver implements DataFetcher> { - private final EntityService _entityService; - private final EntityClient _entityClient; - - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - final UpdateNameInput input = bindArgument(environment.getArgument("input"), UpdateNameInput.class); - Urn targetUrn = Urn.createFromString(input.getUrn()); - log.info("Updating name. input: {}", input); - - return CompletableFuture.supplyAsync(() -> { - if (!_entityService.exists(targetUrn)) { - throw new IllegalArgumentException(String.format("Failed to update %s. %s does not exist.", targetUrn, targetUrn)); - } - - switch (targetUrn.getEntityType()) { - case Constants.GLOSSARY_TERM_ENTITY_NAME: - return updateGlossaryTermName(targetUrn, input, environment.getContext()); - case Constants.GLOSSARY_NODE_ENTITY_NAME: - return updateGlossaryNodeName(targetUrn, input, environment.getContext()); - case Constants.DOMAIN_ENTITY_NAME: - return updateDomainName(targetUrn, input, environment.getContext()); - case Constants.CORP_GROUP_ENTITY_NAME: - return updateGroupName(targetUrn, input, environment.getContext()); - case Constants.DATA_PRODUCT_ENTITY_NAME: - return updateDataProductName(targetUrn, input, environment.getContext()); - default: - throw new RuntimeException( - String.format("Failed to update name. Unsupported resource type %s provided.", targetUrn)); - } - }); - } - - private Boolean updateGlossaryTermName( - Urn targetUrn, - UpdateNameInput input, - QueryContext context - ) { - final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); - if (GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient)) { - try { - GlossaryTermInfo glossaryTermInfo = (GlossaryTermInfo) EntityUtils.getAspectFromEntity( - targetUrn.toString(), Constants.GLOSSARY_TERM_INFO_ASPECT_NAME, _entityService, null); - if (glossaryTermInfo == null) { - throw new IllegalArgumentException("Glossary Term does not exist"); - } - glossaryTermInfo.setName(input.getName()); - Urn actor = UrnUtils.getUrn(context.getActorUrn()); - persistAspect(targetUrn, Constants.GLOSSARY_TERM_INFO_ASPECT_NAME, glossaryTermInfo, actor, _entityService); - - return true; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); - } + private final EntityService _entityService; + private final EntityClient _entityClient; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final UpdateNameInput input = bindArgument(environment.getArgument("input"), UpdateNameInput.class); + Urn targetUrn = Urn.createFromString(input.getUrn()); + log.info("Updating name. input: {}", input); + + return CompletableFuture.supplyAsync(() -> { + if (!_entityService.exists(targetUrn)) { + throw new IllegalArgumentException(String.format("Failed to update %s. %s does not exist.", targetUrn, targetUrn)); + } + + switch (targetUrn.getEntityType()) { + case Constants.GLOSSARY_TERM_ENTITY_NAME: + return updateGlossaryTermName(targetUrn, input, environment.getContext()); + case Constants.GLOSSARY_NODE_ENTITY_NAME: + return updateGlossaryNodeName(targetUrn, input, environment.getContext()); + case Constants.DOMAIN_ENTITY_NAME: + return updateDomainName(targetUrn, input, environment.getContext()); + case Constants.CORP_GROUP_ENTITY_NAME: + return updateGroupName(targetUrn, input, environment.getContext()); + case Constants.DATA_PRODUCT_ENTITY_NAME: + return updateDataProductName(targetUrn, input, environment.getContext()); + case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME: + return updateBusinessAttributeName(targetUrn, input, environment.getContext()); + default: + throw new RuntimeException( + String.format("Failed to update name. Unsupported resource type %s provided.", targetUrn)); + } + }); } - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - - private Boolean updateGlossaryNodeName( - Urn targetUrn, - UpdateNameInput input, - QueryContext context - ) { - final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); - if (GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient)) { - try { - GlossaryNodeInfo glossaryNodeInfo = (GlossaryNodeInfo) EntityUtils.getAspectFromEntity( - targetUrn.toString(), Constants.GLOSSARY_NODE_INFO_ASPECT_NAME, _entityService, null); - if (glossaryNodeInfo == null) { - throw new IllegalArgumentException("Glossary Node does not exist"); + + private Boolean updateGlossaryTermName( + Urn targetUrn, + UpdateNameInput input, + QueryContext context + ) { + final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); + if (GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient)) { + try { + GlossaryTermInfo glossaryTermInfo = (GlossaryTermInfo) EntityUtils.getAspectFromEntity( + targetUrn.toString(), Constants.GLOSSARY_TERM_INFO_ASPECT_NAME, _entityService, null); + if (glossaryTermInfo == null) { + throw new IllegalArgumentException("Glossary Term does not exist"); + } + glossaryTermInfo.setName(input.getName()); + Urn actor = UrnUtils.getUrn(context.getActorUrn()); + persistAspect(targetUrn, Constants.GLOSSARY_TERM_INFO_ASPECT_NAME, glossaryTermInfo, actor, _entityService); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + } } - glossaryNodeInfo.setName(input.getName()); - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - persistAspect(targetUrn, Constants.GLOSSARY_NODE_INFO_ASPECT_NAME, glossaryNodeInfo, actor, _entityService); - - return true; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); - } + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); } - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - - private Boolean updateDomainName( - Urn targetUrn, - UpdateNameInput input, - QueryContext context - ) { - if (AuthorizationUtils.canManageDomains(context)) { - try { - DomainProperties domainProperties = (DomainProperties) EntityUtils.getAspectFromEntity( - targetUrn.toString(), Constants.DOMAIN_PROPERTIES_ASPECT_NAME, _entityService, null); - - if (domainProperties == null) { - throw new IllegalArgumentException("Domain does not exist"); - } - if (DomainUtils.hasNameConflict(input.getName(), DomainUtils.getParentDomainSafely(domainProperties), context, _entityClient)) { - throw new DataHubGraphQLException( - String.format("\"%s\" already exists in this domain. Please pick a unique name.", input.getName()), - DataHubGraphQLErrorCode.CONFLICT - ); + private Boolean updateGlossaryNodeName( + Urn targetUrn, + UpdateNameInput input, + QueryContext context + ) { + final Urn parentNodeUrn = GlossaryUtils.getParentUrn(targetUrn, context, _entityClient); + if (GlossaryUtils.canManageChildrenEntities(context, parentNodeUrn, _entityClient)) { + try { + GlossaryNodeInfo glossaryNodeInfo = (GlossaryNodeInfo) EntityUtils.getAspectFromEntity( + targetUrn.toString(), Constants.GLOSSARY_NODE_INFO_ASPECT_NAME, _entityService, null); + if (glossaryNodeInfo == null) { + throw new IllegalArgumentException("Glossary Node does not exist"); + } + glossaryNodeInfo.setName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect(targetUrn, Constants.GLOSSARY_NODE_INFO_ASPECT_NAME, glossaryNodeInfo, actor, _entityService); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + } } - - domainProperties.setName(input.getName()); - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - persistAspect(targetUrn, Constants.DOMAIN_PROPERTIES_ASPECT_NAME, domainProperties, actor, _entityService); - - return true; - } catch (DataHubGraphQLException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); - } + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); } - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - - private Boolean updateGroupName( - Urn targetUrn, - UpdateNameInput input, - QueryContext context - ) { - if (AuthorizationUtils.canManageUsersAndGroups(context)) { - try { - CorpGroupInfo corpGroupInfo = (CorpGroupInfo) EntityUtils.getAspectFromEntity( - targetUrn.toString(), Constants.CORP_GROUP_INFO_ASPECT_NAME, _entityService, null); - if (corpGroupInfo == null) { - throw new IllegalArgumentException("Group does not exist"); + + private Boolean updateDomainName( + Urn targetUrn, + UpdateNameInput input, + QueryContext context + ) { + if (AuthorizationUtils.canManageDomains(context)) { + try { + DomainProperties domainProperties = (DomainProperties) EntityUtils.getAspectFromEntity( + targetUrn.toString(), Constants.DOMAIN_PROPERTIES_ASPECT_NAME, _entityService, null); + + if (domainProperties == null) { + throw new IllegalArgumentException("Domain does not exist"); + } + + if (DomainUtils.hasNameConflict(input.getName(), DomainUtils.getParentDomainSafely(domainProperties), context, _entityClient)) { + throw new DataHubGraphQLException( + String.format("\"%s\" already exists in this domain. Please pick a unique name.", input.getName()), + DataHubGraphQLErrorCode.CONFLICT + ); + } + + domainProperties.setName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect(targetUrn, Constants.DOMAIN_PROPERTIES_ASPECT_NAME, domainProperties, actor, _entityService); + + return true; + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + } } - corpGroupInfo.setDisplayName(input.getName()); - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - persistAspect(targetUrn, Constants.CORP_GROUP_INFO_ASPECT_NAME, corpGroupInfo, actor, _entityService); - - return true; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); - } + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); } - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - - private Boolean updateDataProductName( - Urn targetUrn, - UpdateNameInput input, - QueryContext context - ) { - try { - DataProductProperties dataProductProperties = (DataProductProperties) EntityUtils.getAspectFromEntity( - targetUrn.toString(), Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME, _entityService, null); - if (dataProductProperties == null) { - throw new IllegalArgumentException("Data Product does not exist"); - } - - Domains dataProductDomains = (Domains) EntityUtils.getAspectFromEntity( - targetUrn.toString(), Constants.DOMAINS_ASPECT_NAME, _entityService, null); - if (dataProductDomains != null && dataProductDomains.hasDomains() && dataProductDomains.getDomains().size() > 0) { - // get first domain since we only allow one domain right now - Urn domainUrn = UrnUtils.getUrn(dataProductDomains.getDomains().get(0).toString()); - // if they can't edit a data product from either the parent domain permission or from permission on the data product itself, throw error - if (!DataProductAuthorizationUtils.isAuthorizedToManageDataProducts(context, domainUrn) - && !DataProductAuthorizationUtils.isAuthorizedToEditDataProduct(context, targetUrn)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - } else { - // should not happen since data products need to have a domain - if (!DataProductAuthorizationUtils.isAuthorizedToEditDataProduct(context, targetUrn)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + + private Boolean updateGroupName( + Urn targetUrn, + UpdateNameInput input, + QueryContext context + ) { + if (AuthorizationUtils.canManageUsersAndGroups(context)) { + try { + CorpGroupInfo corpGroupInfo = (CorpGroupInfo) EntityUtils.getAspectFromEntity( + targetUrn.toString(), Constants.CORP_GROUP_INFO_ASPECT_NAME, _entityService, null); + if (corpGroupInfo == null) { + throw new IllegalArgumentException("Group does not exist"); + } + corpGroupInfo.setDisplayName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect(targetUrn, Constants.CORP_GROUP_INFO_ASPECT_NAME, corpGroupInfo, actor, _entityService); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + } } - } + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } - dataProductProperties.setName(input.getName()); - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - persistAspect(targetUrn, Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME, dataProductProperties, actor, _entityService); + private Boolean updateDataProductName( + Urn targetUrn, + UpdateNameInput input, + QueryContext context + ) { + try { + DataProductProperties dataProductProperties = (DataProductProperties) EntityUtils.getAspectFromEntity( + targetUrn.toString(), Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME, _entityService, null); + if (dataProductProperties == null) { + throw new IllegalArgumentException("Data Product does not exist"); + } + + Domains dataProductDomains = (Domains) EntityUtils.getAspectFromEntity( + targetUrn.toString(), Constants.DOMAINS_ASPECT_NAME, _entityService, null); + if (dataProductDomains != null && dataProductDomains.hasDomains() && dataProductDomains.getDomains().size() > 0) { + // get first domain since we only allow one domain right now + Urn domainUrn = UrnUtils.getUrn(dataProductDomains.getDomains().get(0).toString()); + // if they can't edit a data product from either the parent domain permission or from permission on the data product itself, throw error + if (!DataProductAuthorizationUtils.isAuthorizedToManageDataProducts(context, domainUrn) + && !DataProductAuthorizationUtils.isAuthorizedToEditDataProduct(context, targetUrn)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + } else { + // should not happen since data products need to have a domain + if (!DataProductAuthorizationUtils.isAuthorizedToEditDataProduct(context, targetUrn)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + } + + dataProductProperties.setName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect(targetUrn, Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME, dataProductProperties, actor, _entityService); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + } + } - return true; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + private Boolean updateBusinessAttributeName( + Urn targetUrn, + UpdateNameInput input, + QueryContext context + ) { + try { + BusinessAttributeInfo businessAttributeInfo = (BusinessAttributeInfo) EntityUtils.getAspectFromEntity( + targetUrn.toString(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, _entityService, null); + if (businessAttributeInfo == null) { + throw new IllegalArgumentException("Business Attribute does not exist"); + } + + if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { + throw new DataHubGraphQLException( + String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", input.getName()), + DataHubGraphQLErrorCode.CONFLICT + ); + } + + businessAttributeInfo.setFieldPath(input.getName()); + businessAttributeInfo.setName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect(targetUrn, Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo, actor, _entityService); + return true; + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + } } - } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java new file mode 100644 index 00000000000000..ff9e7827a27706 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java @@ -0,0 +1,95 @@ +package com.linkedin.datahub.graphql.resolvers.mutate.util; + + +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Condition; +import com.linkedin.metadata.query.filter.ConjunctiveCriterion; +import com.linkedin.metadata.query.filter.ConjunctiveCriterionArray; +import com.linkedin.metadata.query.filter.Criterion; +import com.linkedin.metadata.query.filter.CriterionArray; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchResult; +import com.linkedin.r2.RemoteInvocationException; +import com.linkedin.schema.ArrayType; +import com.linkedin.schema.BooleanType; +import com.linkedin.schema.DateType; +import com.linkedin.schema.NumberType; +import com.linkedin.schema.SchemaFieldDataType; +import com.linkedin.schema.StringType; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nonnull; +import java.util.Objects; + +@Slf4j +public class BusinessAttributeUtils { + private static final Integer DEFAULT_START = 0; + private static final Integer DEFAULT_COUNT = 1000; + private static final String DEFAULT_QUERY = ""; + private static final String NAME_INDEX_FIELD_NAME = "name"; + + private BusinessAttributeUtils() { + } + + public static boolean hasNameConflict(String name, QueryContext context, EntityClient entityClient) { + Filter filter = buildNameFilter(name); + try { + final SearchResult gmsResult = entityClient.search( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + DEFAULT_QUERY, + filter, + null, + DEFAULT_START, + DEFAULT_COUNT, + context.getAuthentication(), + new SearchFlags().setFulltext(true)); + return gmsResult.getNumEntities() > 0; + } catch (RemoteInvocationException e) { + throw new RuntimeException("Failed to fetch Business Attributes", e); + } + } + + private static Filter buildNameFilter(String name) { + return new Filter().setOr( + new ConjunctiveCriterionArray( + new ConjunctiveCriterion().setAnd(buildNameCriterion(name)) + ) + ); + } + + private static CriterionArray buildNameCriterion(@Nonnull final String name) { + return new CriterionArray(new Criterion() + .setField(NAME_INDEX_FIELD_NAME) + .setValue(name) + .setCondition(Condition.EQUAL)); + } + + public static SchemaFieldDataType mapSchemaFieldDataType(com.linkedin.datahub.graphql.generated.SchemaFieldDataType type) { + if (Objects.isNull(type)) { + return null; + } + SchemaFieldDataType schemaFieldDataType = new SchemaFieldDataType(); + switch (type) { + case BOOLEAN: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new BooleanType())); + return schemaFieldDataType; + case STRING: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new StringType())); + return schemaFieldDataType; + case NUMBER: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new NumberType())); + return schemaFieldDataType; + case DATE: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new DateType())); + return schemaFieldDataType; + case ARRAY: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new ArrayType())); + return schemaFieldDataType; + default: + return null; + } + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java index a93c7d5b333da1..4d0312db754a70 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java @@ -1,6 +1,9 @@ package com.linkedin.datahub.graphql.resolvers.mutate.util; +import com.datahub.authorization.ConjunctivePrivilegeGroup; +import com.datahub.authorization.DisjunctivePrivilegeGroup; import com.google.common.collect.ImmutableList; +import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTermAssociation; import com.linkedin.common.GlossaryTermAssociationArray; @@ -13,8 +16,6 @@ import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; -import com.datahub.authorization.ConjunctivePrivilegeGroup; -import com.datahub.authorization.DisjunctivePrivilegeGroup; import com.linkedin.datahub.graphql.generated.ResourceRefInput; import com.linkedin.datahub.graphql.generated.SubResourceType; import com.linkedin.metadata.Constants; @@ -24,13 +25,17 @@ import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.schema.EditableSchemaFieldInfo; import com.linkedin.schema.EditableSchemaMetadata; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nonnull; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; -import javax.annotation.Nonnull; -import lombok.extern.slf4j.Slf4j; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.persistAspect; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.validateSubresourceExists; // TODO: Move to consuming GlossaryTermService, TagService. @@ -289,6 +294,10 @@ private static MetadataChangeProposal buildAddTagsProposal( ) throws URISyntaxException { if (resource.getSubResource() == null || resource.getSubResource().equals("")) { // Case 1: Adding tags to a top-level entity + Urn targetUrn = Urn.createFromString(resource.getResourceUrn()); + if (targetUrn.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { + return buildAddTagsToBusinessAttributeProposal(tagUrns, resource, actor, entityService); + } return buildAddTagsToEntityProposal(tagUrns, resource, actor, entityService); } else { // Case 2: Adding tags to subresource (e.g. schema fields) @@ -304,6 +313,10 @@ private static MetadataChangeProposal buildRemoveTagsProposal( ) throws URISyntaxException { if (resource.getSubResource() == null || resource.getSubResource().equals("")) { // Case 1: Adding tags to a top-level entity + Urn targetUrn = Urn.createFromString(resource.getResourceUrn()); + if (targetUrn.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { + return buildRemoveTagsToBusinessAttributeProposal(tagUrns, resource, actor, entityService); + } return buildRemoveTagsToEntityProposal(tagUrns, resource, actor, entityService); } else { // Case 2: Adding tags to subresource (e.g. schema fields) @@ -422,6 +435,10 @@ private static MetadataChangeProposal buildAddTermsProposal( ) throws URISyntaxException { if (resource.getSubResource() == null || resource.getSubResource().equals("")) { // Case 1: Adding terms to a top-level entity + Urn targetUrn = Urn.createFromString(resource.getResourceUrn()); + if (targetUrn.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { + return buildAddTermsToBusinessAttributeProposal(termUrns, resource, actor, entityService); + } return buildAddTermsToEntityProposal(termUrns, resource, actor, entityService); } else { // Case 2: Adding terms to subresource (e.g. schema fields) @@ -437,6 +454,10 @@ private static MetadataChangeProposal buildRemoveTermsProposal( ) throws URISyntaxException { if (resource.getSubResource() == null || resource.getSubResource().equals("")) { // Case 1: Removing terms from a top-level entity + Urn targetUrn = Urn.createFromString(resource.getResourceUrn()); + if (targetUrn.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { + return buildRemoveTermsToBusinessAttributeProposal(termUrns, resource, actor, entityService); + } return buildRemoveTermsToEntityProposal(termUrns, resource, actor, entityService); } else { // Case 2: Removing terms from subresource (e.g. schema fields) @@ -557,4 +578,70 @@ private static GlossaryTermAssociationArray removeTermsIfExists(GlossaryTerms te } return termAssociationArray; } + + private static MetadataChangeProposal buildAddTagsToBusinessAttributeProposal( + List tagUrns, + ResourceRefInput resource, + Urn actor, + EntityService entityService + ) throws URISyntaxException { + BusinessAttributeInfo businessAttributeInfo = + (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, new GlobalTags()); + + if (!businessAttributeInfo.hasGlobalTags()) { + businessAttributeInfo.setGlobalTags(new GlobalTags()); + } + addTagsIfNotExists(businessAttributeInfo.getGlobalTags(), tagUrns); + return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + } + + private static MetadataChangeProposal buildAddTermsToBusinessAttributeProposal( + List termUrns, + ResourceRefInput resource, + Urn actor, + EntityService entityService + ) throws URISyntaxException { + BusinessAttributeInfo businessAttributeInfo = + (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, new GlossaryTerms()); + if (!businessAttributeInfo.hasGlossaryTerms()) { + businessAttributeInfo.setGlossaryTerms(new GlossaryTerms()); + } + businessAttributeInfo.getGlossaryTerms().setAuditStamp(EntityUtils.getAuditStamp(actor)); + addTermsIfNotExists(businessAttributeInfo.getGlossaryTerms(), termUrns); + return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + } + + private static MetadataChangeProposal buildRemoveTagsToBusinessAttributeProposal( + List tagUrns, + ResourceRefInput resource, + Urn actor, + EntityService entityService) { + BusinessAttributeInfo businessAttributeInfo = + (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, new GlobalTags()); + + if (!businessAttributeInfo.hasGlobalTags()) { + businessAttributeInfo.setGlobalTags(new GlobalTags()); + } + removeTagsIfExists(businessAttributeInfo.getGlobalTags(), tagUrns); + return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + } + + private static MetadataChangeProposal buildRemoveTermsToBusinessAttributeProposal( + List termUrns, + ResourceRefInput resource, + Urn actor, + EntityService entityService) { + BusinessAttributeInfo businessAttributeInfo = + (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, new GlossaryTerms()); + if (!businessAttributeInfo.hasGlossaryTerms()) { + businessAttributeInfo.setGlossaryTerms(new GlossaryTerms()); + } + removeTermsIfExists(businessAttributeInfo.getGlossaryTerms(), termUrns); + return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + } + } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java index fb146ef72877d1..0533a515128222 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java @@ -76,6 +76,8 @@ private SearchUtils() { EntityType.DATA_PRODUCT, EntityType.NOTEBOOK); + //TODO: add business attributes to the list of searchable fields + /** * Entities that are part of autocomplete by default in Auto Complete Across Entities diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java new file mode 100644 index 00000000000000..eb98bbd5fc9ca8 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java @@ -0,0 +1,84 @@ +package com.linkedin.datahub.graphql.types.businessattribute; + +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; +import com.linkedin.datahub.graphql.generated.Entity; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributeMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import graphql.execution.DataFetcherResult; +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME; +import static com.linkedin.metadata.Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME; +import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME; +import static com.linkedin.metadata.Constants.STATUS_ASPECT_NAME; + +@RequiredArgsConstructor +public class BusinessAttributeType implements com.linkedin.datahub.graphql.types.EntityType { + + public static final Set ASPECTS_TO_FETCH = ImmutableSet.of( + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME, + OWNERSHIP_ASPECT_NAME, + INSTITUTIONAL_MEMORY_ASPECT_NAME, + STATUS_ASPECT_NAME + ); + + private final EntityClient _entityClient; + + @Override + public EntityType type() { + return EntityType.BUSINESS_ATTRIBUTE; + } + + @Override + public Function getKeyProvider() { + return Entity::getUrn; + } + + @Override + public Class objectClass() { + return BusinessAttribute.class; + } + + @Override + public List> batchLoad(@Nonnull List urns, @Nonnull QueryContext context) throws Exception { + final List businessAttributeUrns = urns.stream() + .map(UrnUtils::getUrn) + .collect(Collectors.toList()); + + try { + final Map businessAttributeMap = _entityClient.batchGetV2(BUSINESS_ATTRIBUTE_ENTITY_NAME, + new HashSet<>(businessAttributeUrns), ASPECTS_TO_FETCH, context.getAuthentication()); + + final List gmsResults = new ArrayList<>(); + for (Urn urn : businessAttributeUrns) { + gmsResults.add(businessAttributeMap.getOrDefault(urn, null)); + } + return gmsResults.stream() + .map(gmsResult -> gmsResult == null ? null + : DataFetcherResult.newResult() + .data(BusinessAttributeMapper.map(gmsResult)) + .build()) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Failed to batch load Business Attributes", e); + } + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java new file mode 100644 index 00000000000000..1008fcf18cf29e --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -0,0 +1,67 @@ +package com.linkedin.datahub.graphql.types.businessattribute.mappers; + +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.common.Ownership; +import com.linkedin.data.DataMap; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper; +import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; +import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; +import com.linkedin.datahub.graphql.types.mappers.ModelMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspectMap; + +import javax.annotation.Nonnull; + +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME; + +public class BusinessAttributeMapper implements ModelMapper { + + public static final BusinessAttributeMapper INSTANCE = new BusinessAttributeMapper(); + + public static BusinessAttribute map(@Nonnull final EntityResponse entityResponse) { + return INSTANCE.apply(entityResponse); + } + + @Override + public BusinessAttribute apply(@Nonnull final EntityResponse entityResponse) { + BusinessAttribute result = new BusinessAttribute(); + result.setUrn(entityResponse.getUrn().toString()); + result.setType(EntityType.BUSINESS_ATTRIBUTE); + + EnvelopedAspectMap aspectMap = entityResponse.getAspects(); + MappingHelper mappingHelper = new MappingHelper<>(aspectMap, result); + mappingHelper.mapToResult(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, ((businessAttribute, dataMap) -> + mapBusinessAttributeInfo(businessAttribute, dataMap))); + mappingHelper.mapToResult(OWNERSHIP_ASPECT_NAME, (businessAttribute, dataMap) -> + businessAttribute.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityResponse.getUrn()))); + return mappingHelper.getResult(); + } + + private void mapBusinessAttributeInfo(BusinessAttribute businessAttribute, DataMap dataMap) { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(dataMap); + com.linkedin.datahub.graphql.generated.BusinessAttributeInfo attributeInfo = new com.linkedin.datahub.graphql.generated.BusinessAttributeInfo(); + if (businessAttributeInfo.hasFieldPath()) { + attributeInfo.setName(businessAttributeInfo.getFieldPath()); + } + if (businessAttributeInfo.hasDescription()) { + attributeInfo.setDescription(businessAttributeInfo.getDescription()); + } + if (businessAttributeInfo.hasCreated()) { + attributeInfo.setCreated(AuditStampMapper.map(businessAttributeInfo.getCreated())); + } + if (businessAttributeInfo.hasLastModified()) { + attributeInfo.setLastModified(AuditStampMapper.map(businessAttributeInfo.getLastModified())); + } + if (businessAttributeInfo.hasGlobalTags()) { + + } + if (businessAttributeInfo.hasGlossaryTerms()) { + + } + businessAttribute.setBusinessAttributeInfo(attributeInfo); + } + +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java index 34bf56a396b620..e8c6b7666a804c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java @@ -2,6 +2,7 @@ import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.generated.Assertion; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.generated.Chart; import com.linkedin.datahub.graphql.generated.Container; import com.linkedin.datahub.graphql.generated.CorpGroup; @@ -193,6 +194,11 @@ public Entity apply(Urn input) { ((OwnershipTypeEntity) partialEntity).setUrn(input.toString()); ((OwnershipTypeEntity) partialEntity).setType(EntityType.CUSTOM_OWNERSHIP_TYPE); } + if (input.getEntityType().equals(BUSINESS_ATTRIBUTE_ENTITY_NAME)) { + partialEntity = new BusinessAttribute(); + ((BusinessAttribute) partialEntity).setUrn(input.toString()); + ((BusinessAttribute) partialEntity).setType(EntityType.BUSINESS_ATTRIBUTE); + } return partialEntity; } } diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index 075a3b0fac43bc..e2df6deec0d278 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -130,6 +130,17 @@ type PlatformPrivileges { Whether the user can create and delete posts pinned to the home page. """ manageGlobalAnnouncements: Boolean! + + """ + Whether the user can create Business Attributes. + """ + createBusinessAttributes: Boolean! + + """ + Whether the user can manage Business Attributes. + """ + manageBusinessAttributes: Boolean! + } """ diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 035f756a10d557..c8a80234a2d4ea 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -231,6 +231,12 @@ type Query { Fetch a Data Platform Instance by primary key (urn) """ dataPlatformInstance(urn: String!): DataPlatformInstance + + """ + Fetch a Business Attribute by primary key (urn) + """ + businessAttribute(urn: String!): BusinessAttribute + } """ @@ -695,6 +701,29 @@ type Mutation { deleteOwnershipType( "Urn of the Custom Ownership Type to remove." urn: String!, deleteReferences: Boolean): Boolean + + """ + Create Business Attribute Api + """ + createBusinessAttribute( + "Inputs required to create a new BusinessAttribute." + input: CreateBusinessAttributeInput!): String + + """ + Delete a Business Attribute by urn. + """ + deleteBusinessAttribute( + "Urn of the business attribute to remove." + urn: String!): Boolean + + """ + Update Business Attribute + """ + updateBusinessAttribute( + "The urn identifier for the Business Attribute to update." + urn: String!, + "Inputs required to create a new Business Attribute." + input: UpdateBusinessAttributeInput!): BusinessAttribute } """ @@ -905,6 +934,11 @@ enum EntityType { A Role from an organisation """ ROLE + + """ + A Business Attribute + """ + BUSINESS_ATTRIBUTE } """ @@ -11229,4 +11263,144 @@ input UpdateOwnershipTypeInput { The description of the Custom Ownership Type """ description: String +} + +""" +A Business Attribute, or a logical schema Field +""" +type BusinessAttribute implements Entity { + """ + The primary key of the Data Product + """ + urn: String! + + """ + A standard Entity Type + """ + type: EntityType! + + """ + Properties about a Business Attribute + """ + businessAttributeInfo: BusinessAttributeInfo + + """ + Ownership metadata of the Business Attribute + """ + ownership: Ownership + + """ + References to internal resources related to Business Attribute + """ + institutionalMemory: InstitutionalMemory + + """ + Status of the Dataset + """ + status: Status + + """ + List of relationships between the source Entity and some destination entities with a given types + """ + relationships(input: RelationshipsInput!): EntityRelationshipsResult +} + +""" +Business Attribute type +""" + +type BusinessAttributeInfo { + + """ + name of the business attribute + """ + name: String! + + """ + description of business attribute + """ + description: String + + """ + Tags associated with the business attribute + """ + tags: GlobalTags + + """ + Glossary terms associated with the business attribute + """ + glossaryTerms: GlossaryTerms + + """ + Platform independent field type of the field + """ + type: SchemaFieldDataType + + """ + A list of platform specific metadata tuples + """ + customProperties: [CustomPropertiesEntry!] + + """ + An AuditStamp corresponding to the creation of this chart + """ + created: AuditStamp! + + """ + An AuditStamp corresponding to the modification of this chart + """ + lastModified: AuditStamp! + + """ + An optional AuditStamp corresponding to the deletion of this chart + """ + deleted: AuditStamp +} + +""" +Input required for creating a BusinessAttribute. +""" +input CreateBusinessAttributeInput { + """ + Input required for creating a BusinessAttributeInfo + """ + businessAttributeInfo: BusinessAttributeInfoInput! + +} + +input BusinessAttributeInfoInput { + """ + name of the business attribute + """ + name: String! + + """ + description of business attribute + """ + description: String + + """ + Platform independent field type of the field + """ + type: SchemaFieldDataType +} + +""" +Input required to update Business Attribute +""" +input UpdateBusinessAttributeInput { + """ + name of the business attribute + """ + name: String + + """ + business attribute description + """ + description: String + + """ + type + """ + type: SchemaFieldDataType } \ No newline at end of file diff --git a/datahub-web-react/src/graphql/me.graphql b/datahub-web-react/src/graphql/me.graphql index af850c9c3ce286..2bcd58e396ff4f 100644 --- a/datahub-web-react/src/graphql/me.graphql +++ b/datahub-web-react/src/graphql/me.graphql @@ -47,6 +47,9 @@ query getMe { manageGlobalViews manageOwnershipTypes manageGlobalAnnouncements + createBusinessAttributes + manageBusinessAttributes + } } } diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index 972f52b8824ceb..c06215c8aea700 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -73,6 +73,7 @@ public class Constants { public static final String QUERY_ENTITY_NAME = "query"; public static final String DATA_PRODUCT_ENTITY_NAME = "dataProduct"; public static final String OWNERSHIP_TYPE_ENTITY_NAME = "ownershipType"; + public static final String BUSINESS_ATTRIBUTE_ENTITY_NAME = "businessAttribute"; /** * Aspects @@ -304,6 +305,10 @@ public class Constants { public static final String CHANGE_EVENT_PLATFORM_EVENT_NAME = "entityChangeEvent"; + //Business Attribute + public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; + public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; + /** * Retention */ diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl new file mode 100644 index 00000000000000..9a7d8922940300 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl @@ -0,0 +1,26 @@ +namespace com.linkedin.businessattribute + +import com.linkedin.schema.SchemaFieldDataType +import com.linkedin.schema.EditableSchemaFieldBase +import com.linkedin.common.CustomProperties +import com.linkedin.common.ChangeAuditStamps + +/** + * Properties associated with a BusinessAttribute + */ +@Aspect = { + "name": "businessAttributeInfo" +} +record BusinessAttributeInfo includes EditableSchemaFieldBase, CustomProperties, ChangeAuditStamps { + /** + * Display name of the BusinessAttribute + */ + @Searchable = { + "fieldType": "WORD_GRAM", + "enableAutocomplete": true, + "boostScore": 10.0, + "fieldNameAliases": [ "_entityName" ] + } + name: string + type: optional SchemaFieldDataType +} diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl new file mode 100644 index 00000000000000..648a35d79534a7 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl @@ -0,0 +1,14 @@ +namespace com.linkedin.businessattribute + +/** + * Key for a Query + */ +@Aspect = { + "name": "businessAttributeKey" +} +record BusinessAttributeKey { + /** + * A unique id for the Data Product. + */ + id: string +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl new file mode 100644 index 00000000000000..c68ca97c939be3 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl @@ -0,0 +1,61 @@ +namespace com.linkedin.schema + +import com.linkedin.common.GlobalTags +import com.linkedin.common.GlossaryTerms + +/** +* Base class to describe metadata related to dataset schema. +*/ + +record EditableSchemaFieldBase { + /** + * FieldPath uniquely identifying the SchemaField this metadata is associated with + */ + fieldPath: string + + /** + * Description + */ + @Searchable = { + "fieldName": "editedFieldDescriptions", + "fieldType": "TEXT", + "boostScore": 0.1 + } + description: optional string + + /** + * Tags associated with the field + */ + @Relationship = { + "/tags/*/tag": { + "name": "EditableSchemaFieldTaggedWith", + "entityTypes": [ "tag" ] + } + } + @Searchable = { + "/tags/*/tag": { + "fieldName": "editedFieldTags", + "fieldType": "URN", + "boostScore": 0.5 + } + } + globalTags: optional GlobalTags + + /** + * Glossary terms associated with the field + */ + @Relationship = { + "/terms/*/urn": { + "name": "EditableSchemaFieldWithGlossaryTerm", + "entityTypes": [ "glossaryTerm" ] + } + } + @Searchable = { + "/terms/*/urn": { + "fieldName": "editedFieldGlossaryTerms", + "fieldType": "URN", + "boostScore": 0.5 + } + } + glossaryTerms: optional GlossaryTerms +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl index 4e6e135ae05da5..2f3253deeb5fc3 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl @@ -1,60 +1,8 @@ namespace com.linkedin.schema -import com.linkedin.common.GlobalTags -import com.linkedin.common.GlossaryTerms - /** * SchemaField to describe metadata related to dataset schema. */ -record EditableSchemaFieldInfo { - /** - * FieldPath uniquely identifying the SchemaField this metadata is associated with - */ - fieldPath: string - - /** - * Description - */ - @Searchable = { - "fieldName": "editedFieldDescriptions", - "fieldType": "TEXT", - "boostScore": 0.1 - } - description: optional string - - /** - * Tags associated with the field - */ - @Relationship = { - "/tags/*/tag": { - "name": "EditableSchemaFieldTaggedWith", - "entityTypes": [ "tag" ] - } - } - @Searchable = { - "/tags/*/tag": { - "fieldName": "editedFieldTags", - "fieldType": "URN", - "boostScore": 0.5 - } - } - globalTags: optional GlobalTags +record EditableSchemaFieldInfo includes EditableSchemaFieldBase { - /** - * Glossary terms associated with the field - */ - @Relationship = { - "/terms/*/urn": { - "name": "EditableSchemaFieldWithGlossaryTerm", - "entityTypes": [ "glossaryTerm" ] - } - } - @Searchable = { - "/terms/*/urn": { - "fieldName": "editedFieldGlossaryTerms", - "fieldType": "URN", - "boostScore": 0.5 - } - } - glossaryTerms: optional GlossaryTerms } diff --git a/metadata-models/src/main/resources/entity-registry.yml b/metadata-models/src/main/resources/entity-registry.yml index a5296d074093be..40b4f98debe71b 100644 --- a/metadata-models/src/main/resources/entity-registry.yml +++ b/metadata-models/src/main/resources/entity-registry.yml @@ -459,6 +459,14 @@ entities: aspects: - ownershipTypeInfo - status + - name: businessAttribute + category: core + keyAspect: businessAttributeKey + aspects: + - businessAttributeInfo + - status + - ownership + - institutionalMemory - name: dataContract category: core keyAspect: dataContractKey diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index df960808d8a419..a47617dd7dd9cf 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -118,6 +118,16 @@ public class PoliciesConfig { "Manage Ownership Types", "Create, update and delete Ownership Types."); + public static final Privilege CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE = Privilege.of( + "CREATE_BUSINESS_ATTRIBUTE", + "Create Business Attribute", + "Create new Business Attribute."); + + public static final Privilege MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE = Privilege.of( + "MANAGE_BUSINESS_ATTRIBUTE", + "Manage Business Attribute", + "Create, update, delete Business Attribute"); + public static final List PLATFORM_PRIVILEGES = ImmutableList.of( MANAGE_POLICIES_PRIVILEGE, MANAGE_USERS_AND_GROUPS_PRIVILEGE, @@ -137,7 +147,9 @@ public class PoliciesConfig { CREATE_DOMAINS_PRIVILEGE, CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE, MANAGE_GLOBAL_VIEWS, - MANAGE_GLOBAL_OWNERSHIP_TYPES + MANAGE_GLOBAL_OWNERSHIP_TYPES, + CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE, + MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE ); // Resource Privileges // From 830294d52331f6338191f3b34a7f12ff1b36076c Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Mon, 18 Dec 2023 22:14:50 +0530 Subject: [PATCH 02/50] business-attribute: add businessAttributeService --- .../datahub/graphql/GmsGraphQLEngine.java | 9 ++-- .../datahub/graphql/GmsGraphQLEngineArgs.java | 2 + .../CreateBusinessAttributeResolver.java | 32 +++++++++---- .../UpdateBusinessAttributeResolver.java | 26 ++++------ .../BusinessAttributeType.java | 29 ++++++++++- .../mappers/BusinessAttributeMapper.java | 48 +++++++++++++++++-- .../src/main/resources/entity.graphql | 2 +- .../BusinessAttributeServiceFactory.java | 25 ++++++++++ .../factory/graphql/GraphQLEngineFactory.java | 6 ++- .../service/BusinessAttributeService.java | 35 ++++++++++++++ 10 files changed, 175 insertions(+), 39 deletions(-) create mode 100644 metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java create mode 100644 metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 665c6b53850453..438e4673ba3f9c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -322,6 +322,7 @@ import com.linkedin.metadata.query.filter.SortOrder; import com.linkedin.metadata.recommendation.RecommendationsService; import com.linkedin.metadata.secret.SecretService; +import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.metadata.service.DataProductService; import com.linkedin.metadata.service.LineageService; import com.linkedin.metadata.service.OwnershipTypeService; @@ -411,7 +412,7 @@ public class GmsGraphQLEngine { private final LineageService lineageService; private final QueryService queryService; private final DataProductService dataProductService; - + private final BusinessAttributeService businessAttributeService; private final FeatureFlags featureFlags; private final IngestionConfiguration ingestionConfiguration; @@ -527,6 +528,8 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.lineageService = args.lineageService; this.queryService = args.queryService; this.dataProductService = args.dataProductService; + this.businessAttributeService = args.businessAttributeService; + this.ingestionConfiguration = Objects.requireNonNull(args.ingestionConfiguration); this.authenticationConfiguration = Objects.requireNonNull(args.authenticationConfiguration); @@ -1036,8 +1039,8 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService)) .dataFetcher("updateOwnershipType", new UpdateOwnershipTypeResolver(this.ownershipTypeService)) .dataFetcher("deleteOwnershipType", new DeleteOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher("createBusinessAttribute", new CreateBusinessAttributeResolver(this.entityClient, this.entityService)) - .dataFetcher("updateBusinessAttribute", new UpdateBusinessAttributeResolver(this.entityClient)) + .dataFetcher("createBusinessAttribute", new CreateBusinessAttributeResolver(this.entityClient, this.entityService, this.businessAttributeService)) + .dataFetcher("updateBusinessAttribute", new UpdateBusinessAttributeResolver(this.entityClient, this.businessAttributeService)) .dataFetcher("deleteBusinessAttribute", new DeleteBusinessAttributeResolver(this.entityClient)) ); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java index 157fb10ce70785..f04559ce4dec35 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java @@ -24,6 +24,7 @@ import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.recommendation.RecommendationsService; import com.linkedin.metadata.secret.SecretService; +import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.metadata.service.DataProductService; import com.linkedin.metadata.service.LineageService; import com.linkedin.metadata.service.OwnershipTypeService; @@ -73,6 +74,7 @@ public class GmsGraphQLEngineArgs { QueryService queryService; FeatureFlags featureFlags; DataProductService dataProductService; + BusinessAttributeService businessAttributeService; //any fork specific args should go below this line } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java index c1cf3443e4365d..dbe45a61676ece 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java @@ -3,19 +3,23 @@ import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.businessattribute.BusinessAttributeKey; import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.data.template.SetMode; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.exception.DataHubGraphQLErrorCode; import com.linkedin.datahub.graphql.exception.DataHubGraphQLException; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.generated.CreateBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.OwnerEntityType; import com.linkedin.datahub.graphql.generated.OwnershipType; import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils; +import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributeMapper; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.metadata.utils.EntityKeyUtils; import com.linkedin.mxe.MetadataChangeProposal; import graphql.schema.DataFetcher; @@ -34,12 +38,13 @@ @Slf4j @RequiredArgsConstructor -public class CreateBusinessAttributeResolver implements DataFetcher> { +public class CreateBusinessAttributeResolver implements DataFetcher> { private final EntityClient _entityClient; private final EntityService _entityService; + private final BusinessAttributeService businessAttributeService; @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { final QueryContext context = environment.getContext(); CreateBusinessAttributeInput input = bindArgument(environment.getArgument("input"), CreateBusinessAttributeInput.class); @@ -72,14 +77,13 @@ public CompletableFuture get(DataFetchingEnvironment environment) throws ); // Ingest the MCP - String businessAttributeUrn = _entityClient.ingestProposal(changeProposal, context.getAuthentication()); - OwnershipType ownershipType = OwnershipType.TECHNICAL_OWNER; - if (!_entityService.exists(UrnUtils.getUrn(mapOwnershipTypeToEntity(ownershipType.name())))) { - log.warn("Technical owner does not exist, defaulting to None ownership."); - ownershipType = OwnershipType.NONE; - } - OwnerUtils.addCreatorAsOwner(context, businessAttributeUrn, OwnerEntityType.CORP_USER, ownershipType, _entityService); - return businessAttributeUrn; + Urn businessAttributeUrn = UrnUtils.getUrn(_entityClient.ingestProposal(changeProposal, context.getAuthentication())); + addOwnerToBusinessAttribute(context, businessAttributeUrn.toString()); + return BusinessAttributeMapper.map( + businessAttributeService.getBusinessAttributeEntityResponse( + businessAttributeUrn, context.getAuthentication() + ) + ); } catch (DataHubGraphQLException e) { throw e; @@ -100,4 +104,12 @@ private BusinessAttributeInfo mapBusinessAttributeInfo(CreateBusinessAttributeIn return info; } + private void addOwnerToBusinessAttribute(QueryContext context, String businessAttributeUrn) { + OwnershipType ownershipType = OwnershipType.TECHNICAL_OWNER; + if (!_entityService.exists(UrnUtils.getUrn(mapOwnershipTypeToEntity(ownershipType.name())))) { + log.warn("Technical owner does not exist, defaulting to None ownership."); + ownershipType = OwnershipType.NONE; + } + OwnerUtils.addCreatorAsOwner(context, businessAttributeUrn, OwnerEntityType.CORP_USER, ownershipType, _entityService); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java index b15b817682fb65..e015d472d0bceb 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java @@ -11,10 +11,12 @@ import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.generated.UpdateBusinessAttributeInput; import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; +import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributeMapper; import com.linkedin.entity.EntityResponse; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.AspectUtils; +import com.linkedin.metadata.service.BusinessAttributeService; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import lombok.RequiredArgsConstructor; @@ -23,7 +25,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Objects; -import java.util.Set; import java.util.concurrent.CompletableFuture; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; @@ -33,6 +34,7 @@ public class UpdateBusinessAttributeResolver implements DataFetcher> { private final EntityClient _entityClient; + private final BusinessAttributeService businessAttributeService; @Override public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { @@ -47,13 +49,14 @@ public CompletableFuture get(DataFetchingEnvironment environm if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { throw new IllegalArgumentException("The Business Attribute provided dos not exist"); } - updateBusinessAttribute(input, businessAttributeUrn, context); + Urn updatedBusinessAttributeUrn = updateBusinessAttribute(input, businessAttributeUrn, context); + return BusinessAttributeMapper.map( + businessAttributeService.getBusinessAttributeEntityResponse(updatedBusinessAttributeUrn, context.getAuthentication())); } catch (DataHubGraphQLException e) { throw e; } catch (Exception e) { throw new RuntimeException(String.format("Failed to update Business Attribute with urn %s", businessAttributeUrn), e); } - return null; }); } @@ -70,7 +73,7 @@ private Urn updateBusinessAttribute(UpdateBusinessAttributeInput input, Urn busi if (Objects.nonNull(input.getName())) { if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { throw new DataHubGraphQLException( - String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", input.getName()), + String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", input.getName()), DataHubGraphQLErrorCode.CONFLICT); } businessAttributeInfo.setName(input.getName()); @@ -100,7 +103,7 @@ private Urn updateBusinessAttribute(UpdateBusinessAttributeInput input, Urn busi public BusinessAttributeInfo getBusinessAttributeInfo(@Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { Objects.requireNonNull(businessAttributeUrn, "businessAttributeUrn must not be null"); Objects.requireNonNull(authentication, "authentication must not be null"); - final EntityResponse response = getBusinessAttributeEntityResponse(businessAttributeUrn, authentication); + final EntityResponse response = businessAttributeService.getBusinessAttributeEntityResponse(businessAttributeUrn, authentication); if (response != null && response.getAspects().containsKey(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME)) { return new BusinessAttributeInfo(response.getAspects().get(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME).getValue().data()); } @@ -108,17 +111,4 @@ public BusinessAttributeInfo getBusinessAttributeInfo(@Nonnull final Urn busines return null; } - private EntityResponse getBusinessAttributeEntityResponse(@Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { - Objects.requireNonNull(businessAttributeUrn, "business attribute must not be null"); - Objects.requireNonNull(authentication, "authentication must not be null"); - try { - return _entityClient.batchGetV2( - Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, - Set.of(businessAttributeUrn), - Set.of(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME), - authentication).get(businessAttributeUrn); - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to retrieve Business Attribute with urn %s", businessAttributeUrn), e); - } - } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java index eb98bbd5fc9ca8..4c57f34af9552b 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java @@ -4,16 +4,26 @@ import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.AutoCompleteResults; import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.generated.Entity; import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.FacetFilterInput; +import com.linkedin.datahub.graphql.generated.SearchResults; +import com.linkedin.datahub.graphql.resolvers.ResolverUtils; +import com.linkedin.datahub.graphql.types.SearchableEntityType; import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributeMapper; +import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper; import com.linkedin.entity.EntityResponse; import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchResult; import graphql.execution.DataFetcherResult; import lombok.RequiredArgsConstructor; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -30,7 +40,7 @@ import static com.linkedin.metadata.Constants.STATUS_ASPECT_NAME; @RequiredArgsConstructor -public class BusinessAttributeType implements com.linkedin.datahub.graphql.types.EntityType { +public class BusinessAttributeType implements SearchableEntityType { public static final Set ASPECTS_TO_FETCH = ImmutableSet.of( BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, @@ -39,7 +49,7 @@ public class BusinessAttributeType implements com.linkedin.datahub.graphql.types INSTITUTIONAL_MEMORY_ASPECT_NAME, STATUS_ASPECT_NAME ); - + private static final Set FACET_FIELDS = ImmutableSet.of(""); private final EntityClient _entityClient; @Override @@ -81,4 +91,19 @@ public List> batchLoad(@Nonnull List filters, + int start, int count, @Nonnull QueryContext context) throws Exception { + final Map facetFilters = ResolverUtils.buildFacetFilters(filters, FACET_FIELDS); + final SearchResult searchResult = _entityClient.search( + "businessAttribute", query, facetFilters, start, count, context.getAuthentication(), new SearchFlags().setFulltext(true)); + return UrnSearchResultsMapper.map(searchResult); + } + + @Override + public AutoCompleteResults autoComplete(@Nonnull String query, @Nullable String field, + @Nullable Filter filters, int limit, @Nonnull QueryContext context) throws Exception { + return null; + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java index 1008fcf18cf29e..ad89071c194888 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -2,13 +2,17 @@ import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.Ownership; +import com.linkedin.common.urn.Urn; import com.linkedin.data.DataMap; import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.SchemaFieldDataType; import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; +import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper; import com.linkedin.datahub.graphql.types.mappers.ModelMapper; +import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper; import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EnvelopedAspectMap; @@ -34,13 +38,13 @@ public BusinessAttribute apply(@Nonnull final EntityResponse entityResponse) { EnvelopedAspectMap aspectMap = entityResponse.getAspects(); MappingHelper mappingHelper = new MappingHelper<>(aspectMap, result); mappingHelper.mapToResult(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, ((businessAttribute, dataMap) -> - mapBusinessAttributeInfo(businessAttribute, dataMap))); + mapBusinessAttributeInfo(businessAttribute, dataMap, entityResponse.getUrn()))); mappingHelper.mapToResult(OWNERSHIP_ASPECT_NAME, (businessAttribute, dataMap) -> businessAttribute.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityResponse.getUrn()))); return mappingHelper.getResult(); } - private void mapBusinessAttributeInfo(BusinessAttribute businessAttribute, DataMap dataMap) { + private void mapBusinessAttributeInfo(BusinessAttribute businessAttribute, DataMap dataMap, Urn entityUrn) { BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(dataMap); com.linkedin.datahub.graphql.generated.BusinessAttributeInfo attributeInfo = new com.linkedin.datahub.graphql.generated.BusinessAttributeInfo(); if (businessAttributeInfo.hasFieldPath()) { @@ -56,12 +60,48 @@ private void mapBusinessAttributeInfo(BusinessAttribute businessAttribute, DataM attributeInfo.setLastModified(AuditStampMapper.map(businessAttributeInfo.getLastModified())); } if (businessAttributeInfo.hasGlobalTags()) { - + attributeInfo.setTags(GlobalTagsMapper.map(businessAttributeInfo.getGlobalTags(), entityUrn)); } if (businessAttributeInfo.hasGlossaryTerms()) { - + attributeInfo.setGlossaryTerms(GlossaryTermsMapper.map(businessAttributeInfo.getGlossaryTerms(), entityUrn)); + } + if (businessAttributeInfo.hasType()) { + attributeInfo.setType(mapSchemaFieldDataType(businessAttributeInfo.getType())); } businessAttribute.setBusinessAttributeInfo(attributeInfo); } + private SchemaFieldDataType mapSchemaFieldDataType(@Nonnull final com.linkedin.schema.SchemaFieldDataType dataTypeUnion) { + final com.linkedin.schema.SchemaFieldDataType.Type type = dataTypeUnion.getType(); + if (type.isBytesType()) { + return SchemaFieldDataType.BYTES; + } else if (type.isFixedType()) { + return SchemaFieldDataType.FIXED; + } else if (type.isBooleanType()) { + return SchemaFieldDataType.BOOLEAN; + } else if (type.isStringType()) { + return SchemaFieldDataType.STRING; + } else if (type.isNumberType()) { + return SchemaFieldDataType.NUMBER; + } else if (type.isDateType()) { + return SchemaFieldDataType.DATE; + } else if (type.isTimeType()) { + return SchemaFieldDataType.TIME; + } else if (type.isEnumType()) { + return SchemaFieldDataType.ENUM; + } else if (type.isNullType()) { + return SchemaFieldDataType.NULL; + } else if (type.isArrayType()) { + return SchemaFieldDataType.ARRAY; + } else if (type.isMapType()) { + return SchemaFieldDataType.MAP; + } else if (type.isRecordType()) { + return SchemaFieldDataType.STRUCT; + } else if (type.isUnionType()) { + return SchemaFieldDataType.UNION; + } else { + throw new RuntimeException(String.format("Unrecognized SchemaFieldDataType provided %s", + type.memberType().toString())); + } + } } diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index c8a80234a2d4ea..880c2d0b0ef1a8 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -707,7 +707,7 @@ type Mutation { """ createBusinessAttribute( "Inputs required to create a new BusinessAttribute." - input: CreateBusinessAttributeInput!): String + input: CreateBusinessAttributeInput!): BusinessAttribute """ Delete a Business Attribute by urn. diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java new file mode 100644 index 00000000000000..1eee928e734c7c --- /dev/null +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java @@ -0,0 +1,25 @@ +package com.linkedin.gms.factory.businessattribute; + +import com.linkedin.metadata.client.JavaEntityClient; +import com.linkedin.metadata.service.BusinessAttributeService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; + +@Component +public class BusinessAttributeServiceFactory { + private final JavaEntityClient entityClient; + + public BusinessAttributeServiceFactory(@Qualifier("javaEntityClient") JavaEntityClient entityClient) { + this.entityClient = entityClient; + } + @Bean(name = "businessAttributeService") + @Scope("singleton") + @Nonnull + protected BusinessAttributeService getINSTANCE() throws Exception { + return new BusinessAttributeService(entityClient); + } +} diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java index c50b4c9088bc2a..2861e0ddbf32e1 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java @@ -28,6 +28,7 @@ import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.recommendation.RecommendationsService; import com.linkedin.metadata.secret.SecretService; +import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.metadata.service.DataProductService; import com.linkedin.metadata.service.OwnershipTypeService; import com.linkedin.metadata.service.QueryService; @@ -169,7 +170,9 @@ public class GraphQLEngineFactory { @Value("${platformAnalytics.enabled}") // TODO: Migrate to DATAHUB_ANALYTICS_ENABLED private Boolean isAnalyticsEnabled; - + @Autowired + @Qualifier("businessAttributeService") + private BusinessAttributeService _businessAttributeService; @Bean(name = "graphQLEngine") @Nonnull protected GraphQLEngine getInstance() { @@ -211,6 +214,7 @@ protected GraphQLEngine getInstance() { args.setQueryService(_queryService); args.setFeatureFlags(_configProvider.getFeatureFlags()); args.setDataProductService(_dataProductService); + args.setBusinessAttributeService(_businessAttributeService); return new GmsGraphQLEngine( args ).builder().build(); diff --git a/metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java b/metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java new file mode 100644 index 00000000000000..5aa10eef0603b6 --- /dev/null +++ b/metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java @@ -0,0 +1,35 @@ +package com.linkedin.metadata.service; + +import com.datahub.authentication.Authentication; +import com.linkedin.common.urn.Urn; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.Set; + +@Slf4j +public class BusinessAttributeService { + private final EntityClient _entityClient; + + public BusinessAttributeService(EntityClient entityClient) { + _entityClient = entityClient; + } + + public EntityResponse getBusinessAttributeEntityResponse(@Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { + Objects.requireNonNull(businessAttributeUrn, "business attribute must not be null"); + Objects.requireNonNull(authentication, "authentication must not be null"); + try { + return _entityClient.batchGetV2( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + Set.of(businessAttributeUrn), + Set.of(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME), + authentication).get(businessAttributeUrn); + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to retrieve Business Attribute with urn %s", businessAttributeUrn), e); + } + } +} From a3d81d158efcb758f78e73a1741273e5dd75548f Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Thu, 21 Dec 2023 10:59:07 +0530 Subject: [PATCH 03/50] business-attribute: add resolvers to add/remove businessattribute to dataset schema field --- .../datahub/graphql/GmsGraphQLEngine.java | 4 + .../AddBusinessAttributeResolver.java | 103 +++++++ .../RemoveBusinessAttributeResolver.java | 93 ++++++ .../src/main/resources/entity.graphql | 26 ++ .../BusinessAttributeAssociation.pdl | 6 + .../schema/EditableSchemaFieldInfo.pdl | 11 + ...linkedin.analytics.analytics.restspec.json | 2 + .../com.linkedin.entity.aspects.restspec.json | 6 + ...com.linkedin.entity.entities.restspec.json | 24 ++ ...m.linkedin.entity.entitiesV2.restspec.json | 3 + ...n.entity.entitiesVersionedV2.restspec.json | 2 + .../com.linkedin.entity.runs.restspec.json | 4 + ...nkedin.lineage.relationships.restspec.json | 4 + ...nkedin.operations.operations.restspec.json | 5 + ...m.linkedin.platform.platform.restspec.json | 2 + ...om.linkedin.usage.usageStats.restspec.json | 4 + ...linkedin.analytics.analytics.snapshot.json | 2 + .../com.linkedin.entity.aspects.snapshot.json | 263 +++++++++------- ...com.linkedin.entity.entities.snapshot.json | 281 +++++++++++------- ...m.linkedin.entity.entitiesV2.snapshot.json | 3 + ...n.entity.entitiesVersionedV2.snapshot.json | 2 + .../com.linkedin.entity.runs.snapshot.json | 261 +++++++++------- ...nkedin.lineage.relationships.snapshot.json | 4 + ...nkedin.operations.operations.snapshot.json | 262 +++++++++------- ...m.linkedin.platform.platform.snapshot.json | 259 +++++++++------- ...om.linkedin.usage.usageStats.snapshot.json | 4 + 26 files changed, 1065 insertions(+), 575 deletions(-) create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java create mode 100644 metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 438e4673ba3f9c..99a46c1a41a4de 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -105,9 +105,11 @@ import com.linkedin.datahub.graphql.resolvers.browse.BrowsePathsResolver; import com.linkedin.datahub.graphql.resolvers.browse.BrowseResolver; import com.linkedin.datahub.graphql.resolvers.browse.EntityBrowsePathsResolver; +import com.linkedin.datahub.graphql.resolvers.businessattribute.AddBusinessAttributeResolver; import com.linkedin.datahub.graphql.resolvers.businessattribute.CreateBusinessAttributeResolver; import com.linkedin.datahub.graphql.resolvers.businessattribute.DeleteBusinessAttributeResolver; import com.linkedin.datahub.graphql.resolvers.businessattribute.ListBusinessAttributesResolver; +import com.linkedin.datahub.graphql.resolvers.businessattribute.RemoveBusinessAttributeResolver; import com.linkedin.datahub.graphql.resolvers.businessattribute.UpdateBusinessAttributeResolver; import com.linkedin.datahub.graphql.resolvers.chart.BrowseV2Resolver; import com.linkedin.datahub.graphql.resolvers.chart.ChartStatsSummaryResolver; @@ -1042,6 +1044,8 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("createBusinessAttribute", new CreateBusinessAttributeResolver(this.entityClient, this.entityService, this.businessAttributeService)) .dataFetcher("updateBusinessAttribute", new UpdateBusinessAttributeResolver(this.entityClient, this.businessAttributeService)) .dataFetcher("deleteBusinessAttribute", new DeleteBusinessAttributeResolver(this.entityClient)) + .dataFetcher("addBusinessAttribute", new AddBusinessAttributeResolver(this.entityClient, this.entityService)) + .dataFetcher("removeBusinessAttribute", new RemoveBusinessAttributeResolver(this.entityClient, this.entityService)) ); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java new file mode 100644 index 00000000000000..2af48612f78564 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java @@ -0,0 +1,103 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; +import com.linkedin.datahub.graphql.generated.ResourceRefInput; +import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.EntityUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.r2.RemoteInvocationException; +import com.linkedin.schema.EditableSchemaFieldInfo; +import com.linkedin.schema.EditableSchemaMetadata; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CompletableFuture; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; + +@Slf4j +@RequiredArgsConstructor +public class AddBusinessAttributeResolver implements DataFetcher> { + + private final EntityClient _entityClient; + private final EntityService _entityService; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); + Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); + ResourceRefInput resourceRefInput = input.getResourceUrn(); + + return CompletableFuture.supplyAsync(() -> { + try { + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new IllegalArgumentException("The Business Attribute provided dos not exist"); + } + validateInputResource(resourceRefInput); + + addBusinessAttribute(businessAttributeUrn, resourceRefInput, context); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to add Business Attribute with urn %s to dataset with urn %s", + businessAttributeUrn, resourceRefInput.getResourceUrn()), e); + } + }); + } + + private void validateInputResource(ResourceRefInput resource) { + final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); + LabelUtils.validateResource(resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); + } + + private void addBusinessAttribute(Urn businessAttributeUrn, ResourceRefInput resourceRefInput, QueryContext context) throws RemoteInvocationException { + _entityClient.ingestProposal( + buildAddBusinessAttributeToSubresourceProposal(businessAttributeUrn, resourceRefInput, context), + context.getAuthentication() + ); + } + + private MetadataChangeProposal buildAddBusinessAttributeToSubresourceProposal(Urn businessAttributeUrn, ResourceRefInput resource, QueryContext context) { + com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = + (com.linkedin.schema.EditableSchemaMetadata) EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + _entityService, new EditableSchemaMetadata() + ); + + EditableSchemaFieldInfo editableFieldInfo = getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); + + if (editableFieldInfo == null) { + throw new IllegalArgumentException(String.format("Subresource %s does not exist in dataset %s", + resource.getSubResource(), resource.getResourceUrn() + )); + } + + if (editableFieldInfo.hasBusinessAttribute()) { + throw new RuntimeException(String.format("Schema field has already attached with business attribute")); + } + editableFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); + addBusinessAttribute(editableFieldInfo.getBusinessAttribute(), businessAttributeUrn, UrnUtils.getUrn(context.getActorUrn())); + return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, editableSchemaMetadata); + } + + private void addBusinessAttribute(BusinessAttributeAssociation businessAttributeAssociation, Urn businessAttributeUrn, Urn actorUrn) { + businessAttributeAssociation.setDestinationUrn(businessAttributeUrn); + AuditStamp nowAuditStamp = new AuditStamp().setTime(System.currentTimeMillis()).setActor(actorUrn); + businessAttributeAssociation.setCreated(nowAuditStamp); + businessAttributeAssociation.setLastModified(nowAuditStamp); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java new file mode 100644 index 00000000000000..565f5420150272 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java @@ -0,0 +1,93 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; +import com.linkedin.datahub.graphql.generated.ResourceRefInput; +import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.EntityUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.r2.RemoteInvocationException; +import com.linkedin.schema.EditableSchemaFieldInfo; +import com.linkedin.schema.EditableSchemaMetadata; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CompletableFuture; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; + +@Slf4j +@RequiredArgsConstructor +public class RemoveBusinessAttributeResolver implements DataFetcher> { + private final EntityClient _entityClient; + private final EntityService _entityService; + + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); + Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); + ResourceRefInput resourceRefInput = input.getResourceUrn(); + + return CompletableFuture.supplyAsync(() -> { + try { + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new IllegalArgumentException("The Business Attribute provided dos not exist"); + } + validateInputResource(resourceRefInput, context); + + removeBusinessAttribute(resourceRefInput, context); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to remove Business Attribute with urn %s to dataset with urn %s", + businessAttributeUrn, resourceRefInput.getResourceUrn()), e); + } + }); + } + + private void validateInputResource(ResourceRefInput resource, QueryContext context) { + final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); + LabelUtils.validateResource(resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); + } + + private void removeBusinessAttribute(ResourceRefInput resourceRefInput, QueryContext context) throws RemoteInvocationException { + _entityClient.ingestProposal( + buildRemoveBusinessAttributeToSubresourceProposal(resourceRefInput), + context.getAuthentication() + ); + } + + private MetadataChangeProposal buildRemoveBusinessAttributeToSubresourceProposal(ResourceRefInput resource) { + com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = + (com.linkedin.schema.EditableSchemaMetadata) EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + _entityService, new EditableSchemaMetadata() + ); + + EditableSchemaFieldInfo editableFieldInfo = getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); + + if (editableFieldInfo == null) { + throw new IllegalArgumentException(String.format("Subresource %s does not exist in dataset %s", + resource.getSubResource(), resource.getResourceUrn() + )); + } + + if (!editableFieldInfo.hasBusinessAttribute()) { + throw new RuntimeException(String.format("Schema field has not attached with business attribute")); + } + editableFieldInfo.removeBusinessAttribute(); + return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, editableSchemaMetadata); + } +} diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 880c2d0b0ef1a8..dc75a94da0e3bb 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -724,6 +724,16 @@ type Mutation { urn: String!, "Inputs required to create a new Business Attribute." input: UpdateBusinessAttributeInput!): BusinessAttribute + + """ + Add Business Attribute + """ + addBusinessAttribute(input: AddBusinessAttributeInput!): Boolean + + """ + Remove Business Attribute + """ + removeBusinessAttribute(input: AddBusinessAttributeInput!): Boolean } """ @@ -11403,4 +11413,20 @@ input UpdateBusinessAttributeInput { type """ type: SchemaFieldDataType +} + +""" +Input required to attach Business Attribute +If businessAttributeUrn is null, then it will remove the business attribute from the resource +""" +input AddBusinessAttributeInput { + """ + The urn of the business attribute to add + """ + businessAttributeUrn: String + + """ + resource urns to add the business attribute to + """ + resourceUrn: ResourceRefInput! } \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl new file mode 100644 index 00000000000000..139a77d463df52 --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl @@ -0,0 +1,6 @@ +namespace com.linkedin.businessattribute +import com.linkedin.common.Edge + +record BusinessAttributeAssociation includes Edge { + +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl index 2f3253deeb5fc3..3b05e5a616b04c 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl @@ -1,8 +1,19 @@ namespace com.linkedin.schema +import com.linkedin.businessattribute.BusinessAttributeAssociation /** * SchemaField to describe metadata related to dataset schema. */ record EditableSchemaFieldInfo includes EditableSchemaFieldBase { + /** + * Business Attribute for this field. + */ + @Relationship = { + "/destinationUrn": { + "name": "EditableSchemaFieldWithBusinessAttribute", + "entityTypes": [ "businessAttribute" ] + } + } + businessAttribute: optional BusinessAttributeAssociation } diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.analytics.analytics.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.analytics.analytics.restspec.json index 3e1b975311b110..27581334814ceb 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.analytics.analytics.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.analytics.analytics.restspec.json @@ -4,10 +4,12 @@ "path" : "/analytics", "schema" : "com.linkedin.analytics.GetTimeseriesAggregatedStatsResponse", "doc" : "Rest.li entry point: /analytics\n\ngenerated from: com.linkedin.metadata.resources.analytics.Analytics", + "resourceClass" : "com.linkedin.metadata.resources.analytics.Analytics", "simple" : { "supports" : [ ], "actions" : [ { "name" : "getTimeseriesStats", + "javaMethodName" : "getTimeseriesStats", "parameters" : [ { "name" : "entityName", "type" : "string" diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.aspects.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.aspects.restspec.json index 3a0df137a04693..917540aca8728c 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.aspects.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.aspects.restspec.json @@ -4,6 +4,7 @@ "path" : "/aspects", "schema" : "com.linkedin.metadata.aspect.VersionedAspect", "doc" : "Single unified resource for fetching, updating, searching, & browsing DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.AspectResource", + "resourceClass" : "com.linkedin.metadata.resources.entity.AspectResource", "collection" : { "identifier" : { "name" : "aspectsId", @@ -12,6 +13,7 @@ "supports" : [ "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.\n TODO: Get rid of this and migrate to getAspect.", "parameters" : [ { "name" : "aspect", @@ -25,6 +27,7 @@ } ], "actions" : [ { "name" : "getCount", + "javaMethodName" : "getCount", "parameters" : [ { "name" : "aspect", "type" : "string" @@ -36,6 +39,7 @@ "returns" : "int" }, { "name" : "getTimeseriesAspectValues", + "javaMethodName" : "getTimeseriesAspectValues", "parameters" : [ { "name" : "urn", "type" : "string" @@ -73,6 +77,7 @@ "returns" : "com.linkedin.aspect.GetTimeseriesAspectValuesResponse" }, { "name" : "ingestProposal", + "javaMethodName" : "ingestProposal", "parameters" : [ { "name" : "proposal", "type" : "com.linkedin.mxe.MetadataChangeProposal" @@ -84,6 +89,7 @@ "returns" : "string" }, { "name" : "restoreIndices", + "javaMethodName" : "restoreIndices", "parameters" : [ { "name" : "aspect", "type" : "string", diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entities.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entities.restspec.json index a9de21d08aedc2..8b009434ef3c35 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entities.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entities.restspec.json @@ -4,6 +4,7 @@ "path" : "/entities", "schema" : "com.linkedin.entity.Entity", "doc" : "Single unified resource for fetching, updating, searching, & browsing DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.EntityResource", + "resourceClass" : "com.linkedin.metadata.resources.entity.EntityResource", "collection" : { "identifier" : { "name" : "entitiesId", @@ -12,6 +13,7 @@ "supports" : [ "batch_get", "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.", "parameters" : [ { "name" : "aspects", @@ -20,6 +22,7 @@ } ] }, { "method" : "batch_get", + "javaMethodName" : "batchGet", "parameters" : [ { "name" : "aspects", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", @@ -28,6 +31,7 @@ } ], "actions" : [ { "name" : "applyRetention", + "javaMethodName" : "applyRetention", "parameters" : [ { "name" : "start", "type" : "int", @@ -52,6 +56,7 @@ "returns" : "string" }, { "name" : "autocomplete", + "javaMethodName" : "autocomplete", "parameters" : [ { "name" : "entity", "type" : "string" @@ -73,6 +78,7 @@ "returns" : "com.linkedin.metadata.query.AutoCompleteResult" }, { "name" : "batchGetTotalEntityCount", + "javaMethodName" : "batchGetTotalEntityCount", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }" @@ -80,6 +86,7 @@ "returns" : "{ \"type\" : \"map\", \"values\" : \"long\" }" }, { "name" : "batchIngest", + "javaMethodName" : "batchIngest", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.entity.Entity\" }" @@ -90,6 +97,7 @@ } ] }, { "name" : "browse", + "javaMethodName" : "browse", "parameters" : [ { "name" : "entity", "type" : "string" @@ -110,6 +118,7 @@ "returns" : "com.linkedin.metadata.browse.BrowseResult" }, { "name" : "delete", + "javaMethodName" : "deleteEntity", "doc" : "Deletes all data related to an individual urn(entity).\nService Returns: - a DeleteEntityResponse object.", "parameters" : [ { "name" : "urn", @@ -134,6 +143,7 @@ "returns" : "com.linkedin.metadata.run.DeleteEntityResponse" }, { "name" : "deleteAll", + "javaMethodName" : "deleteEntities", "parameters" : [ { "name" : "registryId", "type" : "string", @@ -146,6 +156,7 @@ "returns" : "com.linkedin.metadata.run.RollbackResponse" }, { "name" : "deleteReferences", + "javaMethodName" : "deleteReferencesTo", "parameters" : [ { "name" : "urn", "type" : "string" @@ -157,6 +168,7 @@ "returns" : "com.linkedin.metadata.run.DeleteReferencesResponse" }, { "name" : "exists", + "javaMethodName" : "exists", "parameters" : [ { "name" : "urn", "type" : "string" @@ -164,6 +176,7 @@ "returns" : "boolean" }, { "name" : "filter", + "javaMethodName" : "filter", "parameters" : [ { "name" : "entity", "type" : "string" @@ -184,6 +197,7 @@ "returns" : "com.linkedin.metadata.search.SearchResult" }, { "name" : "getBrowsePaths", + "javaMethodName" : "getBrowsePaths", "parameters" : [ { "name" : "urn", "type" : "com.linkedin.common.Urn" @@ -191,6 +205,7 @@ "returns" : "{ \"type\" : \"array\", \"items\" : \"string\" }" }, { "name" : "getTotalEntityCount", + "javaMethodName" : "getTotalEntityCount", "parameters" : [ { "name" : "entity", "type" : "string" @@ -198,6 +213,7 @@ "returns" : "long" }, { "name" : "ingest", + "javaMethodName" : "ingest", "parameters" : [ { "name" : "entity", "type" : "com.linkedin.entity.Entity" @@ -208,6 +224,7 @@ } ] }, { "name" : "list", + "javaMethodName" : "list", "parameters" : [ { "name" : "entity", "type" : "string" @@ -229,6 +246,7 @@ "returns" : "com.linkedin.metadata.query.ListResult" }, { "name" : "listUrns", + "javaMethodName" : "listUrns", "parameters" : [ { "name" : "entity", "type" : "string" @@ -242,6 +260,7 @@ "returns" : "com.linkedin.metadata.query.ListUrnsResult" }, { "name" : "scrollAcrossEntities", + "javaMethodName" : "scrollAcrossEntities", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", @@ -274,6 +293,7 @@ "returns" : "com.linkedin.metadata.search.ScrollResult" }, { "name" : "scrollAcrossLineage", + "javaMethodName" : "scrollAcrossLineage", "parameters" : [ { "name" : "urn", "type" : "string" @@ -325,6 +345,7 @@ "returns" : "com.linkedin.metadata.search.LineageScrollResult" }, { "name" : "search", + "javaMethodName" : "search", "parameters" : [ { "name" : "entity", "type" : "string" @@ -360,6 +381,7 @@ "returns" : "com.linkedin.metadata.search.SearchResult" }, { "name" : "searchAcrossEntities", + "javaMethodName" : "searchAcrossEntities", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", @@ -389,6 +411,7 @@ "returns" : "com.linkedin.metadata.search.SearchResult" }, { "name" : "searchAcrossLineage", + "javaMethodName" : "searchAcrossLineage", "parameters" : [ { "name" : "urn", "type" : "string" @@ -437,6 +460,7 @@ "returns" : "com.linkedin.metadata.search.LineageSearchResult" }, { "name" : "setWritable", + "javaMethodName" : "setWriteable", "parameters" : [ { "name" : "value", "type" : "boolean", diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesV2.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesV2.restspec.json index 0c92a981c7356a..33cfba0f27802c 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesV2.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesV2.restspec.json @@ -4,6 +4,7 @@ "path" : "/entitiesV2", "schema" : "com.linkedin.entity.EntityResponse", "doc" : "Single unified resource for fetching, updating, searching, & browsing DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.EntityV2Resource", + "resourceClass" : "com.linkedin.metadata.resources.entity.EntityV2Resource", "collection" : { "identifier" : { "name" : "entitiesV2Id", @@ -12,6 +13,7 @@ "supports" : [ "batch_get", "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.", "parameters" : [ { "name" : "aspects", @@ -20,6 +22,7 @@ } ] }, { "method" : "batch_get", + "javaMethodName" : "batchGet", "parameters" : [ { "name" : "aspects", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesVersionedV2.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesVersionedV2.restspec.json index 579f1d7c7dddc0..f3eb9d38dc6ae0 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesVersionedV2.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.entitiesVersionedV2.restspec.json @@ -4,6 +4,7 @@ "path" : "/entitiesVersionedV2", "schema" : "com.linkedin.entity.EntityResponse", "doc" : "Single unified resource for fetching, updating, searching, & browsing versioned DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.EntityVersionedV2Resource", + "resourceClass" : "com.linkedin.metadata.resources.entity.EntityVersionedV2Resource", "collection" : { "identifier" : { "name" : "entitiesVersionedV2Id", @@ -12,6 +13,7 @@ "supports" : [ "batch_get" ], "methods" : [ { "method" : "batch_get", + "javaMethodName" : "batchGetVersioned", "parameters" : [ { "name" : "entityType", "type" : "string" diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.runs.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.runs.restspec.json index 5eaa34bc7a2e92..7284cd2bac48f4 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.entity.runs.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.entity.runs.restspec.json @@ -4,6 +4,7 @@ "path" : "/runs", "schema" : "com.linkedin.metadata.aspect.VersionedAspect", "doc" : "resource for showing information and rolling back runs\n\ngenerated from: com.linkedin.metadata.resources.entity.BatchIngestionRunResource", + "resourceClass" : "com.linkedin.metadata.resources.entity.BatchIngestionRunResource", "collection" : { "identifier" : { "name" : "runsId", @@ -12,6 +13,7 @@ "supports" : [ ], "actions" : [ { "name" : "describe", + "javaMethodName" : "describe", "parameters" : [ { "name" : "runId", "type" : "string" @@ -33,6 +35,7 @@ "returns" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.metadata.run.AspectRowSummary\" }" }, { "name" : "list", + "javaMethodName" : "list", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.", "parameters" : [ { "name" : "pageOffset", @@ -50,6 +53,7 @@ "returns" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.metadata.run.IngestionRunSummary\" }" }, { "name" : "rollback", + "javaMethodName" : "rollback", "doc" : "Rolls back an ingestion run", "parameters" : [ { "name" : "runId", diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.lineage.relationships.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.lineage.relationships.restspec.json index 68f9fe8ae152ee..7056368d82c7d9 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.lineage.relationships.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.lineage.relationships.restspec.json @@ -4,10 +4,12 @@ "path" : "/relationships", "schema" : "com.linkedin.common.EntityRelationships", "doc" : "Rest.li entry point: /relationships?type={entityType}&direction={direction}&types={types}\n\ngenerated from: com.linkedin.metadata.resources.lineage.Relationships", + "resourceClass" : "com.linkedin.metadata.resources.lineage.Relationships", "simple" : { "supports" : [ "delete", "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "parameters" : [ { "name" : "urn", "type" : "string" @@ -28,6 +30,7 @@ } ] }, { "method" : "delete", + "javaMethodName" : "delete", "parameters" : [ { "name" : "urn", "type" : "string" @@ -35,6 +38,7 @@ } ], "actions" : [ { "name" : "getLineage", + "javaMethodName" : "getLineage", "parameters" : [ { "name" : "urn", "type" : "string" diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.operations.operations.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.operations.operations.restspec.json index 958ec13b37fcad..0fb6a18a7974bd 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.operations.operations.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.operations.operations.restspec.json @@ -4,6 +4,7 @@ "path" : "/operations", "schema" : "com.linkedin.metadata.aspect.VersionedAspect", "doc" : "Endpoints for performing maintenance operations\n\ngenerated from: com.linkedin.metadata.resources.operations.OperationsResource", + "resourceClass" : "com.linkedin.metadata.resources.operations.OperationsResource", "collection" : { "identifier" : { "name" : "operationsId", @@ -12,6 +13,7 @@ "supports" : [ ], "actions" : [ { "name" : "getEsTaskStatus", + "javaMethodName" : "getTaskStatus", "parameters" : [ { "name" : "nodeId", "type" : "string", @@ -28,9 +30,11 @@ "returns" : "string" }, { "name" : "getIndexSizes", + "javaMethodName" : "getIndexSizes", "returns" : "com.linkedin.timeseries.TimeseriesIndicesSizesResult" }, { "name" : "restoreIndices", + "javaMethodName" : "restoreIndices", "parameters" : [ { "name" : "aspect", "type" : "string", @@ -55,6 +59,7 @@ "returns" : "string" }, { "name" : "truncateTimeseriesAspect", + "javaMethodName" : "truncateTimeseriesAspect", "parameters" : [ { "name" : "entityType", "type" : "string" diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.platform.platform.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.platform.platform.restspec.json index 3346ddd23e3bad..9fbb3e9b6698e4 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.platform.platform.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.platform.platform.restspec.json @@ -4,6 +4,7 @@ "path" : "/platform", "schema" : "com.linkedin.entity.Entity", "doc" : "DataHub Platform Actions\n\ngenerated from: com.linkedin.metadata.resources.platform.PlatformResource", + "resourceClass" : "com.linkedin.metadata.resources.platform.PlatformResource", "collection" : { "identifier" : { "name" : "platformId", @@ -12,6 +13,7 @@ "supports" : [ ], "actions" : [ { "name" : "producePlatformEvent", + "javaMethodName" : "producePlatformEvent", "parameters" : [ { "name" : "name", "type" : "string" diff --git a/metadata-service/restli-api/src/main/idl/com.linkedin.usage.usageStats.restspec.json b/metadata-service/restli-api/src/main/idl/com.linkedin.usage.usageStats.restspec.json index 2a4cf40b58412d..42f0894fbb7a6b 100644 --- a/metadata-service/restli-api/src/main/idl/com.linkedin.usage.usageStats.restspec.json +++ b/metadata-service/restli-api/src/main/idl/com.linkedin.usage.usageStats.restspec.json @@ -7,6 +7,7 @@ "path" : "/usageStats", "schema" : "com.linkedin.usage.UsageAggregation", "doc" : "Rest.li entry point: /usageStats\n\ngenerated from: com.linkedin.metadata.resources.usage.UsageStats", + "resourceClass" : "com.linkedin.metadata.resources.usage.UsageStats", "simple" : { "supports" : [ ], "actions" : [ { @@ -14,12 +15,14 @@ "deprecated" : { } }, "name" : "batchIngest", + "javaMethodName" : "batchIngest", "parameters" : [ { "name" : "buckets", "type" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.usage.UsageAggregation\" }" } ] }, { "name" : "query", + "javaMethodName" : "query", "parameters" : [ { "name" : "resource", "type" : "string" @@ -42,6 +45,7 @@ "returns" : "com.linkedin.usage.UsageQueryResult" }, { "name" : "queryRange", + "javaMethodName" : "queryRange", "parameters" : [ { "name" : "resource", "type" : "string" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json index d75ec585464654..c4532cba9e6be3 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.analytics.analytics.snapshot.json @@ -222,10 +222,12 @@ "path" : "/analytics", "schema" : "com.linkedin.analytics.GetTimeseriesAggregatedStatsResponse", "doc" : "Rest.li entry point: /analytics\n\ngenerated from: com.linkedin.metadata.resources.analytics.Analytics", + "resourceClass" : "com.linkedin.metadata.resources.analytics.Analytics", "simple" : { "supports" : [ ], "actions" : [ { "name" : "getTimeseriesStats", + "javaMethodName" : "getTimeseriesStats", "parameters" : [ { "name" : "entityName", "type" : "string" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json index 0403fa2ceea6f4..cafd5b61c9dbfc 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json @@ -258,6 +258,80 @@ "compliance" : "NONE" } ] }, "com.linkedin.avro2pegasus.events.UUID", { + "type" : "record", + "name" : "BusinessAttributeAssociation", + "namespace" : "com.linkedin.businessattribute", + "include" : [ { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : "Urn", + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } ], + "fields" : [ ] + }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -369,42 +443,7 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, + "type" : "AuditStamp", "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -454,40 +493,7 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : "Urn", - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } + "items" : "com.linkedin.common.Edge" }, "doc" : "Data sources for the chart", "optional" : true, @@ -3126,54 +3132,75 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" + "include" : [ { + "type" : "record", + "name" : "EditableSchemaFieldBase", + "doc" : "Base class to describe metadata related to dataset schema.", + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" + } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" + } } - } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", + "optional" : true, + "Relationship" : { + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" + } + } + } ] + } ], + "fields" : [ { + "name" : "businessAttribute", + "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", + "doc" : "Business Attribute for this field.", "optional" : true, "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" + "/destinationUrn" : { + "createdActor" : "businessAttribute/created/actor", + "createdOn" : "businessAttribute/created/time", + "entityTypes" : [ "businessAttribute" ], + "name" : "EditableSchemaFieldWithBusinessAttribute", + "updatedActor" : "businessAttribute/lastModified/actor", + "updatedOn" : "businessAttribute/lastModified/time" } } } ] @@ -3986,13 +4013,14 @@ "doc" : "A string->string map of custom properties that one might want to attach to an event\n", "optional" : true } ] - }, "com.linkedin.mxe.SystemMetadata", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.mxe.SystemMetadata", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "aspects", "namespace" : "com.linkedin.entity", "path" : "/aspects", "schema" : "com.linkedin.metadata.aspect.VersionedAspect", "doc" : "Single unified resource for fetching, updating, searching, & browsing DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.AspectResource", + "resourceClass" : "com.linkedin.metadata.resources.entity.AspectResource", "collection" : { "identifier" : { "name" : "aspectsId", @@ -4001,6 +4029,7 @@ "supports" : [ "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.\n TODO: Get rid of this and migrate to getAspect.", "parameters" : [ { "name" : "aspect", @@ -4014,6 +4043,7 @@ } ], "actions" : [ { "name" : "getCount", + "javaMethodName" : "getCount", "parameters" : [ { "name" : "aspect", "type" : "string" @@ -4025,6 +4055,7 @@ "returns" : "int" }, { "name" : "getTimeseriesAspectValues", + "javaMethodName" : "getTimeseriesAspectValues", "parameters" : [ { "name" : "urn", "type" : "string" @@ -4062,6 +4093,7 @@ "returns" : "com.linkedin.aspect.GetTimeseriesAspectValuesResponse" }, { "name" : "ingestProposal", + "javaMethodName" : "ingestProposal", "parameters" : [ { "name" : "proposal", "type" : "com.linkedin.mxe.MetadataChangeProposal" @@ -4073,6 +4105,7 @@ "returns" : "string" }, { "name" : "restoreIndices", + "javaMethodName" : "restoreIndices", "parameters" : [ { "name" : "aspect", "type" : "string", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json index d79a4a1919af90..972017bdcaa787 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json @@ -1,5 +1,79 @@ { "models" : [ { + "type" : "record", + "name" : "BusinessAttributeAssociation", + "namespace" : "com.linkedin.businessattribute", + "include" : [ { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : "Urn", + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } ], + "fields" : [ ] + }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -111,42 +185,7 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, + "type" : "AuditStamp", "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -196,40 +235,7 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : "Urn", - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } + "items" : "com.linkedin.common.Edge" }, "doc" : "Data sources for the chart", "optional" : true, @@ -3511,54 +3517,75 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" + "include" : [ { + "type" : "record", + "name" : "EditableSchemaFieldBase", + "doc" : "Base class to describe metadata related to dataset schema.", + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" + } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" + } } - } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", + "optional" : true, + "Relationship" : { + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" + } + } + } ] + } ], + "fields" : [ { + "name" : "businessAttribute", + "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", + "doc" : "Business Attribute for this field.", "optional" : true, "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" + "/destinationUrn" : { + "createdActor" : "businessAttribute/created/actor", + "createdOn" : "businessAttribute/created/time", + "entityTypes" : [ "businessAttribute" ], + "name" : "EditableSchemaFieldWithBusinessAttribute", + "updatedActor" : "businessAttribute/lastModified/actor", + "updatedOn" : "businessAttribute/lastModified/time" } } } ] @@ -6282,13 +6309,14 @@ "doc" : "Additional properties", "optional" : true } ] - }, "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "entities", "namespace" : "com.linkedin.entity", "path" : "/entities", "schema" : "com.linkedin.entity.Entity", "doc" : "Single unified resource for fetching, updating, searching, & browsing DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.EntityResource", + "resourceClass" : "com.linkedin.metadata.resources.entity.EntityResource", "collection" : { "identifier" : { "name" : "entitiesId", @@ -6297,6 +6325,7 @@ "supports" : [ "batch_get", "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.", "parameters" : [ { "name" : "aspects", @@ -6305,6 +6334,7 @@ } ] }, { "method" : "batch_get", + "javaMethodName" : "batchGet", "parameters" : [ { "name" : "aspects", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", @@ -6313,6 +6343,7 @@ } ], "actions" : [ { "name" : "applyRetention", + "javaMethodName" : "applyRetention", "parameters" : [ { "name" : "start", "type" : "int", @@ -6337,6 +6368,7 @@ "returns" : "string" }, { "name" : "autocomplete", + "javaMethodName" : "autocomplete", "parameters" : [ { "name" : "entity", "type" : "string" @@ -6358,6 +6390,7 @@ "returns" : "com.linkedin.metadata.query.AutoCompleteResult" }, { "name" : "batchGetTotalEntityCount", + "javaMethodName" : "batchGetTotalEntityCount", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }" @@ -6365,6 +6398,7 @@ "returns" : "{ \"type\" : \"map\", \"values\" : \"long\" }" }, { "name" : "batchIngest", + "javaMethodName" : "batchIngest", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.entity.Entity\" }" @@ -6375,6 +6409,7 @@ } ] }, { "name" : "browse", + "javaMethodName" : "browse", "parameters" : [ { "name" : "entity", "type" : "string" @@ -6395,6 +6430,7 @@ "returns" : "com.linkedin.metadata.browse.BrowseResult" }, { "name" : "delete", + "javaMethodName" : "deleteEntity", "doc" : "Deletes all data related to an individual urn(entity).\nService Returns: - a DeleteEntityResponse object.", "parameters" : [ { "name" : "urn", @@ -6419,6 +6455,7 @@ "returns" : "com.linkedin.metadata.run.DeleteEntityResponse" }, { "name" : "deleteAll", + "javaMethodName" : "deleteEntities", "parameters" : [ { "name" : "registryId", "type" : "string", @@ -6431,6 +6468,7 @@ "returns" : "com.linkedin.metadata.run.RollbackResponse" }, { "name" : "deleteReferences", + "javaMethodName" : "deleteReferencesTo", "parameters" : [ { "name" : "urn", "type" : "string" @@ -6442,6 +6480,7 @@ "returns" : "com.linkedin.metadata.run.DeleteReferencesResponse" }, { "name" : "exists", + "javaMethodName" : "exists", "parameters" : [ { "name" : "urn", "type" : "string" @@ -6449,6 +6488,7 @@ "returns" : "boolean" }, { "name" : "filter", + "javaMethodName" : "filter", "parameters" : [ { "name" : "entity", "type" : "string" @@ -6469,6 +6509,7 @@ "returns" : "com.linkedin.metadata.search.SearchResult" }, { "name" : "getBrowsePaths", + "javaMethodName" : "getBrowsePaths", "parameters" : [ { "name" : "urn", "type" : "com.linkedin.common.Urn" @@ -6476,6 +6517,7 @@ "returns" : "{ \"type\" : \"array\", \"items\" : \"string\" }" }, { "name" : "getTotalEntityCount", + "javaMethodName" : "getTotalEntityCount", "parameters" : [ { "name" : "entity", "type" : "string" @@ -6483,6 +6525,7 @@ "returns" : "long" }, { "name" : "ingest", + "javaMethodName" : "ingest", "parameters" : [ { "name" : "entity", "type" : "com.linkedin.entity.Entity" @@ -6493,6 +6536,7 @@ } ] }, { "name" : "list", + "javaMethodName" : "list", "parameters" : [ { "name" : "entity", "type" : "string" @@ -6514,6 +6558,7 @@ "returns" : "com.linkedin.metadata.query.ListResult" }, { "name" : "listUrns", + "javaMethodName" : "listUrns", "parameters" : [ { "name" : "entity", "type" : "string" @@ -6527,6 +6572,7 @@ "returns" : "com.linkedin.metadata.query.ListUrnsResult" }, { "name" : "scrollAcrossEntities", + "javaMethodName" : "scrollAcrossEntities", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", @@ -6559,6 +6605,7 @@ "returns" : "com.linkedin.metadata.search.ScrollResult" }, { "name" : "scrollAcrossLineage", + "javaMethodName" : "scrollAcrossLineage", "parameters" : [ { "name" : "urn", "type" : "string" @@ -6610,6 +6657,7 @@ "returns" : "com.linkedin.metadata.search.LineageScrollResult" }, { "name" : "search", + "javaMethodName" : "search", "parameters" : [ { "name" : "entity", "type" : "string" @@ -6645,6 +6693,7 @@ "returns" : "com.linkedin.metadata.search.SearchResult" }, { "name" : "searchAcrossEntities", + "javaMethodName" : "searchAcrossEntities", "parameters" : [ { "name" : "entities", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", @@ -6674,6 +6723,7 @@ "returns" : "com.linkedin.metadata.search.SearchResult" }, { "name" : "searchAcrossLineage", + "javaMethodName" : "searchAcrossLineage", "parameters" : [ { "name" : "urn", "type" : "string" @@ -6722,6 +6772,7 @@ "returns" : "com.linkedin.metadata.search.LineageSearchResult" }, { "name" : "setWritable", + "javaMethodName" : "setWriteable", "parameters" : [ { "name" : "value", "type" : "boolean", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesV2.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesV2.snapshot.json index c7618e5d3c5a18..3eac87e268f5d4 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesV2.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesV2.snapshot.json @@ -162,6 +162,7 @@ "path" : "/entitiesV2", "schema" : "com.linkedin.entity.EntityResponse", "doc" : "Single unified resource for fetching, updating, searching, & browsing DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.EntityV2Resource", + "resourceClass" : "com.linkedin.metadata.resources.entity.EntityV2Resource", "collection" : { "identifier" : { "name" : "entitiesV2Id", @@ -170,6 +171,7 @@ "supports" : [ "batch_get", "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.", "parameters" : [ { "name" : "aspects", @@ -178,6 +180,7 @@ } ] }, { "method" : "batch_get", + "javaMethodName" : "batchGet", "parameters" : [ { "name" : "aspects", "type" : "{ \"type\" : \"array\", \"items\" : \"string\" }", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesVersionedV2.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesVersionedV2.snapshot.json index 45e542883b7235..1733537e68f305 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesVersionedV2.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entitiesVersionedV2.snapshot.json @@ -171,6 +171,7 @@ "path" : "/entitiesVersionedV2", "schema" : "com.linkedin.entity.EntityResponse", "doc" : "Single unified resource for fetching, updating, searching, & browsing versioned DataHub entities\n\ngenerated from: com.linkedin.metadata.resources.entity.EntityVersionedV2Resource", + "resourceClass" : "com.linkedin.metadata.resources.entity.EntityVersionedV2Resource", "collection" : { "identifier" : { "name" : "entitiesVersionedV2Id", @@ -179,6 +180,7 @@ "supports" : [ "batch_get" ], "methods" : [ { "method" : "batch_get", + "javaMethodName" : "batchGetVersioned", "parameters" : [ { "name" : "entityType", "type" : "string" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json index b20953749ac353..4352959f5fb2e2 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json @@ -1,5 +1,79 @@ { "models" : [ { + "type" : "record", + "name" : "BusinessAttributeAssociation", + "namespace" : "com.linkedin.businessattribute", + "include" : [ { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : "Urn", + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } ], + "fields" : [ ] + }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -111,42 +185,7 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, + "type" : "AuditStamp", "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -196,40 +235,7 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : "Urn", - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } + "items" : "com.linkedin.common.Edge" }, "doc" : "Data sources for the chart", "optional" : true, @@ -2860,54 +2866,75 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" + "include" : [ { + "type" : "record", + "name" : "EditableSchemaFieldBase", + "doc" : "Base class to describe metadata related to dataset schema.", + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" + } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" + } } - } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", + "optional" : true, + "Relationship" : { + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" + } + } + } ] + } ], + "fields" : [ { + "name" : "businessAttribute", + "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", + "doc" : "Business Attribute for this field.", "optional" : true, "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" + "/destinationUrn" : { + "createdActor" : "businessAttribute/created/actor", + "createdOn" : "businessAttribute/created/time", + "entityTypes" : [ "businessAttribute" ], + "name" : "EditableSchemaFieldWithBusinessAttribute", + "updatedActor" : "businessAttribute/lastModified/actor", + "updatedOn" : "businessAttribute/lastModified/time" } } } ] @@ -3741,13 +3768,14 @@ } } } ] - }, "com.linkedin.metadata.run.UnsafeEntityInfo", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.metadata.run.UnsafeEntityInfo", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "runs", "namespace" : "com.linkedin.entity", "path" : "/runs", "schema" : "com.linkedin.metadata.aspect.VersionedAspect", "doc" : "resource for showing information and rolling back runs\n\ngenerated from: com.linkedin.metadata.resources.entity.BatchIngestionRunResource", + "resourceClass" : "com.linkedin.metadata.resources.entity.BatchIngestionRunResource", "collection" : { "identifier" : { "name" : "runsId", @@ -3756,6 +3784,7 @@ "supports" : [ ], "actions" : [ { "name" : "describe", + "javaMethodName" : "describe", "parameters" : [ { "name" : "runId", "type" : "string" @@ -3777,6 +3806,7 @@ "returns" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.metadata.run.AspectRowSummary\" }" }, { "name" : "list", + "javaMethodName" : "list", "doc" : "Retrieves the value for an entity that is made up of latest versions of specified aspects.", "parameters" : [ { "name" : "pageOffset", @@ -3794,6 +3824,7 @@ "returns" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.metadata.run.IngestionRunSummary\" }" }, { "name" : "rollback", + "javaMethodName" : "rollback", "doc" : "Rolls back an ingestion run", "parameters" : [ { "name" : "runId", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.lineage.relationships.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.lineage.relationships.snapshot.json index 6febf225ad77d0..9aa40edd0b118d 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.lineage.relationships.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.lineage.relationships.snapshot.json @@ -180,10 +180,12 @@ "path" : "/relationships", "schema" : "com.linkedin.common.EntityRelationships", "doc" : "Rest.li entry point: /relationships?type={entityType}&direction={direction}&types={types}\n\ngenerated from: com.linkedin.metadata.resources.lineage.Relationships", + "resourceClass" : "com.linkedin.metadata.resources.lineage.Relationships", "simple" : { "supports" : [ "delete", "get" ], "methods" : [ { "method" : "get", + "javaMethodName" : "get", "parameters" : [ { "name" : "urn", "type" : "string" @@ -204,6 +206,7 @@ } ] }, { "method" : "delete", + "javaMethodName" : "delete", "parameters" : [ { "name" : "urn", "type" : "string" @@ -211,6 +214,7 @@ } ], "actions" : [ { "name" : "getLineage", + "javaMethodName" : "getLineage", "parameters" : [ { "name" : "urn", "type" : "string" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json index e29dd6809b968b..44da57d0bdfb9f 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json @@ -1,5 +1,79 @@ { "models" : [ { + "type" : "record", + "name" : "BusinessAttributeAssociation", + "namespace" : "com.linkedin.businessattribute", + "include" : [ { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : "Urn", + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } ], + "fields" : [ ] + }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -111,42 +185,7 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, + "type" : "AuditStamp", "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -196,40 +235,7 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : "Urn", - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } + "items" : "com.linkedin.common.Edge" }, "doc" : "Data sources for the chart", "optional" : true, @@ -2854,54 +2860,75 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" + "include" : [ { + "type" : "record", + "name" : "EditableSchemaFieldBase", + "doc" : "Base class to describe metadata related to dataset schema.", + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" + } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" + } } - } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", + "optional" : true, + "Relationship" : { + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" + } + } + } ] + } ], + "fields" : [ { + "name" : "businessAttribute", + "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", + "doc" : "Business Attribute for this field.", "optional" : true, "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" + "/destinationUrn" : { + "createdActor" : "businessAttribute/created/actor", + "createdOn" : "businessAttribute/created/time", + "entityTypes" : [ "businessAttribute" ], + "name" : "EditableSchemaFieldWithBusinessAttribute", + "updatedActor" : "businessAttribute/lastModified/actor", + "updatedOn" : "businessAttribute/lastModified/time" } } } ] @@ -3647,7 +3674,7 @@ "name" : "version", "type" : "long" } ] - }, "com.linkedin.metadata.key.ChartKey", "com.linkedin.metadata.key.CorpGroupKey", "com.linkedin.metadata.key.CorpUserKey", "com.linkedin.metadata.key.DashboardKey", "com.linkedin.metadata.key.DataFlowKey", "com.linkedin.metadata.key.DataJobKey", "com.linkedin.metadata.key.GlossaryNodeKey", "com.linkedin.metadata.key.GlossaryTermKey", "com.linkedin.metadata.key.MLFeatureKey", "com.linkedin.metadata.key.MLModelKey", "com.linkedin.metadata.key.TagKey", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties", { + }, "com.linkedin.metadata.key.ChartKey", "com.linkedin.metadata.key.CorpGroupKey", "com.linkedin.metadata.key.CorpUserKey", "com.linkedin.metadata.key.DashboardKey", "com.linkedin.metadata.key.DataFlowKey", "com.linkedin.metadata.key.DataJobKey", "com.linkedin.metadata.key.GlossaryNodeKey", "com.linkedin.metadata.key.GlossaryTermKey", "com.linkedin.metadata.key.MLFeatureKey", "com.linkedin.metadata.key.MLModelKey", "com.linkedin.metadata.key.TagKey", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties", { "type" : "record", "name" : "TimeseriesIndexSizeResult", "namespace" : "com.linkedin.timeseries", @@ -3690,6 +3717,7 @@ "path" : "/operations", "schema" : "com.linkedin.metadata.aspect.VersionedAspect", "doc" : "Endpoints for performing maintenance operations\n\ngenerated from: com.linkedin.metadata.resources.operations.OperationsResource", + "resourceClass" : "com.linkedin.metadata.resources.operations.OperationsResource", "collection" : { "identifier" : { "name" : "operationsId", @@ -3698,6 +3726,7 @@ "supports" : [ ], "actions" : [ { "name" : "getEsTaskStatus", + "javaMethodName" : "getTaskStatus", "parameters" : [ { "name" : "nodeId", "type" : "string", @@ -3714,9 +3743,11 @@ "returns" : "string" }, { "name" : "getIndexSizes", + "javaMethodName" : "getIndexSizes", "returns" : "com.linkedin.timeseries.TimeseriesIndicesSizesResult" }, { "name" : "restoreIndices", + "javaMethodName" : "restoreIndices", "parameters" : [ { "name" : "aspect", "type" : "string", @@ -3741,6 +3772,7 @@ "returns" : "string" }, { "name" : "truncateTimeseriesAspect", + "javaMethodName" : "truncateTimeseriesAspect", "parameters" : [ { "name" : "entityType", "type" : "string" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json index 8391af60f8ece6..9e90a8910db873 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json @@ -1,5 +1,79 @@ { "models" : [ { + "type" : "record", + "name" : "BusinessAttributeAssociation", + "namespace" : "com.linkedin.businessattribute", + "include" : [ { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : "Urn", + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } ], + "fields" : [ ] + }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -111,42 +185,7 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, + "type" : "AuditStamp", "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -196,40 +235,7 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : "Urn", - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } + "items" : "com.linkedin.common.Edge" }, "doc" : "Data sources for the chart", "optional" : true, @@ -3505,54 +3511,75 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" + "include" : [ { + "type" : "record", + "name" : "EditableSchemaFieldBase", + "doc" : "Base class to describe metadata related to dataset schema.", + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" + } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" + } } - } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", + "optional" : true, + "Relationship" : { + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" + } + } + } ] + } ], + "fields" : [ { + "name" : "businessAttribute", + "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", + "doc" : "Business Attribute for this field.", "optional" : true, "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" + "/destinationUrn" : { + "createdActor" : "businessAttribute/created/actor", + "createdOn" : "businessAttribute/created/time", + "entityTypes" : [ "businessAttribute" ], + "name" : "EditableSchemaFieldWithBusinessAttribute", + "updatedActor" : "businessAttribute/lastModified/actor", + "updatedOn" : "businessAttribute/lastModified/time" } } } ] @@ -5535,13 +5562,14 @@ "type" : "GenericPayload", "doc" : "The event payload." } ] - }, "com.linkedin.mxe.PlatformEventHeader", "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.mxe.PlatformEventHeader", "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "platform", "namespace" : "com.linkedin.platform", "path" : "/platform", "schema" : "com.linkedin.entity.Entity", "doc" : "DataHub Platform Actions\n\ngenerated from: com.linkedin.metadata.resources.platform.PlatformResource", + "resourceClass" : "com.linkedin.metadata.resources.platform.PlatformResource", "collection" : { "identifier" : { "name" : "platformId", @@ -5550,6 +5578,7 @@ "supports" : [ ], "actions" : [ { "name" : "producePlatformEvent", + "javaMethodName" : "producePlatformEvent", "parameters" : [ { "name" : "name", "type" : "string" diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.usage.usageStats.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.usage.usageStats.snapshot.json index a21b0c1cd30bea..e8e68dae4c3688 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.usage.usageStats.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.usage.usageStats.snapshot.json @@ -164,6 +164,7 @@ "path" : "/usageStats", "schema" : "com.linkedin.usage.UsageAggregation", "doc" : "Rest.li entry point: /usageStats\n\ngenerated from: com.linkedin.metadata.resources.usage.UsageStats", + "resourceClass" : "com.linkedin.metadata.resources.usage.UsageStats", "simple" : { "supports" : [ ], "actions" : [ { @@ -171,12 +172,14 @@ "deprecated" : { } }, "name" : "batchIngest", + "javaMethodName" : "batchIngest", "parameters" : [ { "name" : "buckets", "type" : "{ \"type\" : \"array\", \"items\" : \"com.linkedin.usage.UsageAggregation\" }" } ] }, { "name" : "query", + "javaMethodName" : "query", "parameters" : [ { "name" : "resource", "type" : "string" @@ -199,6 +202,7 @@ "returns" : "com.linkedin.usage.UsageQueryResult" }, { "name" : "queryRange", + "javaMethodName" : "queryRange", "parameters" : [ { "name" : "resource", "type" : "string" From 0e8421376adf260fec51ab51d40f495d0601a003 Mon Sep 17 00:00:00 2001 From: ppurswan Date: Wed, 3 Jan 2024 22:34:16 +0530 Subject: [PATCH 04/50] business-attribute: Created initial version of Business Attribute Screens --- .../datahub/graphql/GmsGraphQLEngine.java | 13 +- .../ListBusinessAttributesResolver.java | 91 ++++++- .../mappers/BusinessAttributeMapper.java | 2 +- .../src/main/resources/entity.graphql | 53 +++- datahub-web-react/src/App.tsx | 2 + datahub-web-react/src/Mocks.tsx | 2 + datahub-web-react/src/app/SearchRoutes.tsx | 3 +- datahub-web-react/src/app/analytics/event.ts | 10 +- .../BusinessAttributeItemMenu.tsx | 65 +++++ .../businessAttribute/BusinessAttributes.tsx | 256 ++++++++++++++++++ .../CreateBusinessAttributeModal.tsx | 211 +++++++++++++++ .../utils/useDescriptionRenderer.tsx | 41 +++ .../utils/useTagsAndTermsRenderer.tsx | 38 +++ datahub-web-react/src/app/entity/Entity.tsx | 9 + .../src/app/entity/EntityRegistry.tsx | 5 + .../BusinessAttributeEntity.tsx | 159 +++++++++++ .../businessAttribute/preview/Preview.tsx | 32 +++ .../glossaryTerm/GlossaryTermEntity.tsx | 3 + .../profile/sidebar/SidebarTagsSection.tsx | 13 +- .../entity/shared/containers/profile/utils.ts | 4 + .../src/app/search/BrowseEntityCard.tsx | 6 +- .../src/app/shared/admin/HeaderLinks.tsx | 15 + .../src/app/shared/deleteUtils.ts | 5 + datahub-web-react/src/conf/Global.ts | 1 + .../src/graphql/businessAttribute.graphql | 70 +++++ datahub-web-react/src/graphql/search.graphql | 3 + 26 files changed, 1087 insertions(+), 25 deletions(-) create mode 100644 datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx create mode 100644 datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx create mode 100644 datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx create mode 100644 datahub-web-react/src/app/businessAttribute/utils/useDescriptionRenderer.tsx create mode 100644 datahub-web-react/src/app/businessAttribute/utils/useTagsAndTermsRenderer.tsx create mode 100644 datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx create mode 100644 datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx create mode 100644 datahub-web-react/src/graphql/businessAttribute.graphql diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 99a46c1a41a4de..b0d96d400ad401 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -68,6 +68,7 @@ import com.linkedin.datahub.graphql.generated.ListQueriesResult; import com.linkedin.datahub.graphql.generated.ListTestsResult; import com.linkedin.datahub.graphql.generated.ListViewsResult; +import com.linkedin.datahub.graphql.generated.ListBusinessAttributesResult; import com.linkedin.datahub.graphql.generated.MLFeature; import com.linkedin.datahub.graphql.generated.MLFeatureProperties; import com.linkedin.datahub.graphql.generated.MLFeatureTable; @@ -94,6 +95,7 @@ import com.linkedin.datahub.graphql.generated.Test; import com.linkedin.datahub.graphql.generated.TestResult; import com.linkedin.datahub.graphql.generated.UserUsageCounts; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.resolvers.MeResolver; import com.linkedin.datahub.graphql.resolvers.assertion.AssertionRunEventResolver; import com.linkedin.datahub.graphql.resolvers.assertion.DeleteAssertionResolver; @@ -1890,8 +1892,13 @@ private void configureIngestionSourceResolvers(final RuntimeWiring.Builder build private void configureBusinessAttributeResolver(final RuntimeWiring.Builder builder) { builder.type("BusinessAttribute", typeWiring -> typeWiring .dataFetcher("exists", new EntityExistsResolver(entityService)) - .dataFetcher("privileges", new EntityPrivilegesResolver(entityClient)) - ); - + .dataFetcher("privileges", new EntityPrivilegesResolver(entityClient))) + .type("ListBusinessAttributesResult", typeWiring -> typeWiring + .dataFetcher("businessAttributes", new LoadableTypeBatchResolver<>( + businessAttributeType, + (env) -> ((ListBusinessAttributesResult) env.getSource()).getBusinessAttributes().stream() + .map(BusinessAttribute::getUrn) + .collect(Collectors.toList()))) + ); } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java index de3a32b7832389..6afedb7b2e3a57 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java @@ -1,23 +1,90 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; -import com.linkedin.datahub.graphql.generated.SearchResults; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.ListBusinessAttributesInput; +import com.linkedin.datahub.graphql.generated.ListBusinessAttributesResult; import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.search.SearchEntity; +import com.linkedin.metadata.search.SearchResult; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; -import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.CompletableFuture; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; + +/** + * Resolver used for listing Business Attributes. + */ @Slf4j -@RequiredArgsConstructor -public class ListBusinessAttributesResolver implements DataFetcher> { - private final EntityClient _entityClient; - private static final int DEFAULT_START = 0; - private static final int DEFAULT_COUNT = 10; - - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - return null; +public class ListBusinessAttributesResolver implements DataFetcher> { + + private static final Integer DEFAULT_START = 0; + private static final Integer DEFAULT_COUNT = 20; + private static final String DEFAULT_QUERY = ""; + + private final EntityClient _entityClient; + + public ListBusinessAttributesResolver(@Nonnull final EntityClient entityClient) { + _entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null"); + } + + @Override + public CompletableFuture get(final DataFetchingEnvironment environment) throws Exception { + + final QueryContext context = environment.getContext(); + final ListBusinessAttributesInput input = bindArgument(environment.getArgument("input"), ListBusinessAttributesInput.class); + + return CompletableFuture.supplyAsync(() -> { + final Integer start = input.getStart() == null ? DEFAULT_START : input.getStart(); + final Integer count = input.getCount() == null ? DEFAULT_COUNT : input.getCount(); + final String query = input.getQuery() == null ? DEFAULT_QUERY : input.getQuery(); + + try { + + final SearchResult gmsResult = _entityClient.search( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + query, + Collections.emptyMap(), + start, + count, + context.getAuthentication(), + new SearchFlags().setFulltext(true)); + + final ListBusinessAttributesResult result = new ListBusinessAttributesResult(); + result.setStart(gmsResult.getFrom()); + result.setCount(gmsResult.getPageSize()); + result.setTotal(gmsResult.getNumEntities()); + result.setBusinessAttributes(mapUnresolvedBusinessAttributes(gmsResult.getEntities().stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()))); + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to list Business Attributes", e); + } + }); + } + + private List mapUnresolvedBusinessAttributes(final List entityUrns) { + final List results = new ArrayList<>(); + for (final Urn urn : entityUrns) { + final BusinessAttribute unresolvedBusinessAttribute = new BusinessAttribute(); + unresolvedBusinessAttribute.setUrn(urn.toString()); + unresolvedBusinessAttribute.setType(EntityType.BUSINESS_ATTRIBUTE); + results.add(unresolvedBusinessAttribute); } + return results; + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java index ad89071c194888..93a4301b1a362a 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -68,7 +68,7 @@ private void mapBusinessAttributeInfo(BusinessAttribute businessAttribute, DataM if (businessAttributeInfo.hasType()) { attributeInfo.setType(mapSchemaFieldDataType(businessAttributeInfo.getType())); } - businessAttribute.setBusinessAttributeInfo(attributeInfo); + businessAttribute.setProperties(attributeInfo); } private SchemaFieldDataType mapSchemaFieldDataType(@Nonnull final com.linkedin.schema.SchemaFieldDataType dataTypeUnion) { diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index dc75a94da0e3bb..f10cec261e5d29 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -237,6 +237,10 @@ type Query { """ businessAttribute(urn: String!): BusinessAttribute + """ + Fetch all Business Attributes + """ + listBusinessAttributes(input: ListBusinessAttributesInput!): ListBusinessAttributesResult } """ @@ -11292,7 +11296,7 @@ type BusinessAttribute implements Entity { """ Properties about a Business Attribute """ - businessAttributeInfo: BusinessAttributeInfo + properties: BusinessAttributeInfo """ Ownership metadata of the Business Attribute @@ -11429,4 +11433,49 @@ input AddBusinessAttributeInput { resource urns to add the business attribute to """ resourceUrn: ResourceRefInput! -} \ No newline at end of file +} + +""" +Input provided when listing Business Attribute +""" +input ListBusinessAttributesInput { + """ + The starting offset of the result set returned + """ + start: Int + + """ + The maximum number of Business Attributes to be returned in the result set + """ + count: Int + + """ + Optional search query + """ + query: String +} + +""" +The result obtained when listing Business Attribute +""" +type ListBusinessAttributesResult { + """ + The starting offset of the result set returned + """ + start: Int! + + """ + The number of Business Attributes in the returned result set + """ + count: Int! + + """ + The total number of Business Attributes in the result set + """ + total: Int! + + """ + The Business Attributes + """ + businessAttributes: [BusinessAttribute!]! +} diff --git a/datahub-web-react/src/App.tsx b/datahub-web-react/src/App.tsx index 342a89f350429f..471b0bced3f4cf 100644 --- a/datahub-web-react/src/App.tsx +++ b/datahub-web-react/src/App.tsx @@ -37,6 +37,7 @@ import { DataProductEntity } from './app/entity/dataProduct/DataProductEntity'; import { DataPlatformInstanceEntity } from './app/entity/dataPlatformInstance/DataPlatformInstanceEntity'; import { RoleEntity } from './app/entity/Access/RoleEntity'; import possibleTypesResult from './possibleTypes.generated'; +import { BusinessAttributeEntity } from './app/entity/businessAttribute/BusinessAttributeEntity'; /* Construct Apollo Client @@ -124,6 +125,7 @@ const App: React.VFC = () => { register.register(new DataPlatformEntity()); register.register(new DataProductEntity()); register.register(new DataPlatformInstanceEntity()); + register.register(new BusinessAttributeEntity()); return register; }, []); diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index a2e14308e8cee2..72eb6176bd4f50 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -3633,4 +3633,6 @@ export const platformPrivileges: PlatformPrivileges = { manageGlobalViews: true, manageOwnershipTypes: true, manageGlobalAnnouncements: true, + createBusinessAttributes: true, + manageBusinessAttributes: true, }; diff --git a/datahub-web-react/src/app/SearchRoutes.tsx b/datahub-web-react/src/app/SearchRoutes.tsx index d2ad4ab6f4db19..766c6689c3fcac 100644 --- a/datahub-web-react/src/app/SearchRoutes.tsx +++ b/datahub-web-react/src/app/SearchRoutes.tsx @@ -14,7 +14,7 @@ import { SettingsPage } from './settings/SettingsPage'; import DomainRoutes from './domain/DomainRoutes'; import { useIsNestedDomainsEnabled } from './useAppConfig'; import { ManageDomainsPage } from './domain/ManageDomainsPage'; - +import { BusinessAttributes } from './businessAttribute/BusinessAttributes'; /** * Container for all searchable page routes */ @@ -50,6 +50,7 @@ export const SearchRoutes = (): JSX.Element => { } /> } /> } /> + } /> diff --git a/datahub-web-react/src/app/analytics/event.ts b/datahub-web-react/src/app/analytics/event.ts index 27340264009336..a06a99916b9efb 100644 --- a/datahub-web-react/src/app/analytics/event.ts +++ b/datahub-web-react/src/app/analytics/event.ts @@ -80,6 +80,8 @@ export enum EventType { EmbedProfileViewEvent, EmbedProfileViewInDataHubEvent, EmbedLookupNotFoundEvent, + CreateBusinessAttributeEvent, + UpdateBusinessAttributeEvent, } /** @@ -624,6 +626,11 @@ export interface EmbedLookupNotFoundEvent extends BaseEvent { reason: EmbedLookupNotFoundReason; } +export interface CreateBusinessAttributeEvent extends BaseEvent { + type: EventType.CreateBusinessAttributeEvent; + name: string; +} + /** * Event consisting of a union of specific event types. */ @@ -700,4 +707,5 @@ export type Event = | DeselectQuickFilterEvent | EmbedProfileViewEvent | EmbedProfileViewInDataHubEvent - | EmbedLookupNotFoundEvent; + | EmbedLookupNotFoundEvent + | CreateBusinessAttributeEvent; diff --git a/datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx b/datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx new file mode 100644 index 00000000000000..ae306998910daa --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { DeleteOutlined } from '@ant-design/icons'; +import { Dropdown, Menu, message, Modal } from 'antd'; +import { MenuIcon } from '../entity/shared/EntityDropdown/EntityDropdown'; +import { useDeletePostMutation } from '../../graphql/post.generated'; + +type Props = { + urn: string; + title: string | undefined; + onDelete?: () => void; +}; + +export default function BusinessAttributeItemMenu({ title, urn, onDelete }: Props) { + const [deletePostMutation] = useDeletePostMutation(); + + const deletePost = () => { + deletePostMutation({ + variables: { + urn, + }, + }) + .then(({ errors }) => { + if (!errors) { + message.success('Deleted Business Attribute!'); + onDelete?.(); + } + }) + .catch(() => { + message.destroy(); + message.error({ + content: `Failed to delete Business Attribute!: An unknown error occurred.`, + duration: 3, + }); + }); + }; + + const onConfirmDelete = () => { + Modal.confirm({ + title: `Delete Business Attribute '${title}'`, + content: `Are you sure you want to remove this Business Attribute?`, + onOk() { + deletePost(); + }, + onCancel() {}, + okText: 'Yes', + maskClosable: true, + closable: true, + }); + }; + + return ( + + +  Delete + + + } + > + + + ); +} diff --git a/datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx b/datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx new file mode 100644 index 00000000000000..7533d67f7b69a3 --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx @@ -0,0 +1,256 @@ +import React, { useState, useMemo } from 'react'; +import styled from 'styled-components'; +import { Button, Empty, message, Pagination, Typography } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { AlignType } from 'rc-table/lib/interface'; +import { Link } from 'react-router-dom'; +import { useListBusinessAttributesQuery } from '../../graphql/businessAttribute.generated'; +import { Message } from '../shared/Message'; +import TabToolbar from '../entity/shared/components/styled/TabToolbar'; +import { StyledTable } from '../entity/shared/components/styled/StyledTable'; +import CreateBusinessAttributeModal from './CreateBusinessAttributeModal'; +import { scrollToTop } from '../shared/searchUtils'; +import { useUserContext } from '../context/useUserContext'; +import { BusinessAttribute } from '../../types.generated'; +import { SearchBar } from '../search/SearchBar'; +import { useEntityRegistry } from '../useEntityRegistry'; +import useTagsAndTermsRenderer from './utils/useTagsAndTermsRenderer'; +import useDescriptionRenderer from './utils/useDescriptionRenderer'; +import BusinessAttributeItemMenu from './BusinessAttributeItemMenu'; + +function BusinessAttributeListMenuColumn(handleDelete: () => void) { + return (record: BusinessAttribute) => ( + handleDelete()} /> + ); +} + +const SourceContainer = styled.div` + width: 100%; + padding-top: 20px; + padding-right: 40px; + padding-left: 40px; + display: flex; + flex-direction: column; + overflow: auto; +`; + +const BusinessAttributesContainer = styled.div` + padding-top: 0px; +`; + +const BusinessAttributeHeaderContainer = styled.div` + && { + padding-left: 0px; + } +`; + +const BusinessAttributeTitle = styled(Typography.Title)` + && { + margin-bottom: 8px; + } +`; + +const PaginationContainer = styled.div` + display: flex; + justify-content: center; +`; + +const searchBarStyle = { + maxWidth: 220, + padding: 0, +}; + +const searchBarInputStyle = { + height: 32, + fontSize: 12, +}; + +const DEFAULT_PAGE_SIZE = 10; + +export const BusinessAttributes = () => { + const [isCreatingBusinessAttribute, setIsCreatingBusinessAttribute] = useState(false); + const entityRegistry = useEntityRegistry(); + + // Current User Urn + const authenticatedUser = useUserContext(); + + const canCreateBusinessAttributes = authenticatedUser?.platformPrivileges?.createBusinessAttributes; + const [page, setPage] = useState(1); + const pageSize = DEFAULT_PAGE_SIZE; + const start = (page - 1) * pageSize; + const [query, setQuery] = useState(undefined); + const [tagHoveredUrn, setTagHoveredUrn] = useState(undefined); + + const { + loading: businessAttributeLoading, + error: businessAttributeError, + data: businessAttributeData, + refetch: businessAttributeRefetch, + } = useListBusinessAttributesQuery({ + variables: { + start, + count: pageSize, + query, + }, + }); + const descriptionRender = useDescriptionRenderer(businessAttributeRefetch); + const tagRenderer = useTagsAndTermsRenderer( + tagHoveredUrn, + setTagHoveredUrn, + { + showTags: true, + showTerms: false, + }, + query || '', + businessAttributeRefetch, + ); + + const termRenderer = useTagsAndTermsRenderer( + tagHoveredUrn, + setTagHoveredUrn, + { + showTags: false, + showTerms: true, + }, + query || '', + businessAttributeRefetch, + ); + + const totalBusinessAttributes = businessAttributeData?.listBusinessAttributes?.total || 0; + const businessAttributes = useMemo( + () => businessAttributeData?.listBusinessAttributes?.businessAttributes || [], + [businessAttributeData], + ); + + const onTagTermCell = (record: BusinessAttribute) => ({ + onMouseEnter: () => { + setTagHoveredUrn(record.urn); + }, + onMouseLeave: () => { + setTagHoveredUrn(undefined); + }, + }); + + const handleDelete = () => { + setTimeout(() => { + businessAttributeRefetch?.(); + }, 2000); + }; + const tableData = businessAttributes; + const tableColumns = [ + { + width: '20%', + title: 'Name', + dataIndex: ['properties', 'name'], + key: 'name', + render: (name: string, record: any) => ( + {name} + ), + }, + { + title: 'Description', + dataIndex: ['properties', 'description'], + key: 'description', + // render: (description: string) => description || '', + render: descriptionRender, + }, + { + width: '20%', + title: 'Tags', + dataIndex: ['properties', 'tags'], + key: 'tags', + render: tagRenderer, + onCell: onTagTermCell, + }, + { + width: '20%', + title: 'Glossary Terms', + dataIndex: ['properties', 'glossaryTags'], + key: 'glossaryTags', + render: termRenderer, + onCell: onTagTermCell, + }, + { + width: '13%', + title: 'Data Type', + dataIndex: ['properties', 'businessAttributeDataType'], + key: 'businessAttributeDataType', + render: (dataType: string) => dataType || '', + }, + { + title: '', + dataIndex: '', + width: '5%', + align: 'right' as AlignType, + key: 'menu', + render: BusinessAttributeListMenuColumn(handleDelete), + }, + ]; + + const onChangePage = (newPage: number) => { + scrollToTop(); + setPage(newPage); + }; + + return ( + + {businessAttributeLoading && !businessAttributeData && ( + + )} + {businessAttributeError && message.error('Failed to load businessAttributes :(')} + + + Business Attribute + View your Business Attributes + + + + + null} + onQueryChange={(q) => setQuery(q.length > 0 ? q : undefined)} + entityRegistry={entityRegistry} + /> + + , + }} + pagination={false} + /> + + + + setIsCreatingBusinessAttribute(false)} + onCreateBusinessAttribute={() => { + businessAttributeRefetch?.(); + }} + /> + + ); +}; diff --git a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx new file mode 100644 index 00000000000000..975d707831e736 --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx @@ -0,0 +1,211 @@ +import React, { useState } from 'react'; +import { message, Button, Input, Modal, Typography, Form, Select } from 'antd'; +import styled from 'styled-components'; +import { EditOutlined } from '@ant-design/icons'; +import DOMPurify from 'dompurify'; +import { useEnterKeyListener } from '../shared/useEnterKeyListener'; +import { useCreateBusinessAttributeMutation } from '../../graphql/businessAttribute.generated'; +import { CreateBusinessAttributeInput, SchemaFieldDataType, EntityType } from '../../types.generated'; +import analytics, { EventType } from '../analytics'; +import { useEntityRegistry } from '../useEntityRegistry'; +import DescriptionModal from '../entity/shared/components/legacy/DescriptionModal'; + +type Props = { + visible: boolean; + onClose: () => void; + onCreateBusinessAttribute: () => void; +}; + +type FormProps = { + name: string; + description?: string; + dataType?: SchemaFieldDataType; +}; + +const DataTypeSelectContainer = styled.div` + padding: 1px; +`; + +const DataTypeSelect = styled(Select)` + && { + width: 100%; + margin-top: 1em; + margin-bottom: 1em; + } +`; + +const StyledItem = styled(Form.Item)` + margin-bottom: 0; +`; + +const OptionalWrapper = styled.span` + font-weight: normal; +`; + +const StyledButton = styled(Button)` + padding: 0; +`; + +// Ensures that any newly added datatype is automatically included in the user dropdown. +const DATA_TYPES = Object.values(SchemaFieldDataType); + +export default function CreateBusinessAttributeModal({ visible, onClose, onCreateBusinessAttribute }: Props) { + const [createButtonEnabled, setCreateButtonEnabled] = useState(true); + + const [createBusinessAttribute] = useCreateBusinessAttributeMutation(); + + const [isDocumentationModalVisible, setIsDocumentationModalVisible] = useState(false); + + const [documentation, setDocumentation] = useState(''); + + const [form] = Form.useForm(); + + const entityRegistry = useEntityRegistry(); + + // Function to handle the close or cross button of Create Business Attribute Modal + const onModalClose = () => { + form.resetFields(); + onClose(); + }; + + const onCreateNewBusinessAttribute = () => { + const { name, dataType } = form.getFieldsValue(); + const sanitizedDescription = DOMPurify.sanitize(documentation); + const input: CreateBusinessAttributeInput = { + businessAttributeInfo: { name, description: sanitizedDescription, type: dataType }, + }; + createBusinessAttribute({ variables: { input } }) + .then(() => { + message.loading({ content: 'Updating...', duration: 2 }); + setTimeout(() => { + analytics.event({ + type: EventType.CreateBusinessAttributeEvent, + name, + }); + message.success({ + content: `Created ${entityRegistry.getEntityName(EntityType.BusinessAttribute)}!`, + duration: 2, + }); + if (onCreateBusinessAttribute) { + onCreateBusinessAttribute(); + } + }, 2000); + }) + .catch((e) => { + message.destroy(); + message.error({ content: `Failed to create: \n ${e.message || ''}`, duration: 3 }); + }); + onModalClose(); + }; + + // Handle the Enter press + useEnterKeyListener({ + querySelectorToExecuteClick: '#createBusinessAttributeButton', + }); + + function addDocumentation(description: string) { + setDocumentation(description); + setIsDocumentationModalVisible(false); + } + + return ( + <> + + + + + } + > +
+ setCreateButtonEnabled(form.getFieldsError().some((field) => field.errors.length > 0)) + } + > + Name}> + + + + + + Data Type}> + + + {DATA_TYPES.map((dataType: SchemaFieldDataType) => ( + + {dataType} + + ))} + + + + + + Documentation (optional) + + } + > + setIsDocumentationModalVisible(true)}> + + {documentation ? 'Edit' : 'Add'} Documentation + + {isDocumentationModalVisible && ( + setIsDocumentationModalVisible(false)} + onSubmit={addDocumentation} + description={documentation} + /> + )} + +
+
+ + ); +} diff --git a/datahub-web-react/src/app/businessAttribute/utils/useDescriptionRenderer.tsx b/datahub-web-react/src/app/businessAttribute/utils/useDescriptionRenderer.tsx new file mode 100644 index 00000000000000..ef665e45aeefdd --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/utils/useDescriptionRenderer.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; +import DOMPurify from 'dompurify'; +import { BusinessAttribute } from '../../../types.generated'; +import DescriptionField from '../../entity/dataset/profile/schema/components/SchemaDescriptionField'; +import { useUpdateDescriptionMutation } from '../../../graphql/mutations.generated'; + +export default function useDescriptionRenderer(businessAttributeRefetch: () => Promise) { + const [updateDescription] = useUpdateDescriptionMutation(); + const [expandedRows, setExpandedRows] = useState({}); + + const refresh: any = () => { + businessAttributeRefetch?.(); + }; + + return (description: string, record: BusinessAttribute, index: number): JSX.Element => { + const relevantEditableFieldInfo = record?.properties; + const displayedDescription = relevantEditableFieldInfo?.description || description; + const sanitizedDescription = DOMPurify.sanitize(displayedDescription); + + const handleExpandedRows = (expanded) => setExpandedRows((prev) => ({ ...prev, [index]: expanded })); + + return ( + + updateDescription({ + variables: { + input: { + description: DOMPurify.sanitize(updatedDescription), + resourceUrn: record.urn, + }, + }, + }).then(refresh) + } + /> + ); + }; +} +// diff --git a/datahub-web-react/src/app/businessAttribute/utils/useTagsAndTermsRenderer.tsx b/datahub-web-react/src/app/businessAttribute/utils/useTagsAndTermsRenderer.tsx new file mode 100644 index 00000000000000..7c138c99dbd1a8 --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/utils/useTagsAndTermsRenderer.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { EntityType, GlobalTags, BusinessAttribute } from '../../../types.generated'; +import TagTermGroup from '../../shared/tags/TagTermGroup'; + +export default function useTagsAndTermsRenderer( + tagHoveredUrn: string | undefined, + setTagHoveredUrn: (index: string | undefined) => void, + options: { showTags: boolean; showTerms: boolean }, + filterText: string, + businessAttributeRefetch: () => Promise, +) { + const urn = tagHoveredUrn; + + const refresh: any = () => { + businessAttributeRefetch?.(); + }; + + const tagAndTermRender = (tags: GlobalTags, record: BusinessAttribute) => { + return ( +
+ setTagHoveredUrn(undefined)} + entityUrn={urn} + entityType={EntityType.BusinessAttribute} + highlightText={filterText} + refetch={refresh} + /> +
+ ); + }; + return tagAndTermRender; +} diff --git a/datahub-web-react/src/app/entity/Entity.tsx b/datahub-web-react/src/app/entity/Entity.tsx index 5920919a9cdab2..4d43ded678a2f9 100644 --- a/datahub-web-react/src/app/entity/Entity.tsx +++ b/datahub-web-react/src/app/entity/Entity.tsx @@ -80,6 +80,10 @@ export enum EntityCapabilityType { * Assigning the entity to a data product */ DATA_PRODUCTS, + /** + * Assigning Business Attribute to a entity + */ + BUSINESS_ATTRIBUTES, } /** @@ -176,4 +180,9 @@ export interface Entity { * Returns the profile component to be displayed in our Chrome extension */ renderEmbeddedProfile?: (urn: string) => JSX.Element; + + /** + * Returns the url to be navigated to when clicked on Cards + */ + getCustomCardUrlPath?: () => string | undefined; } diff --git a/datahub-web-react/src/app/entity/EntityRegistry.tsx b/datahub-web-react/src/app/entity/EntityRegistry.tsx index 6642c2c7b0467c..f7bffa4159c154 100644 --- a/datahub-web-react/src/app/entity/EntityRegistry.tsx +++ b/datahub-web-react/src/app/entity/EntityRegistry.tsx @@ -211,4 +211,9 @@ export default class EntityRegistry { .map((entity) => entity.type), ); } + + getCustomCardUrlPath(type: EntityType): string | undefined { + const entity = validatedGet(type, this.entityTypeToEntity); + return entity.getCustomCardUrlPath?.(); + } } diff --git a/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx new file mode 100644 index 00000000000000..c75393ff013cc8 --- /dev/null +++ b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { GlobalOutlined } from '@ant-design/icons'; +import { BusinessAttribute, EntityType, SearchResult } from '../../../types.generated'; +import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from '../Entity'; +import { getDataForEntityType } from '../shared/containers/profile/utils'; +import { EntityProfile } from '../shared/containers/profile/EntityProfile'; +import { useGetBusinessAttributeQuery } from '../../../graphql/businessAttribute.generated'; +import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; +import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab'; +// import GlossaryRelatedEntity from './profile/GlossaryRelatedEntity'; +import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import { SidebarAboutSection } from '../shared/containers/profile/sidebar/AboutSection/SidebarAboutSection'; +import { SidebarOwnerSection } from '../shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection'; +import { SidebarTagsSection } from '../shared/containers/profile/sidebar/SidebarTagsSection'; +import { Preview } from './preview/Preview'; +import { PageRoutes } from '../../../conf/Global'; + +/** + * Definition of datahub Business Attribute Entity + */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +export class BusinessAttributeEntity implements Entity { + type: EntityType = EntityType.BusinessAttribute; + + icon = (fontSize: number, styleType: IconStyleType, color?: string) => { + if (styleType === IconStyleType.TAB_VIEW) { + return ; + } + + if (styleType === IconStyleType.HIGHLIGHT) { + return ; + } + + if (styleType === IconStyleType.SVG) { + // TODO: Update the returned path value to the correct svg icon path + return ( + + ); + } + + return ( + + ); + }; + + displayName = (data: BusinessAttribute) => { + console.log('displayName:::', data?.properties); + return data?.properties?.name || data?.urn; + }; + + getPathName = () => 'business-attribute'; + + getEntityName = () => 'Business Attribute'; + + getCollectionName = () => 'Business Attributes'; + + getCustomCardUrlPath = () => PageRoutes.BUSINESS_ATTRIBUTE; + + isBrowseEnabled = () => true; + + isLineageEnabled = () => false; + + isSearchEnabled = () => true; + + getOverridePropertiesFromEntity = (data: BusinessAttribute) => { + return { + name: data.properties?.name, + }; + }; + + getGenericEntityProperties = (data: BusinessAttribute) => { + return getDataForEntityType({ + data, + entityType: this.type, + getOverrideProperties: this.getOverridePropertiesFromEntity, + }); + }; + + renderPreview = (_: PreviewType, data: BusinessAttribute) => { + return ( + + ); + }; + + renderProfile = (urn: string) => { + return ( + + ); + }; + + renderSearch = (result: SearchResult) => { + return ( + + ); + }; + + supportedCapabilities = () => { + return new Set([ + EntityCapabilityType.OWNERS, + EntityCapabilityType.TAGS, + EntityCapabilityType.GLOSSARY_TERMS, + EntityCapabilityType.BUSINESS_ATTRIBUTES, + ]); + }; +} diff --git a/datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx b/datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx new file mode 100644 index 00000000000000..d402fef600d525 --- /dev/null +++ b/datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { BookOutlined } from '@ant-design/icons'; +import { EntityType, Owner } from '../../../../types.generated'; +import DefaultPreviewCard from '../../../preview/DefaultPreviewCard'; +import { useEntityRegistry } from '../../../useEntityRegistry'; +import { IconStyleType } from '../../Entity'; + +export const Preview = ({ + urn, + name, + description, + owners, +}: { + urn: string; + name: string; + description?: string | null; + owners?: Array | null; +}): JSX.Element => { + const entityRegistry = useEntityRegistry(); + return ( + } + type="Business Attribute" + typeIcon={entityRegistry.getIcon(EntityType.BusinessAttribute, 14, IconStyleType.ACCENT)} + /> + ); +}; diff --git a/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx b/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx index 080ee5889aec92..27759f49fce970 100644 --- a/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx +++ b/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx @@ -17,6 +17,7 @@ import { SidebarAboutSection } from '../shared/containers/profile/sidebar/AboutS import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import { EntityActionItem } from '../shared/entity/EntityActions'; import { SidebarDomainSection } from '../shared/containers/profile/sidebar/Domain/SidebarDomainSection'; +import { PageRoutes } from '../../../conf/Global'; /** * Definition of the DataHub Dataset entity. @@ -57,6 +58,8 @@ export class GlossaryTermEntity implements Entity { getEntityName = () => 'Glossary Term'; + getCustomCardUrlPath = () => PageRoutes.GLOSSARY; + renderProfile = (urn) => { return ( { { (o || {})[p], obj); +} diff --git a/datahub-web-react/src/app/search/BrowseEntityCard.tsx b/datahub-web-react/src/app/search/BrowseEntityCard.tsx index 76da58e1ed6d2e..beacce04b4340e 100644 --- a/datahub-web-react/src/app/search/BrowseEntityCard.tsx +++ b/datahub-web-react/src/app/search/BrowseEntityCard.tsx @@ -18,9 +18,9 @@ export const BrowseEntityCard = ({ entityType, count }: { entityType: EntityType const history = useHistory(); const entityRegistry = useEntityRegistry(); const showBrowseV2 = useIsBrowseV2(); - const isGlossaryEntityCard = entityType === EntityType.GlossaryTerm; const entityPathName = entityRegistry.getPathName(entityType); - const url = isGlossaryEntityCard ? PageRoutes.GLOSSARY : `${PageRoutes.BROWSE}/${entityPathName}`; + const customCardUrlPath = entityRegistry.getCustomCardUrlPath(entityType); + const url = customCardUrlPath || `${PageRoutes.BROWSE}/${entityPathName}`; const onBrowseEntityCardClick = () => { analytics.event({ type: EventType.HomePageBrowseResultClickEvent, @@ -29,7 +29,7 @@ export const BrowseEntityCard = ({ entityType, count }: { entityType: EntityType }; function browse() { - if (showBrowseV2 && !isGlossaryEntityCard) { + if (showBrowseV2 && !customCardUrlPath) { navigateToSearchUrl({ query: '*', filters: [{ field: ENTITY_SUB_TYPE_FILTER_NAME, values: [entityType] }], diff --git a/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx b/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx index 4a7a4938ea9709..3d141fac1d5034 100644 --- a/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx +++ b/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx @@ -7,6 +7,7 @@ import { SettingOutlined, SolutionOutlined, DownOutlined, + GlobalOutlined, } from '@ant-design/icons'; import { Link } from 'react-router-dom'; import { Button, Dropdown, Menu, Tooltip } from 'antd'; @@ -119,6 +120,20 @@ export function HeaderLinks(props: Props) { Manage related groups of data assets + + + + + Business Attribute + + Universal field for data consistency + + } > diff --git a/datahub-web-react/src/app/shared/deleteUtils.ts b/datahub-web-react/src/app/shared/deleteUtils.ts index 37a3758712ad6c..a831f9338a53f5 100644 --- a/datahub-web-react/src/app/shared/deleteUtils.ts +++ b/datahub-web-react/src/app/shared/deleteUtils.ts @@ -3,6 +3,7 @@ import { useDeleteAssertionMutation } from '../../graphql/assertion.generated'; import { useDeleteDataProductMutation } from '../../graphql/dataProduct.generated'; import { useDeleteDomainMutation } from '../../graphql/domain.generated'; import { useDeleteGlossaryEntityMutation } from '../../graphql/glossary.generated'; +import { useDeleteBusinessAttributeMutation } from '../../graphql/businessAttribute.generated'; import { useRemoveGroupMutation } from '../../graphql/group.generated'; import { useDeleteTagMutation } from '../../graphql/tag.generated'; import { useRemoveUserMutation } from '../../graphql/user.generated'; @@ -34,6 +35,8 @@ export const getEntityProfileDeleteRedirectPath = (type: EntityType, entityData: return `/domain/${domain.urn}/Data Products`; } return '/'; + case EntityType.BusinessAttribute: + return `${PageRoutes.BUSINESS_ATTRIBUTE}`; default: return () => undefined; } @@ -63,6 +66,8 @@ export const getDeleteEntityMutation = (type: EntityType) => { return useDeleteGlossaryEntityMutation; case EntityType.DataProduct: return useDeleteDataProductMutation; + case EntityType.BusinessAttribute: + return useDeleteBusinessAttributeMutation; default: return () => undefined; } diff --git a/datahub-web-react/src/conf/Global.ts b/datahub-web-react/src/conf/Global.ts index 82378bb6214271..ac63744be056db 100644 --- a/datahub-web-react/src/conf/Global.ts +++ b/datahub-web-react/src/conf/Global.ts @@ -30,6 +30,7 @@ export enum PageRoutes { EMBED = '/embed', EMBED_LOOKUP = '/embed/lookup/:url', SETTINGS_POSTS = '/settings/posts', + BUSINESS_ATTRIBUTE = '/business-attribute', } /** diff --git a/datahub-web-react/src/graphql/businessAttribute.graphql b/datahub-web-react/src/graphql/businessAttribute.graphql new file mode 100644 index 00000000000000..9464029cdb3fb2 --- /dev/null +++ b/datahub-web-react/src/graphql/businessAttribute.graphql @@ -0,0 +1,70 @@ +# Get a business attribute by URN +query getBusinessAttribute($urn: String!) { + businessAttribute(urn: $urn) { + ...businessAttributeFields + } +} + +query listBusinessAttributes($start: Int!, $count: Int!, $query: String) { + listBusinessAttributes(input: { start: $start, count: $count, query: $query }) { + start + count + total + businessAttributes { + ...businessAttributeFields + } + } +} + +fragment businessAttributeFields on BusinessAttribute { + urn + type + ownership { + ...ownershipFields + } + properties { + name + description + businessAttributeDataType: type + tags { + tags { + tag { + urn + name + properties { + name + } + } + associatedUrn + } + } + glossaryTerms { + terms { + term { + urn + type + properties { + name + } + } + associatedUrn + } + } + } +} + +mutation createBusinessAttribute($input: CreateBusinessAttributeInput!) { + createBusinessAttribute(input: $input) { + ...businessAttributeFields + } +} + +mutation deleteBusinessAttribute($urn: String!) { + deleteBusinessAttribute(urn: $urn) +} + +mutation updateBusinessAttribute($urn: String!, $input: UpdateBusinessAttributeInput!) { + updateBusinessAttribute(urn: $urn, input: $input) { + ...businessAttributeFields + } +} diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index 876be12fd335b7..f978139c41b1ba 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -801,6 +801,9 @@ fragment searchResultFields on Entity { ... on DataProduct { ...dataProductSearchFields } + ... on BusinessAttribute { + ...businessAttributeFields + } } fragment facetFields on FacetMetadata { From 5606c915de46c2daf7d97aa546b4204a04da432b Mon Sep 17 00:00:00 2001 From: ppurswan Date: Wed, 10 Jan 2024 13:55:53 +0530 Subject: [PATCH 05/50] Added lastModified and Created for business Attribute --- datahub-web-react/src/graphql/businessAttribute.graphql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/datahub-web-react/src/graphql/businessAttribute.graphql b/datahub-web-react/src/graphql/businessAttribute.graphql index 9464029cdb3fb2..d801f7a92d7551 100644 --- a/datahub-web-react/src/graphql/businessAttribute.graphql +++ b/datahub-web-react/src/graphql/businessAttribute.graphql @@ -26,6 +26,12 @@ fragment businessAttributeFields on BusinessAttribute { name description businessAttributeDataType: type + lastModified { + time + } + created { + time + } tags { tags { tag { From 583d64772fafa339aacc3a55196059a0bd3b4980 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Thu, 11 Jan 2024 00:43:32 +0530 Subject: [PATCH 06/50] business-attribute: refactoring and added lastmodified --- .../AddBusinessAttributeResolver.java | 12 +++---- .../CreateBusinessAttributeResolver.java | 25 +++++++-------- .../DeleteBusinessAttributeResolver.java | 12 +++---- .../RemoveBusinessAttributeResolver.java | 8 ++--- .../UpdateBusinessAttributeResolver.java | 8 +++-- .../src/main/resources/entity.graphql | 31 +++++++------------ 6 files changed, 43 insertions(+), 53 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java index 2af48612f78564..ee05fbda70b194 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java @@ -30,26 +30,22 @@ @Slf4j @RequiredArgsConstructor public class AddBusinessAttributeResolver implements DataFetcher> { - private final EntityClient _entityClient; private final EntityService _entityService; - @Override public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { final QueryContext context = environment.getContext(); AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); ResourceRefInput resourceRefInput = input.getResourceUrn(); - + //TODO: add authorization check + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); + } return CompletableFuture.supplyAsync(() -> { try { - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new IllegalArgumentException("The Business Attribute provided dos not exist"); - } validateInputResource(resourceRefInput); - addBusinessAttribute(businessAttributeUrn, resourceRefInput, context); - return true; } catch (Exception e) { throw new RuntimeException(String.format("Failed to add Business Attribute with urn %s to dataset with urn %s", diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java index dbe45a61676ece..8a4916b0e28567 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java @@ -47,12 +47,10 @@ public class CreateBusinessAttributeResolver implements DataFetcher get(DataFetchingEnvironment environment) throws Exception { final QueryContext context = environment.getContext(); CreateBusinessAttributeInput input = bindArgument(environment.getArgument("input"), CreateBusinessAttributeInput.class); - + if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } return CompletableFuture.supplyAsync(() -> { - if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - try { final BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); businessAttributeKey.setId(UUID.randomUUID().toString()); @@ -63,10 +61,10 @@ public CompletableFuture get(DataFetchingEnvironment environm throw new IllegalArgumentException("This Business Attribute already exists!"); } - if (BusinessAttributeUtils.hasNameConflict(input.getBusinessAttributeInfo().getName(), context, _entityClient)) { + if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { throw new DataHubGraphQLException( String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", - input.getBusinessAttributeInfo().getName()), DataHubGraphQLErrorCode.CONFLICT); + input.getName()), DataHubGraphQLErrorCode.CONFLICT); } // Create the MCP @@ -88,19 +86,20 @@ public CompletableFuture get(DataFetchingEnvironment environm } catch (DataHubGraphQLException e) { throw e; } catch (Exception e) { - log.error("Failed to create Business Attribute with name: {}: {}", input.getBusinessAttributeInfo().getName(), e.getMessage()); - throw new RuntimeException(String.format("Failed to create Business Attribute with name: %s", input.getBusinessAttributeInfo().getName()), e); + log.error("Failed to create Business Attribute with name: {}: {}", input.getName(), e.getMessage()); + throw new RuntimeException(String.format("Failed to create Business Attribute with name: %s", input.getName()), e); } }); } private BusinessAttributeInfo mapBusinessAttributeInfo(CreateBusinessAttributeInput input, QueryContext context) { final BusinessAttributeInfo info = new BusinessAttributeInfo(); - info.setFieldPath(input.getBusinessAttributeInfo().getName(), SetMode.DISALLOW_NULL); - info.setName(input.getBusinessAttributeInfo().getName(), SetMode.DISALLOW_NULL); - info.setDescription(input.getBusinessAttributeInfo().getDescription(), SetMode.IGNORE_NULL); - info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(input.getBusinessAttributeInfo().getType()), SetMode.IGNORE_NULL); + info.setFieldPath(input.getName(), SetMode.DISALLOW_NULL); + info.setName(input.getName(), SetMode.DISALLOW_NULL); + info.setDescription(input.getDescription(), SetMode.IGNORE_NULL); + info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(input.getType()), SetMode.IGNORE_NULL); info.setCreated(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis())); + info.setLastModified(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis())); return info; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java index 63f274251dac7f..ebbe68e8ea4143 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java @@ -24,14 +24,14 @@ public class DeleteBusinessAttributeResolver implements DataFetcher get(DataFetchingEnvironment environment) throws Exception { final QueryContext context = environment.getContext(); final Urn businessAttributeUrn = UrnUtils.getUrn(environment.getArgument("urn")); + if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); + } return CompletableFuture.supplyAsync(() -> { - if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } try { - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new IllegalArgumentException("The Business Attribute provided dos not exist"); - } _entityClient.deleteEntity(businessAttributeUrn, context.getAuthentication()); CompletableFuture.runAsync(() -> { try { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java index 565f5420150272..f497d63fbd6eca 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java @@ -38,12 +38,12 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); ResourceRefInput resourceRefInput = input.getResourceUrn(); - + //TODO: add authorization check + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); + } return CompletableFuture.supplyAsync(() -> { try { - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new IllegalArgumentException("The Business Attribute provided dos not exist"); - } validateInputResource(resourceRefInput, context); removeBusinessAttribute(resourceRefInput, context); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java index e015d472d0bceb..c1a31ef0ae05aa 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java @@ -2,6 +2,7 @@ import com.datahub.authentication.Authentication; import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.common.AuditStamp; import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; @@ -44,11 +45,11 @@ public CompletableFuture get(DataFetchingEnvironment environm if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); } + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); + } return CompletableFuture.supplyAsync(() -> { try { - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new IllegalArgumentException("The Business Attribute provided dos not exist"); - } Urn updatedBusinessAttributeUrn = updateBusinessAttribute(input, businessAttributeUrn, context); return BusinessAttributeMapper.map( businessAttributeService.getBusinessAttributeEntityResponse(updatedBusinessAttributeUrn, context.getAuthentication())); @@ -85,6 +86,7 @@ private Urn updateBusinessAttribute(UpdateBusinessAttributeInput input, Urn busi if (Objects.nonNull(input.getType())) { businessAttributeInfo.setType(BusinessAttributeUtils.mapSchemaFieldDataType(input.getType())); } + businessAttributeInfo.setLastModified(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis())); // 3. Write changes to GMS return UrnUtils.getUrn(_entityClient.ingestProposal( AspectUtils.buildMetadataChangeProposal( diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index f10cec261e5d29..4cb247389abbff 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -11375,28 +11375,21 @@ type BusinessAttributeInfo { Input required for creating a BusinessAttribute. """ input CreateBusinessAttributeInput { - """ - Input required for creating a BusinessAttributeInfo - """ - businessAttributeInfo: BusinessAttributeInfoInput! + """ + name of the business attribute + """ + name: String! -} + """ + description of business attribute + """ + description: String -input BusinessAttributeInfoInput { - """ - name of the business attribute - """ - name: String! + """ + Platform independent field type of the field + """ + type: SchemaFieldDataType - """ - description of business attribute - """ - description: String - - """ - Platform independent field type of the field - """ - type: SchemaFieldDataType } """ From 41b6731586bc11620bf2f7d0a80f0ae4b8a05c46 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Thu, 11 Jan 2024 16:04:44 +0530 Subject: [PATCH 07/50] business-attribute: junits for business attribute resolvers --- .../AddBusinessAttributeResolverTest.java | 180 +++++++++++++++ ...reateBusinessAttributeProposalMatcher.java | 39 ++++ .../CreateBusinessAttributeResolverTest.java | 197 +++++++++++++++++ .../DeleteBusinessAttributeResolverTest.java | 90 ++++++++ .../RemoveBusinessAttributeResolverTest.java | 169 ++++++++++++++ .../UpdateBusinessAttributeResolverTest.java | 209 ++++++++++++++++++ .../UpdateNameResolverTest.java | 140 ++++++++++++ .../test/resources/test-entity-registry.yaml | 8 + 8 files changed, 1032 insertions(+) create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java new file mode 100644 index 00000000000000..7064d12bca322d --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java @@ -0,0 +1,180 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authentication.Authentication; +import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; +import com.linkedin.datahub.graphql.generated.ResourceRefInput; +import com.linkedin.datahub.graphql.generated.SubResourceType; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.EntityUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.schema.EditableSchemaFieldInfo; +import com.linkedin.schema.EditableSchemaFieldInfoArray; +import com.linkedin.schema.EditableSchemaMetadata; +import com.linkedin.schema.SchemaField; +import com.linkedin.schema.SchemaFieldArray; +import com.linkedin.schema.SchemaMetadata; +import graphql.schema.DataFetchingEnvironment; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.util.concurrent.ExecutionException; + +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class AddBusinessAttributeResolverTest { + private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final String RESOURCE_URN = "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; + private static final String SUB_RESOURCE = "name"; + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + } + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + + AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); + addBusinessAttributeResolver.get(mockEnv).get(); + + Mockito.verify(mockClient, Mockito.times(1)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + + } + + @Test + public void testBusinessAttributeAlreadyAdded() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + Mockito.when(EntityUtils.getAspectFromEntity( + RESOURCE_URN, + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + mockService, null) + ).thenReturn(editableSchemaMetadata()); + + + AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); + ExecutionException exception = expectThrows(ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); + assertTrue(exception.getCause().getMessage().equals( + String.format("Failed to add Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN + ))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + } + + @Test + public void testBusinessAttributeNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(false); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); + RuntimeException exception = expectThrows(RuntimeException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); + assertTrue(exception.getMessage().equals( + String.format("This urn does not exist: %s", BUSINESS_ATTRIBUTE_URN))); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + } + + @Test + public void testResourceNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(false); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); + ExecutionException exception = expectThrows(ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); + assertTrue(exception.getCause().getMessage().equals( + String.format("Failed to add Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN + ))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + } + + @Test + public void testNotAuthorized() throws Exception { + + } + public AddBusinessAttributeInput addBusinessAttributeInput() { + AddBusinessAttributeInput addBusinessAttributeInput = new AddBusinessAttributeInput(); + addBusinessAttributeInput.setBusinessAttributeUrn(BUSINESS_ATTRIBUTE_URN); + addBusinessAttributeInput.setResourceUrn(resourceRefInput()); + return addBusinessAttributeInput; + } + + private ResourceRefInput resourceRefInput() { + ResourceRefInput resourceRefInput = new ResourceRefInput(); + resourceRefInput.setResourceUrn(RESOURCE_URN); + resourceRefInput.setSubResource(SUB_RESOURCE); + resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); + return resourceRefInput; + } + + private SchemaMetadata schemaMetadata() { + SchemaMetadata schemaMetadata = new SchemaMetadata(); + SchemaFieldArray schemaFields = new SchemaFieldArray(); + SchemaField schemaField = new SchemaField(); + schemaField.setFieldPath(SUB_RESOURCE); + schemaFields.add(schemaField); + schemaMetadata.setFields(schemaFields); + return schemaMetadata; + } + + private EditableSchemaMetadata editableSchemaMetadata() { + EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); + EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); + EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); + editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); + editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); + editableSchemaFieldInfos.add(editableSchemaFieldInfo); + editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); + return editableSchemaMetadata; + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java new file mode 100644 index 00000000000000..c59afc0d6134b9 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java @@ -0,0 +1,39 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.GenericAspect; +import com.linkedin.mxe.MetadataChangeProposal; +import org.mockito.ArgumentMatcher; + +public class CreateBusinessAttributeProposalMatcher implements ArgumentMatcher { + private MetadataChangeProposal left; + public CreateBusinessAttributeProposalMatcher(MetadataChangeProposal left) { + this.left = left; + } + + @Override + public boolean matches(MetadataChangeProposal right) { + return left.getEntityType().equals(right.getEntityType()) + && left.getAspectName().equals(right.getAspectName()) + && left.getChangeType().equals(right.getChangeType()) + && businessAttributeInfoMatch(left.getAspect(), right.getAspect()); + } + + private boolean businessAttributeInfoMatch(GenericAspect left, GenericAspect right) { + BusinessAttributeInfo leftProps = GenericRecordUtils.deserializeAspect( + left.getValue(), + "application/json", + BusinessAttributeInfo.class + ); + + BusinessAttributeInfo rightProps = GenericRecordUtils.deserializeAspect( + right.getValue(), + "application/json", + BusinessAttributeInfo.class + ); + + return leftProps.getName().equals(rightProps.getName()) + && leftProps.getDescription().equals(rightProps.getDescription()); + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java new file mode 100644 index 00000000000000..b6cad4c57b2866 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java @@ -0,0 +1,197 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authentication.Authentication; +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.businessattribute.BusinessAttributeKey; +import com.linkedin.common.urn.Urn; +import com.linkedin.data.template.SetMode; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.generated.CreateBusinessAttributeInput; +import com.linkedin.datahub.graphql.generated.SchemaFieldDataType; +import com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils; +import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchResult; +import com.linkedin.metadata.service.BusinessAttributeService; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.schema.BooleanType; +import graphql.schema.DataFetchingEnvironment; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.util.concurrent.ExecutionException; + +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class CreateBusinessAttributeResolverTest { + + private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; + private static final CreateBusinessAttributeInput TEST_INPUT = new CreateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, + SchemaFieldDataType.BOOLEAN + ); + private static final CreateBusinessAttributeInput TEST_INPUT_NULL_NAME = new CreateBusinessAttributeInput( + null, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, + SchemaFieldDataType.BOOLEAN + ); + private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private BusinessAttributeService businessAttributeService; + private Authentication mockAuthentication; + private SearchResult searchResult; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + businessAttributeService = Mockito.mock(BusinessAttributeService.class); + mockAuthentication = Mockito.mock(Authentication.class); + searchResult = Mockito.mock(SearchResult.class); + } + + @Test + public void testSuccess() throws Exception { + //Mock + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))).thenReturn(false); + Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), + Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) + )).thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(0); + Mockito.when(mockClient.ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication))).thenReturn(BUSINESS_ATTRIBUTE_URN); + Mockito.when( + businessAttributeService.getBusinessAttributeEntityResponse(Mockito.any(Urn.class), Mockito.eq(mockAuthentication)) + ).thenReturn(getBusinessAttributeEntityResponse()); + + //Execute + CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + resolver.get(mockEnv).get(); + + //verify + Mockito.verify(mockClient, Mockito.times(1)).ingestProposal( + Mockito.argThat(new CreateBusinessAttributeProposalMatcher(metadataChangeProposal())), + Mockito.any(Authentication.class) + ); + + } + + @Test + public void testNameIsNull() throws Exception { + //Mock + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_NULL_NAME); + Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))).thenReturn(false); + + //Execute + CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + ExecutionException actualException = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + //verify + assertTrue(actualException.getCause().getMessage().equals("Failed to create Business Attribute with name: null")); + + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) + ); + } + + @Test + public void testNameAlreadyExists() throws Exception { + //Mock + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))).thenReturn(false); + Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), + Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) + )).thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(1); + + //Execute + CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + //Verify + assertTrue(exception.getCause().getMessage().equals("\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) + ); + } + @Test + public void testUnauthorized() throws Exception { + init(); + setupDenyContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + + CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + AuthorizationException exception = expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv)); + + assertTrue(exception.getMessage().equals("Unauthorized to perform this action. Please contact your DataHub administrator.")); + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) + ); + } + private EntityResponse getBusinessAttributeEntityResponse() throws Exception { + EnvelopedAspectMap map = new EnvelopedAspectMap(); + BusinessAttributeInfo businessAttributeInfo = businessAttributeInfo(); + map.put(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(businessAttributeInfo.data()))); + EntityResponse entityResponse = new EntityResponse(); + entityResponse.setAspects(map); + entityResponse.setUrn(Urn.createFromString(BUSINESS_ATTRIBUTE_URN)); + return entityResponse; + } + + private MetadataChangeProposal metadataChangeProposal() { + BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); + BusinessAttributeInfo info = new BusinessAttributeInfo(); + info.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + info.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + info.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(SchemaFieldDataType.BOOLEAN), SetMode.IGNORE_NULL); + return MutationUtils.buildMetadataChangeProposalWithKey(businessAttributeKey, BUSINESS_ATTRIBUTE_ENTITY_NAME, + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, info); + } + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private void setupDenyContext() { + mockContext = getMockDenyContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + private BusinessAttributeInfo businessAttributeInfo() { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); + businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = new com.linkedin.schema.SchemaFieldDataType(); + schemaFieldDataType.setType(com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); + businessAttributeInfo.setType(schemaFieldDataType); + return businessAttributeInfo; + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java new file mode 100644 index 00000000000000..a76250031f4298 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java @@ -0,0 +1,90 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authentication.Authentication; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.entity.client.EntityClient; +import graphql.schema.DataFetchingEnvironment; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class DeleteBusinessAttributeResolverTest { + private static final String TEST_BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private EntityClient mockClient; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + } + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private void setupDenyContext() { + mockContext = getMockDenyContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), mockAuthentication)).thenReturn(true); + + DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); + resolver.get(mockEnv).get(); + + Mockito.verify(mockClient, Mockito.times(1)).deleteEntity( + Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), + Mockito.any(Authentication.class) + ); + } + + @Test + public void testUnauthorized() throws Exception { + init(); + setupDenyContext(); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + + DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); + AuthorizationException actualException = expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv).get()); + assertTrue(actualException.getMessage().equals("Unauthorized to perform this action. Please contact your DataHub administrator.")); + + Mockito.verify(mockClient, Mockito.times(0)).deleteEntity( + Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), + Mockito.any(Authentication.class) + ); + } + + @Test + public void testEntityNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), mockAuthentication)).thenReturn(false); + + DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); + RuntimeException actualException = expectThrows(RuntimeException.class, () -> resolver.get(mockEnv).get()); + assertTrue(actualException.getMessage() + .equals(String.format("This urn does not exist: %s", TEST_BUSINESS_ATTRIBUTE_URN) + )); + + Mockito.verify(mockClient, Mockito.times(0)).deleteEntity( + Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), + Mockito.any(Authentication.class) + ); + + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java new file mode 100644 index 00000000000000..d6f664169c90a5 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java @@ -0,0 +1,169 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authentication.Authentication; +import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; +import com.linkedin.datahub.graphql.generated.ResourceRefInput; +import com.linkedin.datahub.graphql.generated.SubResourceType; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.EntityUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.schema.EditableSchemaFieldInfo; +import com.linkedin.schema.EditableSchemaFieldInfoArray; +import com.linkedin.schema.EditableSchemaMetadata; +import com.linkedin.schema.SchemaField; +import com.linkedin.schema.SchemaFieldArray; +import com.linkedin.schema.SchemaMetadata; +import graphql.schema.DataFetchingEnvironment; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.util.concurrent.ExecutionException; + +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class RemoveBusinessAttributeResolverTest { + private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final String RESOURCE_URN = "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; + private static final String SUB_RESOURCE = "name"; + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + } + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + Mockito.when(EntityUtils.getAspectFromEntity( + RESOURCE_URN, + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + mockService, null) + ).thenReturn(editableSchemaMetadata()); + + RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); + resolver.get(mockEnv).get(); + + Mockito.verify(mockClient, Mockito.times(1)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + } + + @Test + public void testBusinessAttributeNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(false); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); + RuntimeException exception = expectThrows(RuntimeException.class, () -> resolver.get(mockEnv).get()); + assertTrue(exception.getMessage().equals( + String.format("This urn does not exist: %s", BUSINESS_ATTRIBUTE_URN))); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + } + + @Test + public void testBusinessAttributeNotAdded() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); + ExecutionException actualException = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + assertTrue(actualException.getCause().getMessage().equals(String.format("Failed to remove Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + + } + + @Test + public void testResourceNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(false); + Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); + ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + assertTrue(exception.getCause().getMessage().equals( + String.format("Failed to remove Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN + ))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), + Mockito.eq(mockAuthentication)); + } + public AddBusinessAttributeInput addBusinessAttributeInput() { + AddBusinessAttributeInput addBusinessAttributeInput = new AddBusinessAttributeInput(); + addBusinessAttributeInput.setBusinessAttributeUrn(BUSINESS_ATTRIBUTE_URN); + addBusinessAttributeInput.setResourceUrn(resourceRefInput()); + return addBusinessAttributeInput; + } + private ResourceRefInput resourceRefInput() { + ResourceRefInput resourceRefInput = new ResourceRefInput(); + resourceRefInput.setResourceUrn(RESOURCE_URN); + resourceRefInput.setSubResource(SUB_RESOURCE); + resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); + return resourceRefInput; + } + + private SchemaMetadata schemaMetadata() { + SchemaMetadata schemaMetadata = new SchemaMetadata(); + SchemaFieldArray schemaFields = new SchemaFieldArray(); + SchemaField schemaField = new SchemaField(); + schemaField.setFieldPath(SUB_RESOURCE); + schemaFields.add(schemaField); + schemaMetadata.setFields(schemaFields); + return schemaMetadata; + } + + private EditableSchemaMetadata editableSchemaMetadata() { + EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); + EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); + EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); + editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); + editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); + editableSchemaFieldInfos.add(editableSchemaFieldInfo); + editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); + return editableSchemaMetadata; + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java new file mode 100644 index 00000000000000..ccff7b1c9f630c --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java @@ -0,0 +1,209 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authentication.Authentication; +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.template.SetMode; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.generated.SchemaFieldDataType; +import com.linkedin.datahub.graphql.generated.UpdateBusinessAttributeInput; +import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.entity.AspectUtils; +import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchResult; +import com.linkedin.metadata.service.BusinessAttributeService; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.schema.BooleanType; +import graphql.schema.DataFetchingEnvironment; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class UpdateBusinessAttributeResolverTest { + private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; + private static final String TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED = "test-business-attribute-updated"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED = "test-description-updated"; + private static final String TEST_BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final Urn TEST_BUSINESS_ATTRIBUTE_URN_OBJ = UrnUtils.getUrn(TEST_BUSINESS_ATTRIBUTE_URN); + private EntityClient mockClient; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private BusinessAttributeService businessAttributeService; + private Authentication mockAuthentication; + private SearchResult searchResult; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + businessAttributeService = Mockito.mock(BusinessAttributeService.class); + mockAuthentication = Mockito.mock(Authentication.class); + searchResult = Mockito.mock(SearchResult.class); + } + + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER + ); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)).thenReturn(true); + Mockito.when(businessAttributeService.getBusinessAttributeEntityResponse(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) + .thenReturn(getBusinessAttributeEntityResponse()); + Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), + Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) + )).thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(0); + Mockito.when(mockClient.ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication))) + .thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + + UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + resolver.get(mockEnv).get(); + + //verify + Mockito.verify(mockClient, Mockito.times(1)).ingestProposal( + Mockito.argThat(new CreateBusinessAttributeProposalMatcher(updatedMetadataChangeProposal())), + Mockito.any(Authentication.class) + ); + } + + @Test + public void testNotExists() throws Exception { + init(); + setupAllowContext(); + final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER + ); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)).thenReturn(false); + + UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + RuntimeException expectedException = expectThrows(RuntimeException.class, () -> resolver.get(mockEnv)); + assertTrue(expectedException.getMessage().equals(String.format("This urn does not exist: %s", TEST_BUSINESS_ATTRIBUTE_URN))); + + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) + ); + } + + @Test + public void testNameConflict() throws Exception { + init(); + setupAllowContext(); + final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER + ); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)).thenReturn(true); + Mockito.when(businessAttributeService.getBusinessAttributeEntityResponse(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) + .thenReturn(getBusinessAttributeEntityResponse()); + Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), + Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) + )).thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(1); + + UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + + ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + //Verify + assertTrue(exception.getCause().getMessage().equals("\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) + ); + + } + @Test + public void testNotAuthorized() throws Exception { + init(); + setupDenyContext(); + final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER + ); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + + UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + AuthorizationException exception = expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv)); + + assertTrue(exception.getMessage().equals("Unauthorized to perform this action. Please contact your DataHub administrator.")); + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) + ); + + } + + private EntityResponse getBusinessAttributeEntityResponse() throws Exception { + Map result = new HashMap<>(); + EnvelopedAspectMap map = new EnvelopedAspectMap(); + BusinessAttributeInfo businessAttributeInfo = businessAttributeInfo(); + map.put(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(businessAttributeInfo.data()))); + EntityResponse entityResponse = new EntityResponse(); + entityResponse.setAspects(map); + entityResponse.setUrn(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)); + return entityResponse; + } + + private MetadataChangeProposal updatedMetadataChangeProposal() { + BusinessAttributeInfo info = new BusinessAttributeInfo(); + info.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + info.setName(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + info.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED); + info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(SchemaFieldDataType.BOOLEAN), SetMode.IGNORE_NULL); + return AspectUtils.buildMetadataChangeProposal(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, info); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private void setupDenyContext() { + mockContext = getMockDenyContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private BusinessAttributeInfo businessAttributeInfo() { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); + businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = new com.linkedin.schema.SchemaFieldDataType(); + schemaFieldDataType.setType(com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); + businessAttributeInfo.setType(schemaFieldDataType); + return businessAttributeInfo; + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java new file mode 100644 index 00000000000000..970ef07525ea7d --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java @@ -0,0 +1,140 @@ +package com.linkedin.datahub.graphql.resolvers.businessattribute; + +import com.datahub.authentication.Authentication; +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.UpdateNameInput; +import com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils; +import com.linkedin.datahub.graphql.resolvers.mutate.UpdateNameResolver; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.EntityUtils; +import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchResult; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.schema.BooleanType; +import graphql.schema.DataFetchingEnvironment; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import java.util.concurrent.ExecutionException; + +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + +public class UpdateNameResolverTest { + private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; + private static final String TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED = "test-business-attribute-updated"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; + private static final String TEST_BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final Urn TEST_BUSINESS_ATTRIBUTE_URN_OBJ = UrnUtils.getUrn(TEST_BUSINESS_ATTRIBUTE_URN); + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + private SearchResult searchResult; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + searchResult = Mockito.mock(SearchResult.class); + } + + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + UpdateNameInput testInput = new UpdateNameInput(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockService.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ)).thenReturn(true); + Mockito.when(EntityUtils.getAspectFromEntity( + TEST_BUSINESS_ATTRIBUTE_URN_OBJ.toString(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + mockService, + null + )).thenReturn(businessAttributeInfo()); + + Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), + Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) + )).thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(0); + + BusinessAttributeInfo updatedBusinessAttributeInfo = businessAttributeInfo(); + updatedBusinessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + updatedBusinessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + MetadataChangeProposal proposal = MutationUtils.buildMetadataChangeProposalWithUrn( + TEST_BUSINESS_ATTRIBUTE_URN_OBJ, + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + updatedBusinessAttributeInfo + ); + + UpdateNameResolver resolver = new UpdateNameResolver(mockService, mockClient); + resolver.get(mockEnv).get(); + + //verify + Mockito.verify(mockService, Mockito.times(1)).ingestProposal( + Mockito.argThat(new CreateBusinessAttributeProposalMatcher(proposal)), + Mockito.any(AuditStamp.class), + Mockito.eq(false) + ); + } + + @Test + public void testNameConflict() throws Exception { + init(); + setupAllowContext(); + UpdateNameInput testInput = new UpdateNameInput(TEST_BUSINESS_ATTRIBUTE_NAME, TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockService.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ)).thenReturn(true); + Mockito.when(EntityUtils.getAspectFromEntity( + TEST_BUSINESS_ATTRIBUTE_URN_OBJ.toString(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + mockService, + null + )).thenReturn(businessAttributeInfo()); + + Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), + Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) + )).thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(1); + + UpdateNameResolver resolver = new UpdateNameResolver(mockService, mockClient); + ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + assertTrue(exception.getCause().getMessage().equals("\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) + ); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private BusinessAttributeInfo businessAttributeInfo() { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); + businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = new com.linkedin.schema.SchemaFieldDataType(); + schemaFieldDataType.setType(com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); + businessAttributeInfo.setType(schemaFieldDataType); + return businessAttributeInfo; + } + + +} diff --git a/datahub-graphql-core/src/test/resources/test-entity-registry.yaml b/datahub-graphql-core/src/test/resources/test-entity-registry.yaml index efd75a7fb07f51..20142cfbb799c7 100644 --- a/datahub-graphql-core/src/test/resources/test-entity-registry.yaml +++ b/datahub-graphql-core/src/test/resources/test-entity-registry.yaml @@ -293,6 +293,14 @@ entities: aspects: - ownershipTypeInfo - status +- name: businessAttribute + category: core + keyAspect: businessAttributeKey + aspects: + - businessAttributeInfo + - status + - ownership + - institutionalMemory - name: dataContract category: core keyAspect: dataContractKey From 730bb5ae31f79003eb205222fc60077252696702 Mon Sep 17 00:00:00 2001 From: aditigup Date: Tue, 16 Jan 2024 16:34:14 +0530 Subject: [PATCH 08/50] Business Attribute Association --- .../datahub/graphql/GmsGraphQLEngine.java | 10 + .../graphql/resolvers/search/SearchUtils.java | 9 +- .../mappers/BusinessAttributesMapper.java | 41 ++ .../EditableSchemaFieldInfoMapper.java | 9 + .../src/main/resources/entity.graphql | 73 +++- .../businessAttribute/AttributeBrowser.tsx | 63 +++ .../app/businessAttribute/AttributeItem.tsx | 61 +++ .../CreateBusinessAttributeModal.tsx | 4 +- .../businessAttributeUtils.ts | 14 + .../BusinessAttributeEntity.tsx | 8 +- .../businessAttribute/preview/Preview.tsx | 14 +- .../BusinessAttributeRelatedEntity.tsx | 44 +++ .../src/app/entity/shared/constants.ts | 4 + .../tabs/Dataset/Schema/SchemaTable.tsx | 38 +- .../utils/useBusinessAttributeRenderer.tsx | 45 +++ .../Schema/utils/useTagsAndTermsRenderer.tsx | 45 ++- .../AddBusinessAttributeModal.tsx | 374 ++++++++++++++++++ .../businessAttribute/AttributeContent.tsx | 119 ++++++ .../BusinessAttributeGroup.tsx | 104 +++++ .../businessAttribute/StyledAttribute.tsx | 58 +++ datahub-web-react/src/graphql/dataset.graphql | 5 + .../src/graphql/fragments.graphql | 46 +++ .../src/graphql/mutations.graphql | 8 + 23 files changed, 1164 insertions(+), 32 deletions(-) create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java create mode 100644 datahub-web-react/src/app/businessAttribute/AttributeBrowser.tsx create mode 100644 datahub-web-react/src/app/businessAttribute/AttributeItem.tsx create mode 100644 datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts create mode 100644 datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx create mode 100644 datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx create mode 100644 datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx create mode 100644 datahub-web-react/src/app/shared/businessAttribute/BusinessAttributeGroup.tsx create mode 100644 datahub-web-react/src/app/shared/businessAttribute/StyledAttribute.tsx diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index b0d96d400ad401..b60fc9f1b49849 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -96,6 +96,7 @@ import com.linkedin.datahub.graphql.generated.TestResult; import com.linkedin.datahub.graphql.generated.UserUsageCounts; import com.linkedin.datahub.graphql.generated.BusinessAttribute; +import com.linkedin.datahub.graphql.generated.BusinessAttributeAssociation; import com.linkedin.datahub.graphql.resolvers.MeResolver; import com.linkedin.datahub.graphql.resolvers.assertion.AssertionRunEventResolver; import com.linkedin.datahub.graphql.resolvers.assertion.DeleteAssertionResolver; @@ -699,6 +700,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureOwnershipTypeResolver(builder); configurePluginResolvers(builder); configureBusinessAttributeResolver(builder); + configureBusinessAttributeAssociationResolver(builder); } private void configureOrganisationRoleResolvers(RuntimeWiring.Builder builder) { @@ -1163,6 +1165,7 @@ private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder .dataFetcher("ownershipType", new EntityTypeResolver(entityTypes, (env) -> ((Owner) env.getSource()).getOwnershipType())) ); + // TODO add business attribute list resolver } /** @@ -1901,4 +1904,11 @@ private void configureBusinessAttributeResolver(final RuntimeWiring.Builder buil .collect(Collectors.toList()))) ); } + private void configureBusinessAttributeAssociationResolver(final RuntimeWiring.Builder builder) { + builder.type("BusinessAttributeAssociation", typeWiring -> typeWiring + .dataFetcher("businessAttribute", + new LoadableTypeResolver<>(businessAttributeType, + (env) -> ((BusinessAttributeAssociation) env.getSource()).getBusinessAttribute().getUrn())) + ); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java index 0533a515128222..c8e5bcadebd102 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java @@ -74,9 +74,9 @@ private SearchUtils() { EntityType.CONTAINER, EntityType.DOMAIN, EntityType.DATA_PRODUCT, - EntityType.NOTEBOOK); + EntityType.NOTEBOOK, + EntityType.BUSINESS_ATTRIBUTE); - //TODO: add business attributes to the list of searchable fields /** @@ -99,7 +99,8 @@ private SearchUtils() { EntityType.CORP_GROUP, EntityType.ROLE, EntityType.NOTEBOOK, - EntityType.DATA_PRODUCT); + EntityType.DATA_PRODUCT, + EntityType.BUSINESS_ATTRIBUTE); /** * A prioritized list of source filter types used to generate quick filters @@ -390,4 +391,4 @@ public static List getEntityNames(List inputTypes) { (inputTypes == null || inputTypes.isEmpty()) ? SEARCHABLE_ENTITY_TYPES : inputTypes; return entityTypes.stream().map(EntityTypeMapper::getName).collect(Collectors.toList()); } -} \ No newline at end of file +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java new file mode 100644 index 00000000000000..00e850517212da --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java @@ -0,0 +1,41 @@ +package com.linkedin.datahub.graphql.types.businessattribute.mappers; + +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; +import com.linkedin.datahub.graphql.generated.BusinessAttributeAssociation; +import com.linkedin.datahub.graphql.generated.BusinessAttributes; +import com.linkedin.datahub.graphql.generated.EntityType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; + +public class BusinessAttributesMapper { + + private static final Logger _logger = LoggerFactory.getLogger(BusinessAttributesMapper.class.getName()); + public static final BusinessAttributesMapper INSTANCE = new BusinessAttributesMapper(); + + public static BusinessAttributes map( + @Nonnull final com.linkedin.businessattribute.BusinessAttributeAssociation businessAttribute, + @Nonnull final Urn entityUrn + ) { + _logger.info("inside mapper"); + return INSTANCE.apply(businessAttribute, entityUrn); + } + + private BusinessAttributes apply(@Nonnull com.linkedin.businessattribute.BusinessAttributeAssociation businessAttributes, @Nonnull Urn entityUrn) { + _logger.info("before try block::{}", businessAttributes.getDestinationUrn()); + final BusinessAttributeAssociation businessAttributeAssociation = new BusinessAttributeAssociation(); + final BusinessAttributes result = new BusinessAttributes(); + final BusinessAttribute businessAttribute = new BusinessAttribute(); + businessAttribute.setUrn(businessAttributes.getDestinationUrn().toString()); + businessAttribute.setType(EntityType.BUSINESS_ATTRIBUTE); + + businessAttributeAssociation.setBusinessAttribute(businessAttribute); + + businessAttributeAssociation.setAssociatedUrn(entityUrn.toString()); + result.setBusinessAttribute(businessAttributeAssociation); + return result; + } + +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java index 922574d5051d30..3ad74bacd1e827 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java @@ -1,14 +1,18 @@ package com.linkedin.datahub.graphql.types.dataset.mappers; import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributesMapper; import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper; import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper; import com.linkedin.schema.EditableSchemaFieldInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; public class EditableSchemaFieldInfoMapper { + private static final Logger _logger = LoggerFactory.getLogger(EditableSchemaFieldInfoMapper.class.getName()); public static final EditableSchemaFieldInfoMapper INSTANCE = new EditableSchemaFieldInfoMapper(); @@ -37,6 +41,11 @@ public com.linkedin.datahub.graphql.generated.EditableSchemaFieldInfo apply( if (input.hasGlossaryTerms()) { result.setGlossaryTerms(GlossaryTermsMapper.map(input.getGlossaryTerms(), entityUrn)); } + _logger.info("inside info mapper before"); + if (input.hasBusinessAttribute()) { + _logger.info("inside info mapper after: {}, entity urn: {}", input.getBusinessAttribute().getDestinationUrn(), entityUrn); + result.setBusinessAttributes(BusinessAttributesMapper.map(input.getBusinessAttribute(), entityUrn)); + } return result; } } diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 4cb247389abbff..a1bac2433b4393 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -1539,7 +1539,7 @@ type RoleUser { type RoleProperties { """ - Name of the Role in an organisation + Name of the Role in an organisation """ name: String! @@ -2980,6 +2980,11 @@ type EditableSchemaFieldInfo { Glossary terms associated with the field """ glossaryTerms: GlossaryTerms + + """ + Business Attribute associated with the field + """ + businessAttributes: BusinessAttributes } """ @@ -11375,23 +11380,40 @@ type BusinessAttributeInfo { Input required for creating a BusinessAttribute. """ input CreateBusinessAttributeInput { - """ - name of the business attribute - """ - name: String! + """ + name of the business attribute + """ + name: String! - """ - description of business attribute - """ - description: String + """ + description of business attribute + """ + description: String - """ - Platform independent field type of the field - """ - type: SchemaFieldDataType + """ + Platform independent field type of the field + """ + type: SchemaFieldDataType } +input BusinessAttributeInfoInput { + """ + name of the business attribute + """ + name: String! + + """ + description of business attribute + """ + description: String + + """ + Platform independent field type of the field + """ + type: SchemaFieldDataType +} + """ Input required to update Business Attribute """ @@ -11428,6 +11450,31 @@ input AddBusinessAttributeInput { resourceUrn: ResourceRefInput! } +""" +Business attributes attached to the metadata +""" +type BusinessAttributes { + """ + Business Attribute attached to the Metadata Entity + """ + businessAttribute: BusinessAttributeAssociation! +} + +""" +Input required to attach business attribute to an entity +""" +type BusinessAttributeAssociation { + """ + Business Attribute itself + """ + businessAttribute: BusinessAttribute! + + """ + Reference back to the associated urn for tracking purposes e.g. when sibling nodes are merged together + """ + associatedUrn: String! +} + """ Input provided when listing Business Attribute """ diff --git a/datahub-web-react/src/app/businessAttribute/AttributeBrowser.tsx b/datahub-web-react/src/app/businessAttribute/AttributeBrowser.tsx new file mode 100644 index 00000000000000..4d8f722aec9883 --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/AttributeBrowser.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import styled from 'styled-components/macro'; +import { useEntityRegistry } from '../useEntityRegistry'; +import { ListBusinessAttributesQuery, useListBusinessAttributesQuery } from '../../graphql/businessAttribute.generated'; +import { sortBusinessAttributes } from './businessAttributeUtils'; +import AttributeItem from './AttributeItem'; + +const BrowserWrapper = styled.div` + color: #262626; + font-size: 12px; + max-height: calc(100% - 47px); + padding: 10px 20px 20px 20px; + overflow: auto; +`; + +interface Props { + isSelecting?: boolean; + hideTerms?: boolean; + refreshBrowser?: boolean; + selectAttribute?: (urn: string, displayName: string) => void; + attributeData?: ListBusinessAttributesQuery; +} + +function AttributeBrowser(props: Props) { + const { isSelecting, hideTerms, refreshBrowser, selectAttribute, attributeData } = props; + + const { refetch: refetchAttributes } = useListBusinessAttributesQuery({ + variables: { + start: 0, + count: 10, + query: '*', + }, + }); + + const displayedAttributes = attributeData?.listBusinessAttributes?.businessAttributes || []; + + const entityRegistry = useEntityRegistry(); + const sortedAttributes = displayedAttributes.sort((termA, termB) => + sortBusinessAttributes(entityRegistry, termA, termB), + ); + + useEffect(() => { + if (refreshBrowser) { + refetchAttributes(); + } + }, [refreshBrowser, refetchAttributes]); + + return ( + + {!hideTerms && + sortedAttributes.map((attribute) => ( + + ))} + + ); +} + +export default AttributeBrowser; diff --git a/datahub-web-react/src/app/businessAttribute/AttributeItem.tsx b/datahub-web-react/src/app/businessAttribute/AttributeItem.tsx new file mode 100644 index 00000000000000..051979d696f493 --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/AttributeItem.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import styled from 'styled-components/macro'; +import { ANTD_GRAY } from '../entity/shared/constants'; +import { useEntityRegistry } from '../useEntityRegistry'; + +const AttributeWrapper = styled.div` + font-weight: normal; + margin-bottom: 4px; +`; + +const nameStyles = ` + color: #262626; + display: inline-block; + height: 100%; + padding: 3px 4px; + width: 100%; +`; + +export const NameWrapper = styled.span<{ showSelectStyles?: boolean }>` + ${nameStyles} + + &:hover { + ${(props) => + props.showSelectStyles && + ` + background-color: ${ANTD_GRAY[3]}; + cursor: pointer; + `} + } +`; + +interface Props { + attribute: any; + isSelecting?: boolean; + selectAttribute?: (urn: string, displayName: string) => void; +} + +function AttributeItem(props: Props) { + const { attribute, isSelecting, selectAttribute } = props; + + const entityRegistry = useEntityRegistry(); + + function handleSelectAttribute() { + if (selectAttribute) { + const displayName = entityRegistry.getDisplayName(attribute.type, attribute); + selectAttribute(attribute.urn, displayName); + } + } + + return ( + + {isSelecting && ( + + {entityRegistry.getDisplayName(attribute.type, attribute)} + + )} + + ); +} + +export default AttributeItem; diff --git a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx index 975d707831e736..c23fca800cb6a0 100644 --- a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx +++ b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx @@ -72,7 +72,9 @@ export default function CreateBusinessAttributeModal({ visible, onClose, onCreat const { name, dataType } = form.getFieldsValue(); const sanitizedDescription = DOMPurify.sanitize(documentation); const input: CreateBusinessAttributeInput = { - businessAttributeInfo: { name, description: sanitizedDescription, type: dataType }, + name, + description: sanitizedDescription, + type: dataType, }; createBusinessAttribute({ variables: { input } }) .then(() => { diff --git a/datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts b/datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts new file mode 100644 index 00000000000000..938cb34a86d2da --- /dev/null +++ b/datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts @@ -0,0 +1,14 @@ +import EntityRegistry from '../entity/EntityRegistry'; +import { Entity, EntityType } from '../../types.generated'; + +export function sortBusinessAttributes(entityRegistry: EntityRegistry, nodeA?: Entity | null, nodeB?: Entity | null) { + const nodeAName = entityRegistry.getDisplayName(EntityType.BusinessAttribute, nodeA) || ''; + const nodeBName = entityRegistry.getDisplayName(EntityType.BusinessAttribute, nodeB) || ''; + return nodeAName.localeCompare(nodeBName); +} + +export function getRelatedEntitiesUrl(entityRegistry: EntityRegistry, urn: string) { + return `${entityRegistry.getEntityUrl(EntityType.BusinessAttribute, urn)}/${encodeURIComponent( + 'Related Entities', + )}`; +} diff --git a/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx index c75393ff013cc8..cf6f3718f42b26 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx @@ -7,13 +7,13 @@ import { EntityProfile } from '../shared/containers/profile/EntityProfile'; import { useGetBusinessAttributeQuery } from '../../../graphql/businessAttribute.generated'; import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab'; -// import GlossaryRelatedEntity from './profile/GlossaryRelatedEntity'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; import { SidebarAboutSection } from '../shared/containers/profile/sidebar/AboutSection/SidebarAboutSection'; import { SidebarOwnerSection } from '../shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection'; import { SidebarTagsSection } from '../shared/containers/profile/sidebar/SidebarTagsSection'; import { Preview } from './preview/Preview'; import { PageRoutes } from '../../../conf/Global'; +import BusinessAttributeRelatedEntity from './profile/BusinessAttributeRelatedEntity'; /** * Definition of datahub Business Attribute Entity @@ -49,7 +49,6 @@ export class BusinessAttributeEntity implements Entity { }; displayName = (data: BusinessAttribute) => { - console.log('displayName:::', data?.properties); return data?.properties?.name || data?.urn; }; @@ -81,9 +80,10 @@ export class BusinessAttributeEntity implements Entity { }); }; - renderPreview = (_: PreviewType, data: BusinessAttribute) => { + renderPreview = (previewType: PreviewType, data: BusinessAttribute) => { return ( { }, { name: 'Related Entities', - component: PropertiesTab, + component: BusinessAttributeRelatedEntity, }, { name: 'Properties', diff --git a/datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx b/datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx index d402fef600d525..323c287a0acd78 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/preview/Preview.tsx @@ -1,32 +1,40 @@ import React from 'react'; -import { BookOutlined } from '@ant-design/icons'; +import { GlobalOutlined } from '@ant-design/icons'; import { EntityType, Owner } from '../../../../types.generated'; import DefaultPreviewCard from '../../../preview/DefaultPreviewCard'; import { useEntityRegistry } from '../../../useEntityRegistry'; -import { IconStyleType } from '../../Entity'; +import { IconStyleType, PreviewType } from '../../Entity'; +import UrlButton from '../../shared/UrlButton'; +import { getRelatedEntitiesUrl } from '../../../businessAttribute/businessAttributeUtils'; export const Preview = ({ urn, name, description, owners, + previewType, }: { urn: string; name: string; description?: string | null; owners?: Array | null; + previewType: PreviewType; }): JSX.Element => { const entityRegistry = useEntityRegistry(); return ( } + logoComponent={} type="Business Attribute" typeIcon={entityRegistry.getIcon(EntityType.BusinessAttribute, 14, IconStyleType.ACCENT)} + entityTitleSuffix={ + View Related Entities + } /> ); }; diff --git a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx new file mode 100644 index 00000000000000..fc8208becc56b6 --- /dev/null +++ b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { UnionType } from '../../../search/utils/constants'; +import { EmbeddedListSearchSection } from '../../shared/components/styled/search/EmbeddedListSearchSection'; + +import { useEntityData } from '../../shared/EntityContext'; + +export default function BusinessAttributeRelatedEntity() { + const { entityData } = useEntityData(); + + const entityUrn = entityData?.urn; + + const fixedOrFilters = + (entityUrn && [ + { + field: 'businessAttributes', + values: [entityUrn], + }, + ]) || + []; + + entityData?.isAChildren?.relationships.forEach((businessAttribute) => { + const childUrn = businessAttribute.entity?.urn; + + if (childUrn) { + fixedOrFilters.push({ + field: 'businessAttributes', + values: [childUrn], + }); + } + }); + + return ( + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/constants.ts b/datahub-web-react/src/app/entity/shared/constants.ts index 9df5923d185423..edf71aa608a16a 100644 --- a/datahub-web-react/src/app/entity/shared/constants.ts +++ b/datahub-web-react/src/app/entity/shared/constants.ts @@ -78,6 +78,10 @@ export const EMPTY_MESSAGES = { title: 'Is not inherited by any terms', description: 'Terms can be inherited by other terms to represent an "Is A" style relationship.', }, + businessAttributes: { + title: 'No business attributes added yet', + description: 'Add business attributes to entities to classify their data.', + }, }; export const ELASTIC_MAX_COUNT = 10000; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx index 41b92aea93b5ad..c654cdeb157599 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx @@ -24,6 +24,7 @@ import useSchemaBlameRenderer from './utils/useSchemaBlameRenderer'; import { ANTD_GRAY } from '../../../constants'; import MenuColumn from './components/MenuColumn'; import translateFieldPath from '../../../../dataset/profile/schema/utils/translateFieldPath'; +import useBusinessAttributeRenderer from './utils/useBusinessAttributeRenderer'; const TableContainer = styled.div` overflow: inherit; @@ -72,6 +73,7 @@ export default function SchemaTable({ const hasUsageStats = useMemo(() => (usageStats?.aggregations?.fields?.length || 0) > 0, [usageStats]); const [tableHeight, setTableHeight] = useState(0); const [tagHoveredIndex, setTagHoveredIndex] = useState(undefined); + const [attributeHoveredIndex, setAttributeHoveredIndex] = useState(undefined); const [selectedFkFieldPath, setSelectedFkFieldPath] = useState(null); @@ -97,6 +99,12 @@ export default function SchemaTable({ }, filterText, ); + const businessAttributeRenderer = useBusinessAttributeRenderer( + editableSchemaMetadata, + attributeHoveredIndex, + setAttributeHoveredIndex, + filterText, + ); const schemaTitleRenderer = useSchemaTitleRenderer(schemaMetadata, setSelectedFkFieldPath, filterText); const schemaBlameRenderer = useSchemaBlameRenderer(schemaFieldBlameList); @@ -113,6 +121,19 @@ export default function SchemaTable({ }, }); + const onAttributeCell = (record: SchemaField) => ({ + onMouseEnter: () => { + if (editMode) { + setAttributeHoveredIndex(record.fieldPath); + } + }, + onMouseLeave: () => { + if (editMode) { + setAttributeHoveredIndex(undefined); + } + }, + }); + const fieldColumn = { width: '22%', title: 'Field', @@ -151,6 +172,15 @@ export default function SchemaTable({ onCell: onTagTermCell, }; + const businessAttributeColumn = { + width: '13%', + title: 'Business Attributes', + dataIndex: 'businessAttribute', + key: 'businessAttribute', + render: businessAttributeRenderer, + onCell: onAttributeCell, + }; + const blameColumn = { width: '10%', dataIndex: 'fieldPath', @@ -192,7 +222,13 @@ export default function SchemaTable({ render: (field: SchemaField) => , }; - let allColumns: ColumnsType = [fieldColumn, descriptionColumn, tagColumn, termColumn]; + let allColumns: ColumnsType = [ + fieldColumn, + descriptionColumn, + tagColumn, + termColumn, + businessAttributeColumn, + ]; if (hasUsageStats) { allColumns = [...allColumns, usageColumn]; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx new file mode 100644 index 00000000000000..2bc28fe6811833 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { EditableSchemaMetadata, EntityType, SchemaField } from '../../../../../../../types.generated'; +import { pathMatchesNewPath } from '../../../../../dataset/profile/schema/utils/utils'; +import { useMutationUrn, useRefetch } from '../../../../EntityContext'; +import { useSchemaRefetch } from '../SchemaContext'; +import BusinessAttributeGroup from '../../../../../../shared/businessAttribute/BusinessAttributeGroup'; + +export default function useBusinessAttributeRenderer( + editableSchemaMetadata: EditableSchemaMetadata | null | undefined, + attributeHoveredIndex: string | undefined, + setAttributeHoveredIndex: (index: string | undefined) => void, + filterText: string, +) { + const urn = useMutationUrn(); + const refetch = useRefetch(); + const schemaRefetch = useSchemaRefetch(); + + const refresh: any = () => { + refetch?.(); + schemaRefetch?.(); + }; + + return (businessAttribute: string, record: SchemaField): JSX.Element => { + const relevantEditableFieldInfo = editableSchemaMetadata?.editableSchemaFieldInfo.find( + (candidateEditableFieldInfo) => pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, record.fieldPath), + ); + + return ( +
+ setAttributeHoveredIndex(undefined)} + entityUrn={urn} + entityType={EntityType.Dataset} + entitySubresource={record.fieldPath} + highlightText={filterText} + refetch={refresh} + /> +
+ ); + }; +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx index a57344e5733b4e..212b556a2b7e99 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx @@ -26,21 +26,54 @@ export default function useTagsAndTermsRenderer( (candidateEditableFieldInfo) => pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, record.fieldPath), ); + const newRecord = { ...record }; + + if (!newRecord.glossaryTerms) { + newRecord.glossaryTerms = { terms: [] }; + } + + if (!newRecord.glossaryTerms.terms) { + newRecord.glossaryTerms.terms = []; + } + + if ( + relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties + ?.glossaryTerms?.terms + ) { + newRecord.glossaryTerms.terms = [ + ...newRecord.glossaryTerms.terms, + ...relevantEditableFieldInfo.businessAttributes.businessAttribute.businessAttribute.properties + .glossaryTerms.terms, + ]; + } + let newTags = {}; + if ( + relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.tags?.tags + ) { + newTags = { + ...tags, + tags: [ + ...(tags?.tags || []), + ...relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties + ?.tags?.tags, + ], + }; + } return ( -
+
setTagHoveredIndex(undefined)} entityUrn={urn} entityType={EntityType.Dataset} - entitySubresource={record.fieldPath} + entitySubresource={newRecord.fieldPath} highlightText={filterText} refetch={refresh} /> diff --git a/datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx b/datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx new file mode 100644 index 00000000000000..9653d371e37d57 --- /dev/null +++ b/datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx @@ -0,0 +1,374 @@ +import React, { useRef, useState } from 'react'; +import { Button, message, Modal, Select, Tag as CustomTag } from 'antd'; +import styled from 'styled-components'; +import { GlobalOutlined } from '@ant-design/icons'; +import { Entity, EntityType, ResourceRefInput } from '../../../types.generated'; +import { useEntityRegistry } from '../../useEntityRegistry'; +import { handleBatchError } from '../../entity/shared/utils'; +import { + useAddBusinessAttributeMutation, + useRemoveBusinessAttributeMutation, +} from '../../../graphql/mutations.generated'; +import { useGetSearchResultsLazyQuery } from '../../../graphql/search.generated'; +import ClickOutside from '../ClickOutside'; +import { useGetRecommendations } from '../recommendation'; +import { useEnterKeyListener } from '../useEnterKeyListener'; +import { ENTER_KEY_CODE } from '../constants'; +import AttributeBrowser from '../../businessAttribute/AttributeBrowser'; +import { useListBusinessAttributesQuery } from '../../../graphql/businessAttribute.generated'; + +export enum OperationType { + ADD, + REMOVE, +} + +const AttributeSelect = styled(Select)` + width: 480px; +`; + +const AttributeName = styled.span` + margin-left: 5px; +`; + +const StyleTag = styled(CustomTag)` + margin: 2px; + display: flex; + justify-content: start; + align-items: center; + white-space: nowrap; + opacity: 1; + color: #434343; + line-height: 16px; +`; + +export const BrowserWrapper = styled.div<{ isHidden: boolean; width?: string; maxHeight?: number }>` + background-color: white; + border-radius: 5px; + box-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%); + max-height: ${(props) => (props.maxHeight ? props.maxHeight : '380')}px; + overflow: auto; + position: absolute; + transition: opacity 0.2s; + width: ${(props) => (props.width ? props.width : '480px')}; + z-index: 1051; + ${(props) => + props.isHidden && + ` + opacity: 0; + height: 0; + `} +`; + +type EditAttributeModalProps = { + visible: boolean; + onCloseModal: () => void; + resources: ResourceRefInput[]; + type?: EntityType; + operationType?: OperationType; + onOkOverride?: (result: string) => void; +}; + +export default function EditBusinessAttributeModal({ + visible, + type = EntityType.BusinessAttribute, + operationType = OperationType.ADD, + onCloseModal, + onOkOverride, + resources, +}: EditAttributeModalProps) { + const entityRegistry = useEntityRegistry(); + const [inputValue, setInputValue] = useState(''); + const [addBusinessAttributeMutation] = useAddBusinessAttributeMutation(); + const [removeBusinessAttributeMutation] = useRemoveBusinessAttributeMutation(); + const [isFocusedOnInput, setIsFocusedOnInput] = useState(false); + const inputEl = useRef(null); + const [urn, setUrn] = useState(''); + const [disableAction, setDisableAction] = useState(false); + const [recommendedData] = useGetRecommendations([EntityType.BusinessAttribute]); + const [selectedAttribute, setSelectedAttribute] = useState(''); + const [attributeSearch, { data: attributeSearchData }] = useGetSearchResultsLazyQuery(); + const attributeSearchResults = + attributeSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || []; + const { data: attributeData } = useListBusinessAttributesQuery({ + variables: { + start: 0, + count: 10, + query: '', + }, + }); + + const displayedAttributes = + attributeData?.listBusinessAttributes?.businessAttributes?.map((defaultValue) => ({ + urn: defaultValue.urn, + component: ( +
+ + {defaultValue?.properties?.name} +
+ ), + })) || []; + + const handleSearch = (text: string) => { + if (text.length > 0) { + attributeSearch({ + variables: { + input: { + type, + query: text, + start: 0, + count: 10, + }, + }, + }); + } + }; + + const renderSearchResult = (entity: Entity) => { + const displayName = entityRegistry.getDisplayName(entity.type, entity); + return ( + +
+ + {displayName} +
+
+ ); + }; + + const attributeResult = !inputValue || inputValue.length === 0 ? recommendedData : attributeSearchResults; + const attributeSearchOptions = attributeResult?.map((result) => { + return renderSearchResult(result); + }); + + const attributeRender = (props) => { + // eslint-disable-next-line react/prop-types + const { closable, onClose, value } = props; + const onPreventMouseDown = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; + /* eslint-disable-next-line react/prop-types */ + const selectedItem = displayedAttributes.find((attribute) => attribute.urn === value)?.component; + return ( + + {selectedItem} + + ); + }; + + // Handle the Enter press + useEnterKeyListener({ + querySelectorToExecuteClick: '#addAttributeButton', + }); + + // When business attribute search result is selected, add the urn + const onSelectValue = (selectedUrn: string) => { + const selectedSearchOption = attributeSearchOptions?.find((option) => option.props.value === selectedUrn); + setUrn(selectedUrn); + if (!selectedAttribute) { + setSelectedAttribute({ + selectedUrn, + component: ( +
+ + {selectedSearchOption?.props.name} +
+ ), + }); + } + if (inputEl && inputEl.current) { + (inputEl.current as any).blur(); + } + }; + + // When a Tag or term search result is deselected, remove the urn from the Owners + const onDeselectValue = (selectedUrn: string) => { + setUrn(urn === selectedUrn ? '' : urn); + setInputValue(''); + setIsFocusedOnInput(true); + setSelectedAttribute(''); + }; + + const addBusinessAttribute = () => { + addBusinessAttributeMutation({ + variables: { + input: { + businessAttributeUrn: urn, + resourceUrn: resources[0], + }, + }, + }) + .then(({ errors }) => { + if (!errors) { + message.success({ + content: `Added Business Attribute!`, + duration: 2, + }); + } + }) + .catch((e) => { + message.destroy(); + message.error({ content: `Failed to add: \n ${e.message || ''}`, duration: 3 }); + }) + .finally(() => { + setDisableAction(false); + onCloseModal(); + setUrn(''); + }); + }; + + const removeBusinessAttribute = () => { + removeBusinessAttributeMutation({ + variables: { + input: { + businessAttributeUrn: urn, + resourceUrn: { + resourceUrn: resources[0].resourceUrn, + subResource: resources[0].subResource, + subResourceType: resources[0].subResourceType, + }, + }, + }, + }) + .then(({ errors }) => { + if (!errors) { + message.success({ + content: `Removed Business Attribute!`, + duration: 2, + }); + } + }) + .catch((e) => { + message.destroy(); + message.error( + handleBatchError(urn, e, { content: `Failed to remove: \n ${e.message || ''}`, duration: 3 }), + ); + }) + .finally(() => { + setDisableAction(false); + onCloseModal(); + setUrn(''); + }); + }; + + const editBusinessAttribute = () => { + if (operationType === OperationType.ADD) { + addBusinessAttribute(); + } else { + removeBusinessAttribute(); + } + }; + + const onOk = () => { + if (onOkOverride) { + onOkOverride(urn); + return; + } + + if (!resources) { + onCloseModal(); + return; + } + setDisableAction(true); + editBusinessAttribute(); + }; + + function selectAttributeFromBrowser(selectedUrn: string, displayName: string) { + setIsFocusedOnInput(false); + setUrn(selectedUrn); + setSelectedAttribute({ + selectedUrn, + component: ( +
+ + {displayName} +
+ ), + }); + } + + function clearInput() { + setInputValue(''); + setTimeout(() => setIsFocusedOnInput(true), 0); // call after click outside + } + + function handleBlur() { + setInputValue(''); + } + + function handleKeyDown(event) { + if (event.keyCode === ENTER_KEY_CODE) { + (inputEl.current as any).blur(); + } + } + + const isShowingAttributeBrowser = !inputValue && type === EntityType.BusinessAttribute && isFocusedOnInput; + + return ( + + + + + } + > + setIsFocusedOnInput(false)}> + { + onSelectValue(asset); + }} + onDeselect={(asset: any) => onDeselectValue(asset)} + onSearch={(value: string) => { + // eslint-disable-next-line react/prop-types + handleSearch(value.trim()); + // eslint-disable-next-line react/prop-types + setInputValue(value.trim()); + }} + tagRender={attributeRender} + value={urn || undefined} + onClear={clearInput} + onFocus={() => setIsFocusedOnInput(true)} + onBlur={handleBlur} + onInputKeyDown={handleKeyDown} + dropdownStyle={isShowingAttributeBrowser ? { display: 'none', color: 'RED' } : {}} + > + {attributeSearchOptions} + + + + + + + ); +} diff --git a/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx b/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx new file mode 100644 index 00000000000000..0f70f8a2b5630f --- /dev/null +++ b/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx @@ -0,0 +1,119 @@ +import styled from 'styled-components'; +import { message, Modal, Tag } from 'antd'; +import { GlobalOutlined } from '@ant-design/icons'; +import React from 'react'; +import Highlight from 'react-highlighter'; +import { useEntityRegistry } from '../../useEntityRegistry'; +import { BusinessAttributeAssociation, EntityType, SubResourceType } from '../../../types.generated'; +import { useHasMatchedFieldByUrn } from '../../search/context/SearchResultContext'; +import { MatchedFieldName } from '../../search/matches/constants'; +import { useRemoveBusinessAttributeMutation } from '../../../graphql/mutations.generated'; + +const highlightMatchStyle = { background: '#ffe58f', padding: '0' }; + +const StyledAttribute = styled(Tag)<{ fontSize?: number; highlightAttribute?: boolean }>` + &&& { + ${(props) => + props.highlightAttribute && + `background: ${props.theme.styles['highlight-color']}; + border: 1px solid ${props.theme.styles['highlight-border-color']}; + `} + } + ${(props) => props.fontSize && `font-size: ${props.fontSize}px;`} +`; + +interface Props { + businessAttribute: BusinessAttributeAssociation | undefined; + entityUrn?: string; + entitySubresource?: string; + canRemove?: boolean; + readOnly?: boolean; + highlightText?: string; + fontSize?: number; + onOpenModal?: () => void; + refetch?: () => Promise; +} + +export default function AttributeContent({ + businessAttribute, + canRemove, + readOnly, + highlightText, + fontSize, + onOpenModal, + entityUrn, + entitySubresource, + refetch, +}: Props) { + const entityRegistry = useEntityRegistry(); + const [removeBusinessAttributeMutation] = useRemoveBusinessAttributeMutation(); + const highlightAttribute = useHasMatchedFieldByUrn( + businessAttribute?.businessAttribute?.urn || '', + 'businessAttributes' as MatchedFieldName, + ); + + const removeAttribute = (attributeToRemove: BusinessAttributeAssociation) => { + onOpenModal?.(); + const AttributeName = + attributeToRemove && + entityRegistry.getDisplayName( + attributeToRemove.businessAttribute.type, + attributeToRemove.businessAttribute, + ); + Modal.confirm({ + title: `Do you want to remove ${AttributeName} attribute?`, + content: `Are you sure you want to remove the ${AttributeName} attribute?`, + onOk() { + if (attributeToRemove.associatedUrn || entityUrn) { + removeBusinessAttributeMutation({ + variables: { + input: { + businessAttributeUrn: attributeToRemove.businessAttribute.urn, + resourceUrn: { + resourceUrn: attributeToRemove.associatedUrn || entityUrn || '', + subResource: entitySubresource, + subResourceType: entitySubresource ? SubResourceType.DatasetField : null, + }, + }, + }, + }) + .then(({ errors }) => { + if (!errors) { + message.success({ content: 'Removed Business Attribute!', duration: 2 }); + } + }) + .then(refetch) + .catch((e) => { + message.destroy(); + message.error({ + content: `Failed to remove business attribute: \n ${e.message || ''}`, + duration: 3, + }); + }); + } + }, + onCancel() {}, + okText: 'Yes', + maskClosable: true, + closable: true, + }); + }; + + return ( + { + e.preventDefault(); + removeAttribute(businessAttribute as BusinessAttributeAssociation); + }} + fontSize={fontSize} + highlightAttribute={highlightAttribute} + > + + + {entityRegistry.getDisplayName(EntityType.BusinessAttribute, businessAttribute?.businessAttribute)} + + + ); +} diff --git a/datahub-web-react/src/app/shared/businessAttribute/BusinessAttributeGroup.tsx b/datahub-web-react/src/app/shared/businessAttribute/BusinessAttributeGroup.tsx new file mode 100644 index 00000000000000..ca2d532b06dc34 --- /dev/null +++ b/datahub-web-react/src/app/shared/businessAttribute/BusinessAttributeGroup.tsx @@ -0,0 +1,104 @@ +import styled from 'styled-components'; +import { Button, Typography } from 'antd'; +import React, { useState } from 'react'; +import { PlusOutlined } from '@ant-design/icons'; +import { EMPTY_MESSAGES } from '../../entity/shared/constants'; +import { BusinessAttributeAssociation, EntityType, SubResourceType } from '../../../types.generated'; +import EditBusinessAttributeModal from './AddBusinessAttributeModal'; +import StyledAttribute from './StyledAttribute'; + +type Props = { + businessAttribute?: BusinessAttributeAssociation; + canRemove?: boolean; + canAddAttribute?: boolean; + showEmptyMessage?: boolean; + buttonProps?: Record; + onOpenModal?: () => void; + maxShow?: number; + entityUrn?: string; + entityType?: EntityType; + entitySubresource?: string; + highlightText?: string; + fontSize?: number; + refetch?: () => Promise; + readOnly?: boolean; +}; + +const NoElementButton = styled(Button)` + :not(:last-child) { + margin-right: 8px; + } +`; + +export default function BusinessAttributeGroup({ + businessAttribute, + canAddAttribute, + showEmptyMessage, + buttonProps, + onOpenModal, + entityUrn, + entityType, + entitySubresource, + refetch, + readOnly, + canRemove, + highlightText, + fontSize, +}: Props) { + const [showAddModal, setShowAddModal] = useState(false); + const [addModalType, setAddModalType] = useState(EntityType.BusinessAttribute); + const businessAttributeEmpty = !businessAttribute?.associatedUrn?.length; + return ( + <> + {!businessAttributeEmpty && businessAttribute !== undefined && ( + + )} + {showEmptyMessage && canAddAttribute && businessAttributeEmpty && ( + + {EMPTY_MESSAGES.businessAttributes.title}. {EMPTY_MESSAGES.businessAttributes.description} + + )} + {canAddAttribute && !readOnly && businessAttributeEmpty && ( + { + setAddModalType(EntityType.BusinessAttribute); + setShowAddModal(true); + }} + {...buttonProps} + > + + Add Attribute + + )} + {showAddModal && !!entityUrn && !!entityType && ( + { + onOpenModal?.(); + setShowAddModal(false); + refetch?.(); + }} + resources={[ + { + resourceUrn: entityUrn, + subResource: entitySubresource, + subResourceType: entitySubresource ? SubResourceType.DatasetField : null, + }, + ]} + /> + )} + + ); +} diff --git a/datahub-web-react/src/app/shared/businessAttribute/StyledAttribute.tsx b/datahub-web-react/src/app/shared/businessAttribute/StyledAttribute.tsx new file mode 100644 index 00000000000000..1a69ed23b2f007 --- /dev/null +++ b/datahub-web-react/src/app/shared/businessAttribute/StyledAttribute.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; +import { BusinessAttributeAssociation, EntityType } from '../../../types.generated'; +import { useEntityRegistry } from '../../useEntityRegistry'; +import { HoverEntityTooltip } from '../../recommendations/renderer/component/HoverEntityTooltip'; +import AttributeContent from './AttributeContent'; + +const AttributeLink = styled(Link)` + display: inline-block; + margin-bottom: 8px; +`; + +const AttributeWrapper = styled.span` + display: inline-block; + margin-bottom: 8px; +`; + +interface Props { + businessAttribute: BusinessAttributeAssociation; + entityUrn?: string; + entitySubresource?: string; + canRemove?: boolean; + readOnly?: boolean; + highlightText?: string; + fontSize?: number; + onOpenModal?: () => void; + refetch?: () => Promise; +} + +export default function StyledAttribute(props: Props) { + const { businessAttribute, readOnly } = props; + const entityRegistry = useEntityRegistry(); + + if (readOnly) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/datahub-web-react/src/graphql/dataset.graphql b/datahub-web-react/src/graphql/dataset.graphql index 658ce2b47c5676..044a9b639c5ba7 100644 --- a/datahub-web-react/src/graphql/dataset.graphql +++ b/datahub-web-react/src/graphql/dataset.graphql @@ -292,6 +292,11 @@ fragment datasetSchema on Dataset { glossaryTerms { ...glossaryTerms } + businessAttributes { + businessAttribute { + ...businessAttribute + } + } } } } diff --git a/datahub-web-react/src/graphql/fragments.graphql b/datahub-web-react/src/graphql/fragments.graphql index 72474911b93101..f7316bbbd90009 100644 --- a/datahub-web-react/src/graphql/fragments.graphql +++ b/datahub-web-react/src/graphql/fragments.graphql @@ -1149,3 +1149,49 @@ fragment entityDisplayNameFields on Entity { instanceId } } + +fragment businessAttribute on BusinessAttributeAssociation { + businessAttribute { + urn + type + ownership { + ...ownershipFields + } + properties { + name + description + businessAttributeDataType: type + lastModified { + time + } + created { + time + } + tags { + tags { + tag { + urn + name + properties { + name + } + } + associatedUrn + } + } + glossaryTerms { + terms { + term { + urn + type + properties { + name + } + } + associatedUrn + } + } + } + } + associatedUrn +} diff --git a/datahub-web-react/src/graphql/mutations.graphql b/datahub-web-react/src/graphql/mutations.graphql index 439d20810ef7c9..4071ae38e898a5 100644 --- a/datahub-web-react/src/graphql/mutations.graphql +++ b/datahub-web-react/src/graphql/mutations.graphql @@ -127,3 +127,11 @@ mutation updateLineage($input: UpdateLineageInput!) { mutation updateEmbed($input: UpdateEmbedInput!) { updateEmbed(input: $input) } + +mutation addBusinessAttribute($input: AddBusinessAttributeInput!) { + addBusinessAttribute(input: $input) +} + +mutation removeBusinessAttribute($input: AddBusinessAttributeInput!) { + removeBusinessAttribute(input: $input) +} From c1f768000de172feef32df6afa61591fc6d8591b Mon Sep 17 00:00:00 2001 From: aditigup Date: Fri, 19 Jan 2024 17:55:59 +0530 Subject: [PATCH 09/50] Business Attribute related entities and css --- .../app/businessAttribute/CreateBusinessAttributeModal.tsx | 1 + .../profile/BusinessAttributeRelatedEntity.tsx | 2 +- .../app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx | 2 +- .../Dataset/Schema/utils/useBusinessAttributeRenderer.tsx | 2 +- .../shared/businessAttribute/AddBusinessAttributeModal.tsx | 2 ++ .../java/com/linkedin/metadata/search/utils/ESUtils.java | 1 + .../com/linkedin/schema/EditableSchemaFieldInfo.pdl | 7 +++++++ 7 files changed, 14 insertions(+), 3 deletions(-) diff --git a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx index c23fca800cb6a0..60dd4cee728890 100644 --- a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx +++ b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx @@ -98,6 +98,7 @@ export default function CreateBusinessAttributeModal({ visible, onClose, onCreat message.error({ content: `Failed to create: \n ${e.message || ''}`, duration: 3 }); }); onModalClose(); + setDocumentation(''); }; // Handle the Enter press diff --git a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx index fc8208becc56b6..46d9d4ea51d245 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeRelatedEntity.tsx @@ -12,7 +12,7 @@ export default function BusinessAttributeRelatedEntity() { const fixedOrFilters = (entityUrn && [ { - field: 'businessAttributes', + field: 'businessAttribute', values: [entityUrn], }, ]) || diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx index c654cdeb157599..730de331086be7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx @@ -173,7 +173,7 @@ export default function SchemaTable({ }; const businessAttributeColumn = { - width: '13%', + width: '18%', title: 'Business Attributes', dataIndex: 'businessAttribute', key: 'businessAttribute', diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx index 2bc28fe6811833..785a80fba681d9 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx @@ -26,7 +26,7 @@ export default function useBusinessAttributeRenderer( ); return ( -
+
` diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index 53765acb8e29e8..35f49eff1c8979 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -86,6 +86,7 @@ public class ESUtils { put("fieldGlossaryTerms", ImmutableList.of("fieldGlossaryTerms", "editedFieldGlossaryTerms")); put("fieldDescriptions", ImmutableList.of("fieldDescriptions", "editedFieldDescriptions")); put("description", ImmutableList.of("description", "editedDescription")); + put("businessAttribute", ImmutableList.of("editedFieldBusinessAttribute", "businessAttribute")); }}; public static final Set BOOLEAN_FIELDS = ImmutableSet.of( diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl index 3b05e5a616b04c..5d8916fcaf7b5e 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl @@ -15,5 +15,12 @@ record EditableSchemaFieldInfo includes EditableSchemaFieldBase { "entityTypes": [ "businessAttribute" ] } } + @Searchable = { + "/destinationUrn": { + "fieldName": "editedFieldBusinessAttribute", + "fieldType": "URN", + "boostScore": 0.5 + } + } businessAttribute: optional BusinessAttributeAssociation } From 58d364e58c92bac9487299a0083ab425f67333ef Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Mon, 22 Jan 2024 22:28:31 +0530 Subject: [PATCH 10/50] businessattribute: openApi support --- .../io/datahubproject/OpenApiEntities.java | 1 + .../delegates/EntityApiDelegateImpl.java | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java index 888c4a0e999311..497f69ba0cec7a 100644 --- a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java +++ b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java @@ -58,6 +58,7 @@ public class OpenApiEntities { .add("notebookInfo").add("editableNotebookProperties") .add("dataProductProperties") .add("institutionalMemory") + .add("businessAttributeInfo") .build(); diff --git a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java index 207c2284e2673c..5d9bd9e1f0dfd2 100644 --- a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java +++ b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java @@ -17,6 +17,8 @@ import io.datahubproject.openapi.exception.UnauthorizedException; import io.datahubproject.openapi.generated.BrowsePathsV2AspectRequestV2; import io.datahubproject.openapi.generated.BrowsePathsV2AspectResponseV2; +import io.datahubproject.openapi.generated.BusinessAttributeInfoAspectRequestV2; +import io.datahubproject.openapi.generated.BusinessAttributeInfoAspectResponseV2; import io.datahubproject.openapi.generated.ChartInfoAspectRequestV2; import io.datahubproject.openapi.generated.ChartInfoAspectResponseV2; import io.datahubproject.openapi.generated.DataProductPropertiesAspectRequestV2; @@ -602,4 +604,34 @@ public ResponseEntity deleteDataProductProperties(String urn) { .map(StackWalker.StackFrame::getMethodName)).get(); return deleteAspect(urn, methodNameToAspectName(methodName)); } + + public ResponseEntity createBusinessAttributeInfo(BusinessAttributeInfoAspectRequestV2 body, String urn) { + String methodName = walker.walk(frames -> frames + .findFirst() + .map(StackWalker.StackFrame::getMethodName)).get(); + return createAspect(urn, methodNameToAspectName(methodName), body, BusinessAttributeInfoAspectRequestV2.class, + BusinessAttributeInfoAspectResponseV2.class); + } + + public ResponseEntity deleteBusinessAttributeInfo(String urn) { + String methodName = walker.walk(frames -> frames + .findFirst() + .map(StackWalker.StackFrame::getMethodName)).get(); + return deleteAspect(urn, methodNameToAspectName(methodName)); + } + + public ResponseEntity getBusinessAttributeInfo(String urn, Boolean systemMetadata) { + String methodName = walker.walk(frames -> frames + .findFirst() + .map(StackWalker.StackFrame::getMethodName)).get(); + return getAspect(urn, systemMetadata, methodNameToAspectName(methodName), _respClazz, + BusinessAttributeInfoAspectResponseV2.class); + } + + public ResponseEntity headBusinessAttributeInfo(String urn) { + String methodName = walker.walk(frames -> frames + .findFirst() + .map(StackWalker.StackFrame::getMethodName)).get(); + return headAspect(urn, methodNameToAspectName(methodName)); + } } From b127d4f44c798db53915b0f5dc99629db29b3645 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Mon, 22 Jan 2024 23:28:14 +0530 Subject: [PATCH 11/50] businessattribute: businessattribute custom properties fetching --- .../businessattribute/mappers/BusinessAttributeMapper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java index 93a4301b1a362a..f5a37646982326 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -8,6 +8,7 @@ import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.SchemaFieldDataType; import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper; +import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper; @@ -68,6 +69,9 @@ private void mapBusinessAttributeInfo(BusinessAttribute businessAttribute, DataM if (businessAttributeInfo.hasType()) { attributeInfo.setType(mapSchemaFieldDataType(businessAttributeInfo.getType())); } + if (businessAttributeInfo.hasCustomProperties()) { + attributeInfo.setCustomProperties(CustomPropertiesMapper.map(businessAttributeInfo.getCustomProperties(), entityUrn)); + } businessAttribute.setProperties(attributeInfo); } From 1ab94f21d270edada76483353eaf621f675711bb Mon Sep 17 00:00:00 2001 From: aditigup Date: Wed, 24 Jan 2024 00:31:09 +0530 Subject: [PATCH 12/50] Business Attribute Minor Issues --- .../BusinessAttributeType.java | 6 +- .../BusinessAttributeEntity.tsx | 13 ++- .../BusinessAttributeDataTypeSection.tsx | 90 +++++++++++++++++++ .../profile/header/EntityHeader.tsx | 2 + .../src/app/entity/shared/types.ts | 1 + .../search/sidebar/useAggregationsQuery.ts | 6 +- .../businessAttribute/AttributeContent.tsx | 2 +- datahub-web-react/src/graphql/search.graphql | 6 ++ 8 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java index 4c57f34af9552b..964c943369ef3d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java @@ -13,9 +13,11 @@ import com.linkedin.datahub.graphql.resolvers.ResolverUtils; import com.linkedin.datahub.graphql.types.SearchableEntityType; import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributeMapper; +import com.linkedin.datahub.graphql.types.mappers.AutoCompleteResultsMapper; import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper; import com.linkedin.entity.EntityResponse; import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.query.AutoCompleteResult; import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; @@ -104,6 +106,8 @@ public SearchResults search(@Nonnull String query, @Nullable List { { component: SidebarAboutSection, }, + { + component: BusinessAttributeDataTypeSection, + }, { component: SidebarOwnerSection, }, @@ -138,14 +142,7 @@ export class BusinessAttributeEntity implements Entity { }; renderSearch = (result: SearchResult) => { - return ( - - ); + return this.renderPreview(PreviewType.SEARCH, result.entity as BusinessAttribute); }; supportedCapabilities = () => { diff --git a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx new file mode 100644 index 00000000000000..e7cdbafdd54cc4 --- /dev/null +++ b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx @@ -0,0 +1,90 @@ +import { Button, message, Select } from 'antd'; +import { EditOutlined } from '@ant-design/icons'; +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { useEntityData, useRefetch } from '../../shared/EntityContext'; +import { SidebarHeader } from '../../shared/containers/profile/sidebar/SidebarHeader'; +import { SchemaFieldDataType } from '../../../../types.generated'; +import { useUpdateBusinessAttributeMutation } from '../../../../graphql/businessAttribute.generated'; + +interface Props { + readOnly?: boolean; +} + +const DataTypeSelect = styled(Select)` + && { + width: 100%; + margin-top: 1em; + margin-bottom: 1em; + } +`; +// Ensures that any newly added datatype is automatically included in the user dropdown. +const DATA_TYPES = Object.values(SchemaFieldDataType); +export const BusinessAttributeDataTypeSection = ({ readOnly }: Props) => { + const { urn, entityData } = useEntityData(); + const [originalDescription, setOriginalDescription] = useState(null); + const [isEditing, setEditing] = useState(false); + const refetch = useRefetch(); + + useEffect(() => { + if (entityData?.properties?.businessAttributeDataType) { + setOriginalDescription(entityData?.properties?.businessAttributeDataType); + } + }, [entityData]); + + const [updateBusinessAttribute] = useUpdateBusinessAttributeMutation(); + + const handleChange = (value) => { + if (value === originalDescription) { + setEditing(false); + return; + } + + updateBusinessAttribute({ variables: { urn, input: { type: value } } }) + .then(() => { + setEditing(false); + setOriginalDescription(value); + message.success({ content: 'Data Type Updated', duration: 2 }); + refetch(); + }) + .catch((e: unknown) => { + message.destroy(); + if (e instanceof Error) { + message.error({ content: `Failed to update Data Type: \n ${e.message || ''}`, duration: 3 }); + } + }); + }; + + // Toggle editing mode + const handleEditClick = () => { + setEditing(!isEditing); + }; + + return ( +
+ + + + ) + } + /> + {originalDescription} + {isEditing && ( + + {DATA_TYPES.map((dataType: SchemaFieldDataType) => ( + + {dataType} + + ))} + + )} +
+ ); +}; + +export default BusinessAttributeDataTypeSection; diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx index 69389f5dcf6fc0..09fa23dbc9f57c 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx @@ -69,6 +69,8 @@ export function getCanEditName( return privileges?.manageDomains; case EntityType.DataProduct: return true; // TODO: add permissions for data products + case EntityType.BusinessAttribute: + return privileges?.manageBusinessAttributes; default: return false; } diff --git a/datahub-web-react/src/app/entity/shared/types.ts b/datahub-web-react/src/app/entity/shared/types.ts index 6596711d4e82a6..2dfdb2468d07a4 100644 --- a/datahub-web-react/src/app/entity/shared/types.ts +++ b/datahub-web-react/src/app/entity/shared/types.ts @@ -73,6 +73,7 @@ export type GenericEntityProperties = { qualifiedName?: Maybe; sourceUrl?: Maybe; sourceRef?: Maybe; + businessAttributeDataType?: Maybe; }>; globalTags?: Maybe; glossaryTerms?: Maybe; diff --git a/datahub-web-react/src/app/search/sidebar/useAggregationsQuery.ts b/datahub-web-react/src/app/search/sidebar/useAggregationsQuery.ts index c32dca8ba05377..bd9ac540b02204 100644 --- a/datahub-web-react/src/app/search/sidebar/useAggregationsQuery.ts +++ b/datahub-web-react/src/app/search/sidebar/useAggregationsQuery.ts @@ -51,7 +51,11 @@ const useAggregationsQuery = ({ facets, excludeFilters = false, skip }: Props) = ?.find((facet) => facet.field === ENTITY_FILTER_NAME) ?.aggregations.filter((aggregation) => { const type = aggregation.value as EntityType; - return registry.getEntity(type).isBrowseEnabled() && !GLOSSARY_ENTITY_TYPES.includes(type); + return ( + registry.getEntity(type).isBrowseEnabled() && + !GLOSSARY_ENTITY_TYPES.includes(type) && + EntityType.BusinessAttribute !== type + ); }) .sort((a, b) => { const nameA = registry.getCollectionName(a.value as EntityType); diff --git a/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx b/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx index 0f70f8a2b5630f..0d426ba7c7c774 100644 --- a/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx +++ b/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx @@ -101,7 +101,7 @@ export default function AttributeContent({ return ( { e.preventDefault(); diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index f978139c41b1ba..aabf8639919d46 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -233,6 +233,12 @@ fragment autoCompleteFields on Entity { ... on DataPlatform { ...nonConflictingPlatformFields } + ... on BusinessAttribute { + properties { + name + description + } + } } query getAutoCompleteResults($input: AutoCompleteInput!) { From fcbc9c19714f0ae38685e097376d5c35ed29df36 Mon Sep 17 00:00:00 2001 From: aditigup Date: Wed, 24 Jan 2024 16:04:30 +0530 Subject: [PATCH 13/50] Business Attribute Minor Issues --- .../mutate/util/BusinessAttributeUtils.java | 26 ++++++++++++++++--- .../mappers/BusinessAttributeMapper.java | 6 ----- .../CreateBusinessAttributeModal.tsx | 3 ++- .../businessAttributeUtils.ts | 23 ++++++++++++++++ .../BusinessAttributeDataTypeSection.tsx | 2 +- .../src/graphql/businessAttribute.graphql | 5 ++++ 6 files changed, 54 insertions(+), 11 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java index ff9e7827a27706..d3fab88e91e2a5 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java @@ -13,12 +13,17 @@ import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; import com.linkedin.r2.RemoteInvocationException; -import com.linkedin.schema.ArrayType; +import com.linkedin.schema.EnumType; +import com.linkedin.schema.FixedType; +import com.linkedin.schema.MapType; import com.linkedin.schema.BooleanType; -import com.linkedin.schema.DateType; +import com.linkedin.schema.StringType; +import com.linkedin.schema.ArrayType; +import com.linkedin.schema.BytesType; import com.linkedin.schema.NumberType; +import com.linkedin.schema.TimeType; +import com.linkedin.schema.DateType; import com.linkedin.schema.SchemaFieldDataType; -import com.linkedin.schema.StringType; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nonnull; @@ -73,6 +78,21 @@ public static SchemaFieldDataType mapSchemaFieldDataType(com.linkedin.datahub.gr } SchemaFieldDataType schemaFieldDataType = new SchemaFieldDataType(); switch (type) { + case BYTES: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new BytesType())); + return schemaFieldDataType; + case FIXED: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new FixedType())); + return schemaFieldDataType; + case ENUM: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new EnumType())); + return schemaFieldDataType; + case MAP: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new MapType())); + return schemaFieldDataType; + case TIME: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new TimeType())); + return schemaFieldDataType; case BOOLEAN: schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new BooleanType())); return schemaFieldDataType; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java index f5a37646982326..e881a3a24594bc 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -93,16 +93,10 @@ private SchemaFieldDataType mapSchemaFieldDataType(@Nonnull final com.linkedin.s return SchemaFieldDataType.TIME; } else if (type.isEnumType()) { return SchemaFieldDataType.ENUM; - } else if (type.isNullType()) { - return SchemaFieldDataType.NULL; } else if (type.isArrayType()) { return SchemaFieldDataType.ARRAY; } else if (type.isMapType()) { return SchemaFieldDataType.MAP; - } else if (type.isRecordType()) { - return SchemaFieldDataType.STRUCT; - } else if (type.isUnionType()) { - return SchemaFieldDataType.UNION; } else { throw new RuntimeException(String.format("Unrecognized SchemaFieldDataType provided %s", type.memberType().toString())); diff --git a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx index 60dd4cee728890..a2078b87893339 100644 --- a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx +++ b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx @@ -5,10 +5,11 @@ import { EditOutlined } from '@ant-design/icons'; import DOMPurify from 'dompurify'; import { useEnterKeyListener } from '../shared/useEnterKeyListener'; import { useCreateBusinessAttributeMutation } from '../../graphql/businessAttribute.generated'; -import { CreateBusinessAttributeInput, SchemaFieldDataType, EntityType } from '../../types.generated'; +import { CreateBusinessAttributeInput, EntityType } from '../../types.generated'; import analytics, { EventType } from '../analytics'; import { useEntityRegistry } from '../useEntityRegistry'; import DescriptionModal from '../entity/shared/components/legacy/DescriptionModal'; +import { SchemaFieldDataType } from './businessAttributeUtils'; type Props = { visible: boolean; diff --git a/datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts b/datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts index 938cb34a86d2da..ec8c44d79901c3 100644 --- a/datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts +++ b/datahub-web-react/src/app/businessAttribute/businessAttributeUtils.ts @@ -12,3 +12,26 @@ export function getRelatedEntitiesUrl(entityRegistry: EntityRegistry, urn: strin 'Related Entities', )}`; } + +export enum SchemaFieldDataType { + /** A boolean type */ + Boolean = 'BOOLEAN', + /** A fixed bytestring type */ + Fixed = 'FIXED', + /** A string type */ + String = 'STRING', + /** A string of bytes */ + Bytes = 'BYTES', + /** A number, including integers, floats, and doubles */ + Number = 'NUMBER', + /** A datestrings type */ + Date = 'DATE', + /** A timestamp type */ + Time = 'TIME', + /** An enum type */ + Enum = 'ENUM', + /** A map collection type */ + Map = 'MAP', + /** An array collection type */ + Array = 'ARRAY', +} diff --git a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx index e7cdbafdd54cc4..0b90665fe3a3b1 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx @@ -4,8 +4,8 @@ import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { useEntityData, useRefetch } from '../../shared/EntityContext'; import { SidebarHeader } from '../../shared/containers/profile/sidebar/SidebarHeader'; -import { SchemaFieldDataType } from '../../../../types.generated'; import { useUpdateBusinessAttributeMutation } from '../../../../graphql/businessAttribute.generated'; +import { SchemaFieldDataType } from '../../../businessAttribute/businessAttributeUtils'; interface Props { readOnly?: boolean; diff --git a/datahub-web-react/src/graphql/businessAttribute.graphql b/datahub-web-react/src/graphql/businessAttribute.graphql index d801f7a92d7551..c58b2cd8451a5a 100644 --- a/datahub-web-react/src/graphql/businessAttribute.graphql +++ b/datahub-web-react/src/graphql/businessAttribute.graphql @@ -26,6 +26,11 @@ fragment businessAttributeFields on BusinessAttribute { name description businessAttributeDataType: type + customProperties { + key + value + associatedUrn + } lastModified { time } From fa2349464e224ea4ed4089cc966708baf3dfc12b Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Sat, 27 Jan 2024 23:50:59 +0530 Subject: [PATCH 14/50] businessattribute: generate platform events for business attributes --- .../java/com/linkedin/metadata/Constants.java | 1 + ...sinessAttributeAssociationChangeEvent.java | 46 ++ ...ributeAssociationChangeEventGenerator.java | 61 +++ ...nessAttributeInfoChangeEventGenerator.java | 102 ++++ ...bleSchemaMetadataChangeEventGenerator.java | 472 +++++++++--------- .../event/EntityChangeEventGeneratorHook.java | 1 + ...tyChangeEventGeneratorRegistryFactory.java | 4 + .../timeline/data/ChangeCategory.java | 4 +- 8 files changed, 466 insertions(+), 225 deletions(-) create mode 100644 metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java create mode 100644 metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java create mode 100644 metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index c06215c8aea700..e6c3fdc5bc7dee 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -308,6 +308,7 @@ public class Constants { //Business Attribute public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; + public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; /** * Retention diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java new file mode 100644 index 00000000000000..65e6afd7ba66c4 --- /dev/null +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java @@ -0,0 +1,46 @@ +package com.linkedin.metadata.timeline.data.entity; + +import com.google.common.collect.ImmutableMap; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.Urn; +import com.linkedin.metadata.timeline.data.ChangeCategory; +import com.linkedin.metadata.timeline.data.ChangeEvent; +import com.linkedin.metadata.timeline.data.ChangeOperation; +import com.linkedin.metadata.timeline.data.SemanticChangeType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Value; +import lombok.experimental.NonFinal; + +import java.util.Map; + +@EqualsAndHashCode(callSuper = true) +@Value +@NonFinal +@Getter +public class BusinessAttributeAssociationChangeEvent extends ChangeEvent { + @Builder(builderMethodName = "entityBusinessAttributeAssociationChangeEventBuilder") + public BusinessAttributeAssociationChangeEvent(String entityUrn, + ChangeCategory category, + ChangeOperation operation, + String modifier, + Map parameters, + AuditStamp auditStamp, + SemanticChangeType semVerChange, + String description, + Urn businessAttributeUrn) { + super( + entityUrn, + category, + operation, + modifier, + ImmutableMap.of( + "businessAttributeUrn", businessAttributeUrn.toString() + ), + auditStamp, + semVerChange, + description + ); + } +} diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java new file mode 100644 index 00000000000000..f2775f0b3478aa --- /dev/null +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java @@ -0,0 +1,61 @@ +package com.linkedin.metadata.timeline.eventgenerator; + +import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.Urn; +import com.linkedin.metadata.timeline.data.ChangeCategory; +import com.linkedin.metadata.timeline.data.ChangeEvent; +import com.linkedin.metadata.timeline.data.ChangeOperation; +import com.linkedin.metadata.timeline.data.SemanticChangeType; +import com.linkedin.metadata.timeline.data.entity.BusinessAttributeAssociationChangeEvent; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class BusinessAttributeAssociationChangeEventGenerator extends EntityChangeEventGenerator { + + private static final String BUSINESS_ATTRIBUTE_ADDED_FORMAT = "BusinessAttribute '%s' added to entity '%s'."; + private static final String BUSINESS_ATTRIBUTE_REMOVED_FORMAT = "BusinessAttribute '%s' removed from entity '%s'."; + + public static List computeDiffs(BusinessAttributeAssociation baseAssociation, + BusinessAttributeAssociation targetAssociation, + String urn, AuditStamp auditStamp) { + List changeEvents = new ArrayList<>(); + + if (Objects.nonNull(baseAssociation) && Objects.isNull(targetAssociation)) { + changeEvents.add(createChangeEvent(baseAssociation, urn, ChangeOperation.REMOVE, + BUSINESS_ATTRIBUTE_REMOVED_FORMAT, auditStamp)); + + } else if (Objects.isNull(baseAssociation) && Objects.nonNull(targetAssociation)) { + changeEvents.add(createChangeEvent(targetAssociation, urn, ChangeOperation.ADD, + BUSINESS_ATTRIBUTE_ADDED_FORMAT, auditStamp)); + } + return changeEvents; + } + + private static ChangeEvent createChangeEvent(BusinessAttributeAssociation association, String entityUrn, ChangeOperation operation, + String format, AuditStamp auditStamp) { + return BusinessAttributeAssociationChangeEvent.entityBusinessAttributeAssociationChangeEventBuilder() + .modifier(association.getDestinationUrn().toString()) + .entityUrn(entityUrn) + .category(ChangeCategory.BUSINESS_ATTRIBUTE) + .operation(operation) + .semVerChange(SemanticChangeType.MINOR) + .description(String.format(format, association.getDestinationUrn().getId(), entityUrn)) + .businessAttributeUrn(association.getDestinationUrn()) + .auditStamp(auditStamp) + .build(); + } + + @Override + public List getChangeEvents(@Nonnull Urn urn, + @Nonnull String entity, + @Nonnull String aspect, + @Nonnull Aspect from, + @Nonnull Aspect to, + @Nonnull AuditStamp auditStamp) { + return computeDiffs(from.getValue(), to.getValue(), urn.toString(), auditStamp); + } +} diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java new file mode 100644 index 00000000000000..dd31629ff116e3 --- /dev/null +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java @@ -0,0 +1,102 @@ +package com.linkedin.metadata.timeline.eventgenerator; + +import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.GlobalTags; +import com.linkedin.common.GlossaryTerms; +import com.linkedin.common.urn.Urn; +import com.linkedin.metadata.timeline.data.ChangeCategory; +import com.linkedin.metadata.timeline.data.ChangeEvent; +import com.linkedin.metadata.timeline.data.ChangeOperation; +import com.linkedin.metadata.timeline.data.SemanticChangeType; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +public class BusinessAttributeInfoChangeEventGenerator extends EntityChangeEventGenerator { + + public static final String ATTRIBUTE_DOCUMENTATION_ADDED_FORMAT = + "Documentation for the businessAttribute '%s' has been added: '%s'"; + public static final String ATTRIBUTE_DOCUMENTATION_REMOVED_FORMAT = + "Documentation for the businessAttribute '%s' has been removed: '%s'"; + public static final String ATTRIBUTE_DOCUMENTATION_UPDATED_FORMAT = + "Documentation for the businessAttribute '%s' has been updated from '%s' to '%s'."; + + @Override + public List getChangeEvents(@Nonnull Urn urn, + @Nonnull String entity, + @Nonnull String aspect, + @Nonnull Aspect from, + @Nonnull Aspect to, + @Nonnull AuditStamp auditStamp) { + final List changeEvents = new ArrayList<>(); + changeEvents.addAll(getDocumentationChangeEvent(from.getValue(), to.getValue(), urn.toString(), auditStamp)); + changeEvents.addAll(getGlossaryTermChangeEvents(from.getValue(), to.getValue(), urn.toString(), auditStamp)); + changeEvents.addAll(getTagChangeEvents(from.getValue(), to.getValue(), urn.toString(), auditStamp)); + return changeEvents; + } + + private List getDocumentationChangeEvent(BusinessAttributeInfo baseBusinessAttributeInfo, + BusinessAttributeInfo targetBusinessAttributeInfo, + String entityUrn, AuditStamp auditStamp) { + String baseDescription = (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getDescription() : null; + String targetDescription = (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getDescription() : null; + List changeEvents = new ArrayList<>(); + if (baseDescription == null && targetDescription != null) { + changeEvents.add(createChangeEvent(targetBusinessAttributeInfo, entityUrn, + ChangeOperation.ADD, ATTRIBUTE_DOCUMENTATION_ADDED_FORMAT, auditStamp, targetDescription)); + } + + if (baseDescription != null && targetDescription == null) { + changeEvents.add(createChangeEvent(baseBusinessAttributeInfo, entityUrn, + ChangeOperation.REMOVE, ATTRIBUTE_DOCUMENTATION_REMOVED_FORMAT, auditStamp, baseDescription)); + } + + if (baseDescription != null && !baseDescription.equals(targetDescription)) { + changeEvents.add(createChangeEvent(targetBusinessAttributeInfo, entityUrn, + ChangeOperation.MODIFY, ATTRIBUTE_DOCUMENTATION_UPDATED_FORMAT, auditStamp, baseDescription, targetDescription)); + } + + return changeEvents; + } + + private List getGlossaryTermChangeEvents(BusinessAttributeInfo baseBusinessAttributeInfo, + BusinessAttributeInfo targetBusinessAttributeInfo, + String entityUrn, AuditStamp auditStamp) { + GlossaryTerms baseGlossaryTerms = (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getGlossaryTerms() : null; + GlossaryTerms targetGlossaryTerms = (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getGlossaryTerms() : null; + + List entityGlossaryTermsChangeEvents = + GlossaryTermsChangeEventGenerator.computeDiffs(baseGlossaryTerms, targetGlossaryTerms, + entityUrn.toString(), auditStamp); + + return entityGlossaryTermsChangeEvents; + } + + private List getTagChangeEvents(BusinessAttributeInfo baseBusinessAttributeInfo, + BusinessAttributeInfo targetBusinessAttributeInfo, + String entityUrn, AuditStamp auditStamp) { + GlobalTags baseGlobalTags = (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getGlobalTags() : null; + GlobalTags targetGlobalTags = (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getGlobalTags() : null; + + List entityTagChangeEvents = + GlobalTagsChangeEventGenerator.computeDiffs(baseGlobalTags, targetGlobalTags, entityUrn.toString(), + auditStamp); + + return entityTagChangeEvents; + } + + private ChangeEvent createChangeEvent(BusinessAttributeInfo businessAttributeInfo, String entityUrn, + ChangeOperation operation, String format, AuditStamp auditStamp, String... descriptions) { + return ChangeEvent.builder() + .modifier(businessAttributeInfo.getFieldPath()) + .entityUrn(entityUrn) + .category(ChangeCategory.DOCUMENTATION) + .operation(operation) + .semVerChange(SemanticChangeType.MINOR) + .description(String.format(format, businessAttributeInfo.getFieldPath(), descriptions)) + .auditStamp(auditStamp) + .build(); + } +} diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java index 4a1de4c3421eda..3c862430d26456 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java @@ -2,6 +2,7 @@ import com.datahub.util.RecordUtils; import com.github.fge.jsonpatch.JsonPatch; +import com.linkedin.businessattribute.BusinessAttributeAssociation; import com.linkedin.common.AuditStamp; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; @@ -17,6 +18,7 @@ import com.linkedin.schema.EditableSchemaFieldInfoArray; import com.linkedin.schema.EditableSchemaMetadata; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -25,255 +27,277 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.Nonnull; -import static com.linkedin.metadata.Constants.*; -import static com.linkedin.metadata.timeline.eventgenerator.ChangeEventGeneratorUtils.*; +import static com.linkedin.metadata.Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME; +import static com.linkedin.metadata.timeline.eventgenerator.ChangeEventGeneratorUtils.convertEntityGlossaryTermChangeEvents; +import static com.linkedin.metadata.timeline.eventgenerator.ChangeEventGeneratorUtils.convertEntityTagChangeEvents; +import static com.linkedin.metadata.timeline.eventgenerator.ChangeEventGeneratorUtils.getSchemaFieldUrn; public class EditableSchemaMetadataChangeEventGenerator extends EntityChangeEventGenerator { - public static final String FIELD_DOCUMENTATION_ADDED_FORMAT = - "Documentation for the field '%s' of '%s' has been added: '%s'"; - public static final String FIELD_DOCUMENTATION_REMOVED_FORMAT = - "Documentation for the field '%s' of '%s' has been removed: '%s'"; - public static final String FIELD_DOCUMENTATION_UPDATED_FORMAT = - "Documentation for the field '%s' of '%s' has been updated from '%s' to '%s'."; - private static final Set SUPPORTED_CATEGORIES = - Stream.of(ChangeCategory.DOCUMENTATION, ChangeCategory.TAG, ChangeCategory.GLOSSARY_TERM) - .collect(Collectors.toSet()); - - private static void sortEditableSchemaMetadataByFieldPath(EditableSchemaMetadata editableSchemaMetadata) { - if (editableSchemaMetadata == null) { - return; - } - List editableSchemaFieldInfos = - new ArrayList<>(editableSchemaMetadata.getEditableSchemaFieldInfo()); - editableSchemaFieldInfos.sort(Comparator.comparing(EditableSchemaFieldInfo::getFieldPath)); - editableSchemaMetadata.setEditableSchemaFieldInfo(new EditableSchemaFieldInfoArray(editableSchemaFieldInfos)); - } - - private static List getAllChangeEvents(EditableSchemaFieldInfo baseFieldInfo, - EditableSchemaFieldInfo targetFieldInfo, String entityUrn, ChangeCategory changeCategory, - AuditStamp auditStamp) { - List changeEvents = new ArrayList<>(); - Urn datasetFieldUrn = getDatasetFieldUrn(baseFieldInfo, targetFieldInfo, entityUrn); - if (changeCategory == ChangeCategory.DOCUMENTATION) { - ChangeEvent documentationChangeEvent = getDocumentationChangeEvent(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp); - if (documentationChangeEvent != null) { - changeEvents.add(documentationChangeEvent); - } - } - if (changeCategory == ChangeCategory.TAG) { - changeEvents.addAll(getTagChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); - } - if (changeCategory == ChangeCategory.GLOSSARY_TERM) { - changeEvents.addAll(getGlossaryTermChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); - } - return changeEvents; - } - - private static List computeDiffs(EditableSchemaMetadata baseEditableSchemaMetadata, - EditableSchemaMetadata targetEditableSchemaMetadata, String entityUrn, ChangeCategory changeCategory, AuditStamp auditStamp) { - sortEditableSchemaMetadataByFieldPath(baseEditableSchemaMetadata); - sortEditableSchemaMetadataByFieldPath(targetEditableSchemaMetadata); - List changeEvents = new ArrayList<>(); - EditableSchemaFieldInfoArray baseFieldInfos = - (baseEditableSchemaMetadata != null) ? baseEditableSchemaMetadata.getEditableSchemaFieldInfo() - : new EditableSchemaFieldInfoArray(); - EditableSchemaFieldInfoArray targetFieldInfos = targetEditableSchemaMetadata.getEditableSchemaFieldInfo(); - int baseIdx = 0; - int targetIdx = 0; - while (baseIdx < baseFieldInfos.size() && targetIdx < targetFieldInfos.size()) { - EditableSchemaFieldInfo baseFieldInfo = baseFieldInfos.get(baseIdx); - EditableSchemaFieldInfo targetFieldInfo = targetFieldInfos.get(targetIdx); - int comparison = baseFieldInfo.getFieldPath().compareTo(targetFieldInfo.getFieldPath()); - if (comparison == 0) { - changeEvents.addAll(getAllChangeEvents(baseFieldInfo, targetFieldInfo, entityUrn, changeCategory, auditStamp)); - ++baseIdx; - ++targetIdx; - } else if (comparison < 0) { - // EditableFieldInfo got removed. - changeEvents.addAll(getAllChangeEvents(baseFieldInfo, null, entityUrn, changeCategory, auditStamp)); - ++baseIdx; - } else { - // EditableFieldInfo got added. - changeEvents.addAll(getAllChangeEvents(null, targetFieldInfo, entityUrn, changeCategory, auditStamp)); - ++targetIdx; - } - } + public static final String FIELD_DOCUMENTATION_ADDED_FORMAT = + "Documentation for the field '%s' of '%s' has been added: '%s'"; + public static final String FIELD_DOCUMENTATION_REMOVED_FORMAT = + "Documentation for the field '%s' of '%s' has been removed: '%s'"; + public static final String FIELD_DOCUMENTATION_UPDATED_FORMAT = + "Documentation for the field '%s' of '%s' has been updated from '%s' to '%s'."; + private static final Set SUPPORTED_CATEGORIES = + Stream.of(ChangeCategory.DOCUMENTATION, ChangeCategory.TAG, ChangeCategory.GLOSSARY_TERM) + .collect(Collectors.toSet()); - while (baseIdx < baseFieldInfos.size()) { - // Handle removed baseFieldInfo - EditableSchemaFieldInfo baseFieldInfo = baseFieldInfos.get(baseIdx); - changeEvents.addAll(getAllChangeEvents(baseFieldInfo, null, entityUrn, changeCategory, auditStamp)); - ++baseIdx; + private static void sortEditableSchemaMetadataByFieldPath(EditableSchemaMetadata editableSchemaMetadata) { + if (editableSchemaMetadata == null) { + return; + } + List editableSchemaFieldInfos = + new ArrayList<>(editableSchemaMetadata.getEditableSchemaFieldInfo()); + editableSchemaFieldInfos.sort(Comparator.comparing(EditableSchemaFieldInfo::getFieldPath)); + editableSchemaMetadata.setEditableSchemaFieldInfo(new EditableSchemaFieldInfoArray(editableSchemaFieldInfos)); } - while (targetIdx < targetFieldInfos.size()) { - // Handle newly added targetFieldInfo - EditableSchemaFieldInfo targetFieldInfo = targetFieldInfos.get(targetIdx); - changeEvents.addAll(getAllChangeEvents(null, targetFieldInfo, entityUrn, changeCategory, auditStamp)); - ++targetIdx; - } - return changeEvents; - } - private static EditableSchemaMetadata getEditableSchemaMetadataFromAspect(EntityAspect entityAspect) { - if (entityAspect != null && entityAspect.getMetadata() != null) { - return RecordUtils.toRecordTemplate(EditableSchemaMetadata.class, entityAspect.getMetadata()); - } - return null; - } - - private static ChangeEvent getDocumentationChangeEvent(EditableSchemaFieldInfo baseFieldInfo, - EditableSchemaFieldInfo targetFieldInfo, Urn datasetFieldUrn, AuditStamp auditStamp) { - String baseFieldDescription = (baseFieldInfo != null) ? baseFieldInfo.getDescription() : null; - String targetFieldDescription = (targetFieldInfo != null) ? targetFieldInfo.getDescription() : null; - - if (baseFieldDescription == null && targetFieldDescription != null) { - return ChangeEvent.builder() - .modifier(targetFieldInfo.getFieldPath()) - .entityUrn(datasetFieldUrn.toString()) - .category(ChangeCategory.DOCUMENTATION) - .operation(ChangeOperation.ADD) - .semVerChange(SemanticChangeType.MINOR) - .description(String.format(FIELD_DOCUMENTATION_ADDED_FORMAT, targetFieldInfo.getFieldPath(), datasetFieldUrn, - targetFieldDescription)) - .auditStamp(auditStamp) - .build(); + private static List getAllChangeEvents(EditableSchemaFieldInfo baseFieldInfo, + EditableSchemaFieldInfo targetFieldInfo, String entityUrn, ChangeCategory changeCategory, + AuditStamp auditStamp) { + List changeEvents = new ArrayList<>(); + Urn datasetFieldUrn = getDatasetFieldUrn(baseFieldInfo, targetFieldInfo, entityUrn); + if (changeCategory == ChangeCategory.DOCUMENTATION) { + ChangeEvent documentationChangeEvent = getDocumentationChangeEvent(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp); + if (documentationChangeEvent != null) { + changeEvents.add(documentationChangeEvent); + } + } + if (changeCategory == ChangeCategory.TAG) { + changeEvents.addAll(getTagChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); + } + if (changeCategory == ChangeCategory.GLOSSARY_TERM) { + changeEvents.addAll(getGlossaryTermChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); + } + if (changeCategory == ChangeCategory.BUSINESS_ATTRIBUTE) { + changeEvents.addAll(getBusinessAttributeAssociationChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); + } + + return changeEvents; } - if (baseFieldDescription != null && targetFieldDescription == null) { - return ChangeEvent.builder() - .modifier(baseFieldInfo.getFieldPath()) - .entityUrn(datasetFieldUrn.toString()) - .category(ChangeCategory.DOCUMENTATION) - .operation(ChangeOperation.REMOVE) - .semVerChange(SemanticChangeType.MINOR) - .description(String.format(FIELD_DOCUMENTATION_REMOVED_FORMAT, - Optional.ofNullable(targetFieldInfo).map(EditableSchemaFieldInfo::getFieldPath), - datasetFieldUrn, baseFieldDescription)) - .auditStamp(auditStamp) - .build(); + private static List computeDiffs(EditableSchemaMetadata baseEditableSchemaMetadata, + EditableSchemaMetadata targetEditableSchemaMetadata, + String entityUrn, ChangeCategory changeCategory, AuditStamp auditStamp) { + sortEditableSchemaMetadataByFieldPath(baseEditableSchemaMetadata); + sortEditableSchemaMetadataByFieldPath(targetEditableSchemaMetadata); + List changeEvents = new ArrayList<>(); + EditableSchemaFieldInfoArray baseFieldInfos = + (baseEditableSchemaMetadata != null) ? baseEditableSchemaMetadata.getEditableSchemaFieldInfo() + : new EditableSchemaFieldInfoArray(); + EditableSchemaFieldInfoArray targetFieldInfos = targetEditableSchemaMetadata.getEditableSchemaFieldInfo(); + int baseIdx = 0; + int targetIdx = 0; + while (baseIdx < baseFieldInfos.size() && targetIdx < targetFieldInfos.size()) { + EditableSchemaFieldInfo baseFieldInfo = baseFieldInfos.get(baseIdx); + EditableSchemaFieldInfo targetFieldInfo = targetFieldInfos.get(targetIdx); + int comparison = baseFieldInfo.getFieldPath().compareTo(targetFieldInfo.getFieldPath()); + if (comparison == 0) { + changeEvents.addAll(getAllChangeEvents(baseFieldInfo, targetFieldInfo, entityUrn, changeCategory, auditStamp)); + ++baseIdx; + ++targetIdx; + } else if (comparison < 0) { + // EditableFieldInfo got removed. + changeEvents.addAll(getAllChangeEvents(baseFieldInfo, null, entityUrn, changeCategory, auditStamp)); + ++baseIdx; + } else { + // EditableFieldInfo got added. + changeEvents.addAll(getAllChangeEvents(null, targetFieldInfo, entityUrn, changeCategory, auditStamp)); + ++targetIdx; + } + } + + while (baseIdx < baseFieldInfos.size()) { + // Handle removed baseFieldInfo + EditableSchemaFieldInfo baseFieldInfo = baseFieldInfos.get(baseIdx); + changeEvents.addAll(getAllChangeEvents(baseFieldInfo, null, entityUrn, changeCategory, auditStamp)); + ++baseIdx; + } + while (targetIdx < targetFieldInfos.size()) { + // Handle newly added targetFieldInfo + EditableSchemaFieldInfo targetFieldInfo = targetFieldInfos.get(targetIdx); + changeEvents.addAll(getAllChangeEvents(null, targetFieldInfo, entityUrn, changeCategory, auditStamp)); + ++targetIdx; + } + return changeEvents; } - if (baseFieldDescription != null && targetFieldDescription != null && !baseFieldDescription.equals( - targetFieldDescription)) { - return ChangeEvent.builder() - .modifier(targetFieldInfo.getFieldPath()) - .entityUrn(datasetFieldUrn.toString()) - .category(ChangeCategory.DOCUMENTATION) - .operation(ChangeOperation.MODIFY) - .semVerChange(SemanticChangeType.PATCH) - .description(String.format(FIELD_DOCUMENTATION_UPDATED_FORMAT, targetFieldInfo.getFieldPath(), datasetFieldUrn, - baseFieldDescription, targetFieldDescription)) - .auditStamp(auditStamp) - .build(); + private static EditableSchemaMetadata getEditableSchemaMetadataFromAspect(EntityAspect entityAspect) { + if (entityAspect != null && entityAspect.getMetadata() != null) { + return RecordUtils.toRecordTemplate(EditableSchemaMetadata.class, entityAspect.getMetadata()); + } + return null; } - return null; - } - - private static List getGlossaryTermChangeEvents(EditableSchemaFieldInfo baseFieldInfo, - EditableSchemaFieldInfo targetFieldInfo, Urn datasetFieldUrn, AuditStamp auditStamp) { - GlossaryTerms baseGlossaryTerms = (baseFieldInfo != null) ? baseFieldInfo.getGlossaryTerms() : null; - GlossaryTerms targetGlossaryTerms = (targetFieldInfo != null) ? targetFieldInfo.getGlossaryTerms() : null; - - // 1. Get EntityGlossaryTermChangeEvent, then rebind into a SchemaFieldGlossaryTermChangeEvent. - List entityGlossaryTermsChangeEvents = - GlossaryTermsChangeEventGenerator.computeDiffs(baseGlossaryTerms, targetGlossaryTerms, - datasetFieldUrn.toString(), auditStamp); - - if (targetFieldInfo != null || baseFieldInfo != null) { - String fieldPath = targetFieldInfo != null ? targetFieldInfo.getFieldPath() : baseFieldInfo.getFieldPath(); - // 2. Convert EntityGlossaryTermChangeEvent into a SchemaFieldGlossaryTermChangeEvent. - return convertEntityGlossaryTermChangeEvents( - fieldPath, - datasetFieldUrn, - entityGlossaryTermsChangeEvents); + private static ChangeEvent getDocumentationChangeEvent(EditableSchemaFieldInfo baseFieldInfo, + EditableSchemaFieldInfo targetFieldInfo, Urn datasetFieldUrn, AuditStamp auditStamp) { + String baseFieldDescription = (baseFieldInfo != null) ? baseFieldInfo.getDescription() : null; + String targetFieldDescription = (targetFieldInfo != null) ? targetFieldInfo.getDescription() : null; + + if (baseFieldDescription == null && targetFieldDescription != null) { + return ChangeEvent.builder() + .modifier(targetFieldInfo.getFieldPath()) + .entityUrn(datasetFieldUrn.toString()) + .category(ChangeCategory.DOCUMENTATION) + .operation(ChangeOperation.ADD) + .semVerChange(SemanticChangeType.MINOR) + .description(String.format(FIELD_DOCUMENTATION_ADDED_FORMAT, targetFieldInfo.getFieldPath(), datasetFieldUrn, + targetFieldDescription)) + .auditStamp(auditStamp) + .build(); + } + + if (baseFieldDescription != null && targetFieldDescription == null) { + return ChangeEvent.builder() + .modifier(baseFieldInfo.getFieldPath()) + .entityUrn(datasetFieldUrn.toString()) + .category(ChangeCategory.DOCUMENTATION) + .operation(ChangeOperation.REMOVE) + .semVerChange(SemanticChangeType.MINOR) + .description(String.format(FIELD_DOCUMENTATION_REMOVED_FORMAT, + Optional.ofNullable(targetFieldInfo).map(EditableSchemaFieldInfo::getFieldPath), + datasetFieldUrn, baseFieldDescription)) + .auditStamp(auditStamp) + .build(); + } + + if (baseFieldDescription != null && targetFieldDescription != null && !baseFieldDescription.equals( + targetFieldDescription)) { + return ChangeEvent.builder() + .modifier(targetFieldInfo.getFieldPath()) + .entityUrn(datasetFieldUrn.toString()) + .category(ChangeCategory.DOCUMENTATION) + .operation(ChangeOperation.MODIFY) + .semVerChange(SemanticChangeType.PATCH) + .description(String.format(FIELD_DOCUMENTATION_UPDATED_FORMAT, targetFieldInfo.getFieldPath(), datasetFieldUrn, + baseFieldDescription, targetFieldDescription)) + .auditStamp(auditStamp) + .build(); + } + + return null; } - return Collections.emptyList(); - } - - private static List getTagChangeEvents(EditableSchemaFieldInfo baseFieldInfo, - EditableSchemaFieldInfo targetFieldInfo, Urn datasetFieldUrn, AuditStamp auditStamp) { - GlobalTags baseGlobalTags = (baseFieldInfo != null) ? baseFieldInfo.getGlobalTags() : null; - GlobalTags targetGlobalTags = (targetFieldInfo != null) ? targetFieldInfo.getGlobalTags() : null; - - // 1. Get EntityTagChangeEvent, then rebind into a SchemaFieldTagChangeEvent. - List entityTagChangeEvents = - GlobalTagsChangeEventGenerator.computeDiffs(baseGlobalTags, targetGlobalTags, datasetFieldUrn.toString(), - auditStamp); - - if (targetFieldInfo != null || baseFieldInfo != null) { - String fieldPath = targetFieldInfo != null ? targetFieldInfo.getFieldPath() : baseFieldInfo.getFieldPath(); - // 2. Convert EntityTagChangeEvent into a SchemaFieldTagChangeEvent. - return convertEntityTagChangeEvents( - fieldPath, - datasetFieldUrn, - entityTagChangeEvents); + private static List getGlossaryTermChangeEvents(EditableSchemaFieldInfo baseFieldInfo, + EditableSchemaFieldInfo targetFieldInfo, Urn datasetFieldUrn, AuditStamp auditStamp) { + GlossaryTerms baseGlossaryTerms = (baseFieldInfo != null) ? baseFieldInfo.getGlossaryTerms() : null; + GlossaryTerms targetGlossaryTerms = (targetFieldInfo != null) ? targetFieldInfo.getGlossaryTerms() : null; + + // 1. Get EntityGlossaryTermChangeEvent, then rebind into a SchemaFieldGlossaryTermChangeEvent. + List entityGlossaryTermsChangeEvents = + GlossaryTermsChangeEventGenerator.computeDiffs(baseGlossaryTerms, targetGlossaryTerms, + datasetFieldUrn.toString(), auditStamp); + + if (targetFieldInfo != null || baseFieldInfo != null) { + String fieldPath = targetFieldInfo != null ? targetFieldInfo.getFieldPath() : baseFieldInfo.getFieldPath(); + // 2. Convert EntityGlossaryTermChangeEvent into a SchemaFieldGlossaryTermChangeEvent. + return convertEntityGlossaryTermChangeEvents( + fieldPath, + datasetFieldUrn, + entityGlossaryTermsChangeEvents); + } + + return Collections.emptyList(); } - return Collections.emptyList(); - } + private static List getTagChangeEvents(EditableSchemaFieldInfo baseFieldInfo, + EditableSchemaFieldInfo targetFieldInfo, Urn datasetFieldUrn, AuditStamp auditStamp) { + GlobalTags baseGlobalTags = (baseFieldInfo != null) ? baseFieldInfo.getGlobalTags() : null; + GlobalTags targetGlobalTags = (targetFieldInfo != null) ? targetFieldInfo.getGlobalTags() : null; - @Override - public ChangeTransaction getSemanticDiff(EntityAspect previousValue, EntityAspect currentValue, - ChangeCategory element, JsonPatch rawDiff, boolean rawDiffsRequested) { + // 1. Get EntityTagChangeEvent, then rebind into a SchemaFieldTagChangeEvent. + List entityTagChangeEvents = + GlobalTagsChangeEventGenerator.computeDiffs(baseGlobalTags, targetGlobalTags, datasetFieldUrn.toString(), + auditStamp); - if (currentValue == null) { - throw new IllegalArgumentException("EntityAspect currentValue should not be null"); + if (targetFieldInfo != null || baseFieldInfo != null) { + String fieldPath = targetFieldInfo != null ? targetFieldInfo.getFieldPath() : baseFieldInfo.getFieldPath(); + // 2. Convert EntityTagChangeEvent into a SchemaFieldTagChangeEvent. + return convertEntityTagChangeEvents( + fieldPath, + datasetFieldUrn, + entityTagChangeEvents); + } + + return Collections.emptyList(); } - if (!previousValue.getAspect().equals(EDITABLE_SCHEMA_METADATA_ASPECT_NAME) || !currentValue.getAspect() - .equals(EDITABLE_SCHEMA_METADATA_ASPECT_NAME)) { - throw new IllegalArgumentException("Aspect is not " + EDITABLE_SCHEMA_METADATA_ASPECT_NAME); + private static List getBusinessAttributeAssociationChangeEvents(EditableSchemaFieldInfo baseFieldInfo, + EditableSchemaFieldInfo targetFieldInfo, + Urn datasetFieldUrn, AuditStamp auditStamp) { + BusinessAttributeAssociation baseBusinessAttributeAssociation = (baseFieldInfo != null) ? baseFieldInfo.getBusinessAttribute() : null; + BusinessAttributeAssociation targetBusinessAttributeAssociation = (targetFieldInfo != null) ? targetFieldInfo.getBusinessAttribute() : null; + + // 1. Get EntityBusinessAttributeAssociationChangeEvent, then rebind into a SchemaFieldBusinessAttributeAssociationChangeEvent. + List entityBusinessAttributeAssociationChangeEvents = + BusinessAttributeAssociationChangeEventGenerator.computeDiffs(baseBusinessAttributeAssociation, + targetBusinessAttributeAssociation, datasetFieldUrn.toString(), + auditStamp); + + return entityBusinessAttributeAssociationChangeEvents; } - EditableSchemaMetadata baseEditableSchemaMetadata = getEditableSchemaMetadataFromAspect(previousValue); - EditableSchemaMetadata targetEditableSchemaMetadata = getEditableSchemaMetadataFromAspect(currentValue); - List changeEvents = new ArrayList<>(); - if (SUPPORTED_CATEGORIES.contains(element)) { - changeEvents.addAll( - computeDiffs(baseEditableSchemaMetadata, targetEditableSchemaMetadata, currentValue.getUrn(), element, null)); + @Override + public ChangeTransaction getSemanticDiff(EntityAspect previousValue, EntityAspect currentValue, + ChangeCategory element, JsonPatch rawDiff, boolean rawDiffsRequested) { + + if (currentValue == null) { + throw new IllegalArgumentException("EntityAspect currentValue should not be null"); + } + + if (!previousValue.getAspect().equals(EDITABLE_SCHEMA_METADATA_ASPECT_NAME) || !currentValue.getAspect() + .equals(EDITABLE_SCHEMA_METADATA_ASPECT_NAME)) { + throw new IllegalArgumentException("Aspect is not " + EDITABLE_SCHEMA_METADATA_ASPECT_NAME); + } + + EditableSchemaMetadata baseEditableSchemaMetadata = getEditableSchemaMetadataFromAspect(previousValue); + EditableSchemaMetadata targetEditableSchemaMetadata = getEditableSchemaMetadataFromAspect(currentValue); + List changeEvents = new ArrayList<>(); + if (SUPPORTED_CATEGORIES.contains(element)) { + changeEvents.addAll( + computeDiffs(baseEditableSchemaMetadata, targetEditableSchemaMetadata, currentValue.getUrn(), element, null)); + } + + // Assess the highest change at the transaction(schema) level. + SemanticChangeType highestSemanticChange = SemanticChangeType.NONE; + ChangeEvent highestChangeEvent = + changeEvents.stream().max(Comparator.comparing(ChangeEvent::getSemVerChange)).orElse(null); + if (highestChangeEvent != null) { + highestSemanticChange = highestChangeEvent.getSemVerChange(); + } + + return ChangeTransaction.builder() + .semVerChange(highestSemanticChange) + .changeEvents(changeEvents) + .timestamp(currentValue.getCreatedOn().getTime()) + .rawDiff(rawDiffsRequested ? rawDiff : null) + .actor(currentValue.getCreatedBy()) + .build(); } - // Assess the highest change at the transaction(schema) level. - SemanticChangeType highestSemanticChange = SemanticChangeType.NONE; - ChangeEvent highestChangeEvent = - changeEvents.stream().max(Comparator.comparing(ChangeEvent::getSemVerChange)).orElse(null); - if (highestChangeEvent != null) { - highestSemanticChange = highestChangeEvent.getSemVerChange(); + @Override + public List getChangeEvents( + @Nonnull Urn urn, + @Nonnull String entity, + @Nonnull String aspect, + @Nonnull Aspect from, + @Nonnull Aspect to, + @Nonnull AuditStamp auditStamp) { + final List changeEvents = new ArrayList<>(); + changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.DOCUMENTATION, auditStamp)); + changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.TAG, auditStamp)); + changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.TECHNICAL_SCHEMA, auditStamp)); + changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.GLOSSARY_TERM, auditStamp)); + changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.BUSINESS_ATTRIBUTE, auditStamp)); + return changeEvents; } - return ChangeTransaction.builder() - .semVerChange(highestSemanticChange) - .changeEvents(changeEvents) - .timestamp(currentValue.getCreatedOn().getTime()) - .rawDiff(rawDiffsRequested ? rawDiff : null) - .actor(currentValue.getCreatedBy()) - .build(); - } - - @Override - public List getChangeEvents( - @Nonnull Urn urn, - @Nonnull String entity, - @Nonnull String aspect, - @Nonnull Aspect from, - @Nonnull Aspect to, - @Nonnull AuditStamp auditStamp) { - final List changeEvents = new ArrayList<>(); - changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.DOCUMENTATION, auditStamp)); - changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.TAG, auditStamp)); - changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.TECHNICAL_SCHEMA, auditStamp)); - changeEvents.addAll(computeDiffs(from.getValue(), to.getValue(), urn.toString(), ChangeCategory.GLOSSARY_TERM, auditStamp)); - return changeEvents; - } - - private static Urn getDatasetFieldUrn(final EditableSchemaFieldInfo previous, final EditableSchemaFieldInfo latest, String entityUrn) { - return previous != null - ? getSchemaFieldUrn(UrnUtils.getUrn(entityUrn), previous.getFieldPath()) - : getSchemaFieldUrn(UrnUtils.getUrn(entityUrn), latest.getFieldPath()); - } + private static Urn getDatasetFieldUrn(final EditableSchemaFieldInfo previous, final EditableSchemaFieldInfo latest, String entityUrn) { + return previous != null + ? getSchemaFieldUrn(UrnUtils.getUrn(entityUrn), previous.getFieldPath()) + : getSchemaFieldUrn(UrnUtils.getUrn(entityUrn), latest.getFieldPath()); + } } diff --git a/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java b/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java index 3b65ecccad3368..6358659c6c6a1a 100644 --- a/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java +++ b/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java @@ -62,6 +62,7 @@ public class EntityChangeEventGeneratorHook implements MetadataChangeLogHook { Constants.EDITABLE_DATASET_PROPERTIES_ASPECT_NAME, Constants.ASSERTION_RUN_EVENT_ASPECT_NAME, Constants.DATA_PROCESS_INSTANCE_RUN_EVENT_ASPECT_NAME, + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, // Entity Lifecycle Event Constants.DATASET_KEY_ASPECT_NAME, diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java index 89a7e7dd8d71a8..500954aedcfee9 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java @@ -3,6 +3,7 @@ import com.datahub.authentication.Authentication; import com.linkedin.entity.client.SystemRestliEntityClient; import com.linkedin.metadata.timeline.eventgenerator.AssertionRunEventChangeEventGenerator; +import com.linkedin.metadata.timeline.eventgenerator.BusinessAttributeAssociationChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.DataProcessInstanceRunEventChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.DatasetPropertiesChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.GlossaryTermInfoChangeEventGenerator; @@ -17,6 +18,7 @@ import com.linkedin.metadata.timeline.eventgenerator.SchemaMetadataChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.SingleDomainChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.StatusChangeEventGenerator; +import com.linkedin.metadata.timeline.eventgenerator.BusinessAttributeInfoChangeEventGenerator; import javax.annotation.Nonnull; import javax.inject.Singleton; import org.springframework.beans.factory.annotation.Autowired; @@ -54,6 +56,8 @@ protected com.linkedin.metadata.timeline.eventgenerator.EntityChangeEventGenerat registry.register(DOMAINS_ASPECT_NAME, new SingleDomainChangeEventGenerator()); registry.register(DATASET_PROPERTIES_ASPECT_NAME, new DatasetPropertiesChangeEventGenerator()); registry.register(EDITABLE_DATASET_PROPERTIES_ASPECT_NAME, new EditableDatasetPropertiesChangeEventGenerator()); + registry.register(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, new BusinessAttributeInfoChangeEventGenerator()); + registry.register(BUSINESS_ATTRIBUTE_ASSOCIATION, new BusinessAttributeAssociationChangeEventGenerator()); // Entity Lifecycle Differs registry.register(DATASET_KEY_ASPECT_NAME, new EntityKeyChangeEventGenerator<>()); diff --git a/metadata-service/services/src/main/java/com/linkedin/metadata/timeline/data/ChangeCategory.java b/metadata-service/services/src/main/java/com/linkedin/metadata/timeline/data/ChangeCategory.java index 72218c37fe5cef..cb2488b3e092f9 100644 --- a/metadata-service/services/src/main/java/com/linkedin/metadata/timeline/data/ChangeCategory.java +++ b/metadata-service/services/src/main/java/com/linkedin/metadata/timeline/data/ChangeCategory.java @@ -24,7 +24,9 @@ public enum ChangeCategory { // Entity Lifecycle events (create, soft delete, hard delete) LIFECYCLE, // Run event - RUN; + RUN, + + BUSINESS_ATTRIBUTE; public static final Map, ChangeCategory> COMPOUND_CATEGORIES; From aaf5182be772da9c035affdcb60a6251fe3e99ba Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Thu, 1 Feb 2024 13:54:41 +0530 Subject: [PATCH 15/50] businessattribute: metadata access management for Business Attribute --- .../AddBusinessAttributeResolver.java | 6 +++++- .../BusinessAttributeAuthorizationUtils.java | 17 +++++++++++++++++ .../RemoveBusinessAttributeResolver.java | 6 +++++- .../resolvers/config/AppConfigResolver.java | 2 ++ .../resolvers/mutate/UpdateNameResolver.java | 6 +++++- .../graphql/types/dataset/DatasetType.java | 1 + .../authorization/PoliciesConfig.java | 19 +++++++++++++++++-- 7 files changed, 52 insertions(+), 5 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java index ee05fbda70b194..d07e858dcde15d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java @@ -5,6 +5,7 @@ import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.ResourceRefInput; import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils; @@ -24,6 +25,7 @@ import java.util.concurrent.CompletableFuture; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; @@ -38,7 +40,9 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); ResourceRefInput resourceRefInput = input.getResourceUrn(); - //TODO: add authorization check + if (!isAuthorizeToUpdateDataset(context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java index 24e60a5aee7679..c30e26a3bf9e66 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java @@ -3,12 +3,15 @@ import com.datahub.authorization.ConjunctivePrivilegeGroup; import com.datahub.authorization.DisjunctivePrivilegeGroup; 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.metadata.authorization.PoliciesConfig; import javax.annotation.Nonnull; +import static com.linkedin.datahub.graphql.resolvers.AuthUtils.ALL_PRIVILEGES_GROUP; + public class BusinessAttributeAuthorizationUtils { private BusinessAttributeAuthorizationUtils() { @@ -37,4 +40,18 @@ public static boolean canManageBusinessAttribute(@Nonnull QueryContext context) context.getActorUrn(), orPrivilegeGroups); } + + public static boolean isAuthorizeToUpdateDataset(QueryContext context, Urn targetUrn) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( + ALL_PRIVILEGES_GROUP, + new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())) + )); + + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + targetUrn.getEntityType(), + targetUrn.toString(), + orPrivilegeGroups); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java index f497d63fbd6eca..86028869d0715e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java @@ -3,6 +3,7 @@ import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.ResourceRefInput; import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils; @@ -22,6 +23,7 @@ import java.util.concurrent.CompletableFuture; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; @@ -38,7 +40,9 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); ResourceRefInput resourceRefInput = input.getResourceUrn(); - //TODO: add authorization check + if (!isAuthorizeToUpdateDataset(context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java index f6bc68caa0821c..d01717e4ae1285 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java @@ -226,6 +226,8 @@ private EntityType mapResourceTypeToEntityType(final String resourceType) { return EntityType.CORP_GROUP; } else if (com.linkedin.metadata.authorization.PoliciesConfig.CORP_USER_PRIVILEGES.getResourceType().equals(resourceType)) { return EntityType.CORP_USER; + } else if (com.linkedin.metadata.authorization.PoliciesConfig.BUSINESS_ATTRIBUTE_PRIVILEGES.getResourceType().equals(resourceType)) { + return EntityType.BUSINESS_ATTRIBUTE; } else { return null; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java index 9bcfc7d5ee55be..a7c766b5b713e2 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java @@ -10,6 +10,7 @@ import com.linkedin.datahub.graphql.exception.DataHubGraphQLErrorCode; import com.linkedin.datahub.graphql.exception.DataHubGraphQLException; import com.linkedin.datahub.graphql.generated.UpdateNameInput; +import com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils; import com.linkedin.datahub.graphql.resolvers.dataproduct.DataProductAuthorizationUtils; import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; import com.linkedin.datahub.graphql.resolvers.mutate.util.DomainUtils; @@ -18,8 +19,8 @@ import com.linkedin.domain.DomainProperties; import com.linkedin.domain.Domains; import com.linkedin.entity.client.EntityClient; -import com.linkedin.glossary.GlossaryTermInfo; import com.linkedin.glossary.GlossaryNodeInfo; +import com.linkedin.glossary.GlossaryTermInfo; import com.linkedin.identity.CorpGroupInfo; import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.EntityService; @@ -225,6 +226,9 @@ private Boolean updateBusinessAttributeName( UpdateNameInput input, QueryContext context ) { + if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } try { BusinessAttributeInfo businessAttributeInfo = (BusinessAttributeInfo) EntityUtils.getAspectFromEntity( targetUrn.toString(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, _entityService, null); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java index 0fc4399ac902d7..d4bb1b14407735 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java @@ -276,6 +276,7 @@ private DisjunctivePrivilegeGroup getAuthorizedPrivileges(final DatasetUpdateInp if (updateInput.getEditableSchemaMetadata() != null) { specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_TAGS_PRIVILEGE.getType()); specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_DESCRIPTION_PRIVILEGE.getType()); + specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType()); } final ConjunctivePrivilegeGroup specificPrivilegeGroup = new ConjunctivePrivilegeGroup(specificPrivileges); diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index a47617dd7dd9cf..4d5565e537dd0c 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -380,6 +380,12 @@ public class PoliciesConfig { "Produce Platform Event API", "The ability to produce Platform Events using the API."); + public static final Privilege EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE = Privilege.of( + "EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE", + "Edit Dataset Column Business Attribute", + "The ability to edit the column (field) business attribute associated with a dataset schema." + ); + public static final ResourcePrivileges DATASET_PRIVILEGES = ResourcePrivileges.of( "dataset", "Datasets", @@ -394,7 +400,7 @@ public class PoliciesConfig { EDIT_ENTITY_ASSERTIONS_PRIVILEGE, EDIT_LINEAGE_PRIVILEGE, EDIT_ENTITY_EMBED_PRIVILEGE, - EDIT_QUERIES_PRIVILEGE)) + EDIT_QUERIES_PRIVILEGE, EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE)) .flatMap(Collection::stream) .collect(Collectors.toList()) ); @@ -547,6 +553,14 @@ public class PoliciesConfig { EDIT_ENTITY_PRIVILEGE) ); + public static final ResourcePrivileges BUSINESS_ATTRIBUTE_PRIVILEGES = ResourcePrivileges.of( + "businessAttribute", + "Business Attribute", + "Business Attribute created on Datahub", + ImmutableList.of(VIEW_ENTITY_PAGE_PRIVILEGE, EDIT_ENTITY_OWNERS_PRIVILEGE, EDIT_ENTITY_DOCS_PRIVILEGE, EDIT_ENTITY_TAGS_PRIVILEGE, + EDIT_ENTITY_GLOSSARY_TERMS_PRIVILEGE) + ); + public static final List ENTITY_RESOURCE_PRIVILEGES = ImmutableList.of( DATASET_PRIVILEGES, DASHBOARD_PRIVILEGES, @@ -561,7 +575,8 @@ public class PoliciesConfig { CORP_GROUP_PRIVILEGES, CORP_USER_PRIVILEGES, NOTEBOOK_PRIVILEGES, - DATA_PRODUCT_PRIVILEGES + DATA_PRODUCT_PRIVILEGES, + BUSINESS_ATTRIBUTE_PRIVILEGES ); // Merge all entity specific resource privileges to create a superset of all resource privileges From 24baf28f4ce2c3b9349e505cba90220f4a04fe75 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Fri, 2 Feb 2024 19:09:59 +0530 Subject: [PATCH 16/50] businessattribute: modifify policies.json --- .../war/src/main/resources/boot/policies.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/metadata-service/war/src/main/resources/boot/policies.json b/metadata-service/war/src/main/resources/boot/policies.json index 32e68e7b133430..68d6807a2ddc23 100644 --- a/metadata-service/war/src/main/resources/boot/policies.json +++ b/metadata-service/war/src/main/resources/boot/policies.json @@ -32,7 +32,9 @@ "SET_WRITEABLE_PRIVILEGE", "APPLY_RETENTION_PRIVILEGE", "MANAGE_GLOBAL_OWNERSHIP_TYPES", - "GET_ANALYTICS_PRIVILEGE" + "GET_ANALYTICS_PRIVILEGE", + "CREATE_BUSINESS_ATTRIBUTE", + "MANAGE_BUSINESS_ATTRIBUTE" ], "displayName":"Root User - All Platform Privileges", "description":"Grants full platform privileges to root datahub super user.", @@ -173,7 +175,9 @@ "SET_WRITEABLE_PRIVILEGE", "APPLY_RETENTION_PRIVILEGE", "MANAGE_GLOBAL_OWNERSHIP_TYPES", - "GET_ANALYTICS_PRIVILEGE" + "GET_ANALYTICS_PRIVILEGE", + "CREATE_BUSINESS_ATTRIBUTE", + "MANAGE_BUSINESS_ATTRIBUTE" ], "displayName":"Admins - Platform Policy", "description":"Admins have all platform privileges.", @@ -211,6 +215,7 @@ "EDIT_DATASET_COL_TAGS", "EDIT_DATASET_COL_GLOSSARY_TERMS", "EDIT_DATASET_COL_DESCRIPTION", + "EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE", "VIEW_DATASET_USAGE", "VIEW_DATASET_PROFILE", "EDIT_TAG_COLOR", @@ -253,7 +258,8 @@ "MANAGE_DOMAINS", "MANAGE_GLOBAL_ANNOUNCEMENTS", "MANAGE_GLOSSARIES", - "MANAGE_TAGS" + "MANAGE_TAGS", + "MANAGE_BUSINESS_ATTRIBUTE" ], "displayName":"Editors - Platform Policy", "description":"Editors can manage ingestion and view analytics.", @@ -289,6 +295,7 @@ "EDIT_DATASET_COL_TAGS", "EDIT_DATASET_COL_GLOSSARY_TERMS", "EDIT_DATASET_COL_DESCRIPTION", + "EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE", "VIEW_DATASET_USAGE", "VIEW_DATASET_PROFILE", "EDIT_TAG_COLOR", @@ -434,6 +441,7 @@ "EDIT_DATASET_COL_TAGS", "EDIT_DATASET_COL_GLOSSARY_TERMS", "EDIT_DATASET_COL_DESCRIPTION", + "EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE", "VIEW_DATASET_USAGE", "VIEW_DATASET_PROFILE", "EDIT_TAG_COLOR", From 8bc96690c290ac136c38f1d928a91996e93e85f5 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Fri, 2 Feb 2024 23:51:36 +0530 Subject: [PATCH 17/50] businessattribute: generate lifecycle platform events --- .../businessattribute/mappers/BusinessAttributesMapper.java | 2 -- .../dataset/mappers/EditableSchemaFieldInfoMapper.java | 2 -- .../BusinessAttributeInfoChangeEventGenerator.java | 6 +++++- .../kafka/hook/event/EntityChangeEventGeneratorHook.java | 3 ++- .../timeline/EntityChangeEventGeneratorRegistryFactory.java | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java index 00e850517212da..71f5390d139015 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java @@ -19,12 +19,10 @@ public static BusinessAttributes map( @Nonnull final com.linkedin.businessattribute.BusinessAttributeAssociation businessAttribute, @Nonnull final Urn entityUrn ) { - _logger.info("inside mapper"); return INSTANCE.apply(businessAttribute, entityUrn); } private BusinessAttributes apply(@Nonnull com.linkedin.businessattribute.BusinessAttributeAssociation businessAttributes, @Nonnull Urn entityUrn) { - _logger.info("before try block::{}", businessAttributes.getDestinationUrn()); final BusinessAttributeAssociation businessAttributeAssociation = new BusinessAttributeAssociation(); final BusinessAttributes result = new BusinessAttributes(); final BusinessAttribute businessAttribute = new BusinessAttribute(); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java index 3ad74bacd1e827..eb73dc558b341c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java @@ -41,9 +41,7 @@ public com.linkedin.datahub.graphql.generated.EditableSchemaFieldInfo apply( if (input.hasGlossaryTerms()) { result.setGlossaryTerms(GlossaryTermsMapper.map(input.getGlossaryTerms(), entityUrn)); } - _logger.info("inside info mapper before"); if (input.hasBusinessAttribute()) { - _logger.info("inside info mapper after: {}, entity urn: {}", input.getBusinessAttribute().getDestinationUrn(), entityUrn); result.setBusinessAttributes(BusinessAttributesMapper.map(input.getBusinessAttribute(), entityUrn)); } return result; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java index dd31629ff116e3..5c4abde5c1e2b2 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java @@ -12,6 +12,7 @@ import javax.annotation.Nonnull; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class BusinessAttributeInfoChangeEventGenerator extends EntityChangeEventGenerator { @@ -89,13 +90,16 @@ private List getTagChangeEvents(BusinessAttributeInfo baseBusinessA private ChangeEvent createChangeEvent(BusinessAttributeInfo businessAttributeInfo, String entityUrn, ChangeOperation operation, String format, AuditStamp auditStamp, String... descriptions) { + List args = new ArrayList<>(); + args.add(0, businessAttributeInfo.getFieldPath()); + Arrays.stream(descriptions).forEach(val -> args.add(val)); return ChangeEvent.builder() .modifier(businessAttributeInfo.getFieldPath()) .entityUrn(entityUrn) .category(ChangeCategory.DOCUMENTATION) .operation(operation) .semVerChange(SemanticChangeType.MINOR) - .description(String.format(format, businessAttributeInfo.getFieldPath(), descriptions)) + .description(String.format(format, args.toArray())) .auditStamp(auditStamp) .build(); } diff --git a/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java b/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java index 6358659c6c6a1a..4bd1c9788085df 100644 --- a/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java +++ b/metadata-jobs/mae-consumer/src/main/java/com/linkedin/metadata/kafka/hook/event/EntityChangeEventGeneratorHook.java @@ -74,7 +74,8 @@ public class EntityChangeEventGeneratorHook implements MetadataChangeLogHook { Constants.GLOSSARY_TERM_KEY_ASPECT_NAME, Constants.DOMAIN_KEY_ASPECT_NAME, Constants.TAG_KEY_ASPECT_NAME, - Constants.STATUS_ASPECT_NAME); + Constants.STATUS_ASPECT_NAME, + Constants.BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME); /** * The list of change types that are supported for generating semantic change events. */ diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java index 500954aedcfee9..90e86a27483f18 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/EntityChangeEventGeneratorRegistryFactory.java @@ -72,6 +72,7 @@ protected com.linkedin.metadata.timeline.eventgenerator.EntityChangeEventGenerat registry.register(CORP_GROUP_KEY_ASPECT_NAME, new EntityKeyChangeEventGenerator<>()); registry.register(STATUS_ASPECT_NAME, new StatusChangeEventGenerator()); registry.register(DEPRECATION_ASPECT_NAME, new DeprecationChangeEventGenerator()); + registry.register(BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME, new EntityKeyChangeEventGenerator<>()); // Assertion differs registry.register(ASSERTION_RUN_EVENT_ASPECT_NAME, new AssertionRunEventChangeEventGenerator()); From 249adeca9762a77dd0e08badd891d2edc6b66718 Mon Sep 17 00:00:00 2001 From: aditigup Date: Mon, 5 Feb 2024 15:57:40 +0530 Subject: [PATCH 18/50] Cypress Test Cases, Preview Test Case, updating delete BA api, Removing deleted BA from dataset --- .../RemoveBusinessAttributeResolver.java | 8 +- datahub-web-react/src/Mocks.tsx | 141 ++++++++++++++---- .../BusinessAttributeItemMenu.tsx | 6 +- .../preview/_tests_/Preview.test.tsx | 26 ++++ .../BusinessAttributeDataTypeSection.tsx | 13 +- .../utils/test-utils/TestPageContainer.tsx | 2 + .../businessAttribute/attribute_mutations.js | 78 ++++++++++ .../businessAttribute/businessAttribute.js | 117 +++++++++++++++ .../tests/cypress/cypress/e2e/home/home.js | 3 +- .../cypress/e2e/mutations/mutations.js | 26 ++++ .../tests/cypress/cypress/support/commands.js | 46 ++++++ smoke-test/tests/cypress/data.json | 54 ++++++- 12 files changed, 483 insertions(+), 37 deletions(-) create mode 100644 datahub-web-react/src/app/entity/businessAttribute/preview/_tests_/Preview.test.tsx create mode 100644 smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js create mode 100644 smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java index 86028869d0715e..8a3d936fc1250e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java @@ -43,11 +43,13 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw if (!isAuthorizeToUpdateDataset(context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); } - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); - } return CompletableFuture.supplyAsync(() -> { try { + if (!businessAttributeUrn.getEntityType().equals("businessAttribute")) { + log.error("Failed to remove {}. It is not a business attribute urn.", businessAttributeUrn.toString()); + return false; + } + validateInputResource(resourceRefInput, context); removeBusinessAttribute(resourceRefInput, context); diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 72eb6176bd4f50..02bce714ae23f8 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -1,44 +1,45 @@ -import { GetDatasetDocument, UpdateDatasetDocument, GetDatasetSchemaDocument } from './graphql/dataset.generated'; -import { GetDataFlowDocument } from './graphql/dataFlow.generated'; -import { GetDataJobDocument } from './graphql/dataJob.generated'; -import { GetBrowsePathsDocument, GetBrowseResultsDocument } from './graphql/browse.generated'; +import {GetDatasetDocument, GetDatasetSchemaDocument, UpdateDatasetDocument} from './graphql/dataset.generated'; +import {GetDataFlowDocument} from './graphql/dataFlow.generated'; +import {GetDataJobDocument} from './graphql/dataJob.generated'; +import {GetBrowsePathsDocument, GetBrowseResultsDocument} from './graphql/browse.generated'; import { - GetAutoCompleteResultsDocument, GetAutoCompleteMultipleResultsDocument, + GetAutoCompleteResultsDocument, GetSearchResultsDocument, - GetSearchResultsQuery, GetSearchResultsForMultipleDocument, GetSearchResultsForMultipleQuery, + GetSearchResultsQuery, } from './graphql/search.generated'; -import { GetUserDocument } from './graphql/user.generated'; +import {GetUserDocument} from './graphql/user.generated'; import { - Dataset, + AppConfig, + BusinessAttribute, + Container, DataFlow, DataJob, - GlossaryTerm, - GlossaryNode, + Dataset, EntityType, - PlatformType, + FilterOperator, + GlossaryNode, + GlossaryTerm, MlModel, MlModelGroup, - SchemaFieldDataType, - ScenarioType, + PlatformPrivileges, + PlatformType, RecommendationRenderType, RelationshipDirection, - Container, - PlatformPrivileges, - FilterOperator, - AppConfig, + ScenarioType, + SchemaFieldDataType, } from './types.generated'; -import { GetTagDocument } from './graphql/tag.generated'; -import { GetMlModelDocument } from './graphql/mlModel.generated'; -import { GetMlModelGroupDocument } from './graphql/mlModelGroup.generated'; -import { GetGlossaryTermDocument, GetGlossaryTermQuery } from './graphql/glossaryTerm.generated'; -import { GetEntityCountsDocument, AppConfigDocument } from './graphql/app.generated'; -import { GetMeDocument } from './graphql/me.generated'; -import { ListRecommendationsDocument } from './graphql/recommendations.generated'; -import { FetchedEntity } from './app/lineage/types'; -import { DEFAULT_APP_CONFIG } from './appConfigContext'; +import {GetTagDocument} from './graphql/tag.generated'; +import {GetMlModelDocument} from './graphql/mlModel.generated'; +import {GetMlModelGroupDocument} from './graphql/mlModelGroup.generated'; +import {GetGlossaryTermDocument, GetGlossaryTermQuery} from './graphql/glossaryTerm.generated'; +import {AppConfigDocument, GetEntityCountsDocument} from './graphql/app.generated'; +import {GetMeDocument} from './graphql/me.generated'; +import {ListRecommendationsDocument} from './graphql/recommendations.generated'; +import {FetchedEntity} from './app/lineage/types'; +import {DEFAULT_APP_CONFIG} from './appConfigContext'; export const user1 = { username: 'sdas', @@ -1321,6 +1322,92 @@ export const dataJob1 = { deprecation: null, } as DataJob; +export const businessAttribute = { + urn: 'urn:li:businessAttribute:ba1', + type: EntityType.BusinessAttribute, + __typename: 'BusinessAttribute', + properties: { + name: 'TestBusinessAtt-2', + description: 'lorem upsum updated 12', + created: { + time: 1705857132786 + }, + lastModified: { + time: 1705857132786 + }, + glossaryTerms: { + terms: [ + { + term: { + urn: 'urn:li:glossaryTerm:1' + }, + associatedUrn: 'urn:li:businessAttribute:ba1' + } + ], + __typename: 'GlossaryTerms', + }, + tags: { + __typename: 'GlobalTags', + tags: [ + { + tag: { + urn: 'urn:li:tag:abc-sample-tag', + __typename: 'Tag' + }, + __typename: 'TagAssociation', + associatedUrn: 'urn:li:businessAttribute:ba1' + }, + { + tag: { + urn: 'urn:li:tag:TestTag', + __typename: 'Tag' + }, + __typename: 'TagAssociation', + associatedUrn: 'urn:li:businessAttribute:ba1' + } + ] + }, + customProperties: [ + { + key: 'prop2', + value: 'val2', + __typename: 'CustomPropertiesEntry' + }, + { + key: 'prop1', + value: 'val1', + __typename: 'CustomPropertiesEntry' + }, + { + key: 'prop3', + value: 'val3', + __typename: 'CustomPropertiesEntry' + } + ] + }, + ownership: { + owners: [ + { + owner: { + ...user1, + }, + associatedUrn: 'urn:li:businessAttribute:ba', + type: 'DATAOWNER', + }, + { + owner: { + ...user2, + }, + associatedUrn: 'urn:li:businessAttribute:ba', + type: 'DELEGATE', + }, + ], + lastModified: { + time: 0, + }, + }, +} as BusinessAttribute; + export const dataJob2 = { __typename: 'DataJob', urn: 'urn:li:dataJob:2', @@ -1686,7 +1773,7 @@ export const recommendationModules = [ ]; /* - Define mock data to be returned by Apollo MockProvider. + Define mock data to be returned by Apollo MockProvider. */ export const mocks = [ { diff --git a/datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx b/datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx index ae306998910daa..4e56d81203b6f5 100644 --- a/datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx +++ b/datahub-web-react/src/app/businessAttribute/BusinessAttributeItemMenu.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { DeleteOutlined } from '@ant-design/icons'; import { Dropdown, Menu, message, Modal } from 'antd'; import { MenuIcon } from '../entity/shared/EntityDropdown/EntityDropdown'; -import { useDeletePostMutation } from '../../graphql/post.generated'; +import { useDeleteBusinessAttributeMutation } from '../../graphql/businessAttribute.generated'; type Props = { urn: string; @@ -11,10 +11,10 @@ type Props = { }; export default function BusinessAttributeItemMenu({ title, urn, onDelete }: Props) { - const [deletePostMutation] = useDeletePostMutation(); + const [deleteBusinessAttributeMutation] = useDeleteBusinessAttributeMutation(); const deletePost = () => { - deletePostMutation({ + deleteBusinessAttributeMutation({ variables: { urn, }, diff --git a/datahub-web-react/src/app/entity/businessAttribute/preview/_tests_/Preview.test.tsx b/datahub-web-react/src/app/entity/businessAttribute/preview/_tests_/Preview.test.tsx new file mode 100644 index 00000000000000..51a6db654129f9 --- /dev/null +++ b/datahub-web-react/src/app/entity/businessAttribute/preview/_tests_/Preview.test.tsx @@ -0,0 +1,26 @@ +import {MockedProvider} from '@apollo/client/testing'; +import {render} from '@testing-library/react'; +import React from 'react'; +import {mocks} from '../../../../../Mocks'; +import TestPageContainer from '../../../../../utils/test-utils/TestPageContainer'; +import {Preview} from '../Preview'; +import {PreviewType} from "../../../Entity"; + +describe('Preview', () => { + it('renders', () => { + const { getByText } = render( + + + + + , + ); + expect(getByText('definition')).toBeInTheDocument(); + }); +}); diff --git a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx index 0b90665fe3a3b1..db7204abfd9336 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx @@ -67,7 +67,12 @@ export const BusinessAttributeDataTypeSection = ({ readOnly }: Props) => { actions={ originalDescription && !readOnly && ( - ) @@ -75,7 +80,11 @@ export const BusinessAttributeDataTypeSection = ({ readOnly }: Props) => { /> {originalDescription} {isEditing && ( - + {DATA_TYPES.map((dataType: SchemaFieldDataType) => ( {dataType} diff --git a/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx b/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx index 0903aeeaf4fe5b..1d8db5f3994224 100644 --- a/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx +++ b/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx @@ -25,6 +25,7 @@ import UserContextProvider from '../../app/context/UserContextProvider'; import { DataPlatformEntity } from '../../app/entity/dataPlatform/DataPlatformEntity'; import { ContainerEntity } from '../../app/entity/container/ContainerEntity'; import AppConfigProvider from '../../AppConfigProvider'; +import {BusinessAttributeEntity} from "../../app/entity/businessAttribute/BusinessAttributeEntity"; type Props = { children: React.ReactNode; @@ -47,6 +48,7 @@ export function getTestEntityRegistry() { entityRegistry.register(new MLModelGroupEntity()); entityRegistry.register(new DataPlatformEntity()); entityRegistry.register(new ContainerEntity()); + entityRegistry.register(new BusinessAttributeEntity()); return entityRegistry; } diff --git a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js new file mode 100644 index 00000000000000..4b4faaf607e8fb --- /dev/null +++ b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js @@ -0,0 +1,78 @@ +describe("attribute list adding tags and terms", () => { + it("can create and add a tag to business attribute and visit new tag page", () => { + cy.login(); + cy.goToBusinessAttributeList(); + + cy.mouseover('[data-testid="schema-field-cypressTestAttribute-tags"]'); + cy.get('[data-testid="schema-field-cypressTestAttribute-tags"]').within(() => + cy.contains("Add Tags").click() + ); + + cy.enterTextInTestId("tag-term-modal-input", "CypressAddTagToAttribute"); + + cy.contains("Create CypressAddTagToAttribute").click({ force: true }); + + cy.get("textarea").type("CypressAddTagToAttribute Test Description"); + + cy.contains(/Create$/).click({ force: true }); + + // wait a breath for elasticsearch to index the tag being applied to the business attribute- if we navigate too quick ES + // wont know and we'll see applied to 0 entities + cy.wait(2000); + + // go to tag drawer + cy.contains("CypressAddTagToAttribute").click({ force: true }); + + cy.wait(1000); + + // Click the Tag Details to launch full profile + cy.contains("Tag Details").click({ force: true }); + + cy.wait(1000); + + // title of tag page + cy.contains("CypressAddTagToAttribute"); + + // description of tag page + cy.contains("CypressAddTagToAttribute Test Description"); + + // used by panel - click to search + cy.contains("1 Business Attributes").click({ force: true }); + + // verify business attribute shows up in search now + cy.contains("of 1 result").click({ force: true }); + cy.contains("cypressTestAttribute").click({ force: true }); + cy.get('[data-testid="tag-CypressAddTagToAttribute"]').within(() => + cy.get("span[aria-label=close]").click() + ); + cy.contains("Yes").click(); + + cy.contains("CypressAddTagToAttribute").should("not.exist"); + + cy.goToTag("urn:li:tag:CypressAddTagToAttribute", "CypressAddTagToAttribute"); + cy.deleteFromDropdown(); + + }); + + it("can add and remove terms from a business attribute", () => { + cy.login(); + cy.addTermToBusinessAttribute( + "urn:li:businessAttribute:cypressTestAttribute", + "cypressTestAttribute", + "CypressTerm" + ) + + cy.goToBusinessAttributeList(); + cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').contains("CypressTerm"); + + cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + cy.contains("Yes").click({ force: true }); + + cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').contains("CypressTerm").should("not.exist"); + }); +}); diff --git a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js new file mode 100644 index 00000000000000..3c8ec3a87c4bd1 --- /dev/null +++ b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js @@ -0,0 +1,117 @@ +describe("businessAttribute", () => { + it('go to business attribute page, create attribute ', function () { + const urn="urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; + const businessAttribute="CypressBusinessAttribute"; + const datasetName = "cypress_logging_events"; + cy.login(); + cy.goToBusinessAttributeList(); + + cy.clickOptionWithText("Create Business Attribute"); + cy.addViaModal(businessAttribute, "Create Business Attribute"); + + cy.wait(3000); + cy.goToBusinessAttributeList().contains(businessAttribute).should("be.visible"); + + cy.addAttributeToDataset(urn, datasetName, businessAttribute); + + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + cy.contains("Yes").click({ force: true }); + + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').contains("CypressBusinessAttribute").should("not.exist"); + + cy.goToBusinessAttributeList(); + cy.clickOptionWithText(businessAttribute); + cy.deleteFromDropdown(); + + cy.goToBusinessAttributeList(); + cy.ensureTextNotPresent(businessAttribute); + }); + + it('Inheriting tags and terms from business attribute to dataset ', function () { + const urn="urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; + const businessAttribute="CypressAttribute"; + const datasetName = "cypress_logging_events"; + const term="CypressTerm"; + const tag="Cypress"; + + cy.login(); + + cy.addAttributeToDataset(urn, datasetName, businessAttribute); + cy.contains(term); + cy.contains(tag); + + }); + + it("can visit related entities", () => { + const businessAttribute="CypressAttribute"; + cy.login(); + cy.goToBusinessAttributeList(); + cy.clickOptionWithText(businessAttribute); + cy.clickOptionWithText("Related Entities"); + //cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); + //cy.wait(5000); + cy.contains("of 0").should("not.exist"); + cy.contains(/of [0-9]+/); + }); + + + it("can search related entities by query", () => { + cy.login(); + cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); + cy.get('[placeholder="Filter entities..."]').click().type( + "logging{enter}" + ); + cy.wait(5000); + cy.contains("of 0").should("not.exist"); + cy.contains(/of 1/); + cy.contains("cypress_logging_events"); + }); + + it("remove business attribute from dataset", () => { + const urn="urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; + const datasetName = "cypress_logging_events"; + cy.login(); + cy.goToDataset(urn, datasetName); + + cy.wait(3000); + + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + cy.contains("Yes").click({ force: true }); + + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').contains("CypressAttribute").should("not.exist"); + }); + + it("update the data type of a business attribute", () => { + const businessAttribute="cypressTestAttribute"; + cy.login(); + cy.goToBusinessAttributeList(); + + cy.clickOptionWithText(businessAttribute); + + cy.get('[data-testid="edit-data-type-button"]').within(() => + cy + .get("span[aria-label=edit]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + + cy.get('[data-testid="add-data-type-option"]').get('.ant-select-selection-search-input').click({multiple: true}); + + cy.get('.ant-select-item-option-content') + .contains('STRING') + .click(); + + cy.contains("STRING"); + + }); +}); diff --git a/smoke-test/tests/cypress/cypress/e2e/home/home.js b/smoke-test/tests/cypress/cypress/e2e/home/home.js index 8fa6b43e5b5d21..0039114ff9c14c 100644 --- a/smoke-test/tests/cypress/cypress/e2e/home/home.js +++ b/smoke-test/tests/cypress/cypress/e2e/home/home.js @@ -8,5 +8,6 @@ describe('home', () => { cy.get('[data-testid="entity-type-browse-card-CHART"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-DATA_FLOW"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-GLOSSARY_TERM"]').should('exist'); + cy.get('[data-testid="entity-type-browse-card-BUSINESS_ATTRIBUTE"]').should('exist'); }); - }) \ No newline at end of file + }) diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js index 1baa33901724f8..40af628f3f5a1e 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js @@ -153,4 +153,30 @@ describe("mutations", () => { cy.contains("CypressTerm").should("not.exist"); }); + + it("can add and remove business attribute from a dataset field", () => { + cy.login(); + // make space for the glossary term column + cy.viewport(2000, 800); + + cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); + cy.get('[data-testid="schema-field-event_data-businessAttribute"]').trigger( + "mouseover", + { force: true } + ); + cy.get('[data-testid="schema-field-event_data-businessAttribute"]').within(() => + cy.contains("Add Attribute").click({ force: true }) + ); + + cy.selectOptionInAttributeModal("test"); + + cy.contains("test"); + + cy.get( + 'a[href="/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449"]' + ).within(() => cy.get("span[aria-label=close]").click({ force: true })); + cy.contains("Yes").click({ force: true }); + + cy.contains("test").should("not.exist"); + }); }); diff --git a/smoke-test/tests/cypress/cypress/support/commands.js b/smoke-test/tests/cypress/cypress/support/commands.js index 5e3664f944edf1..e2130d98b04962 100644 --- a/smoke-test/tests/cypress/cypress/support/commands.js +++ b/smoke-test/tests/cypress/cypress/support/commands.js @@ -69,6 +69,12 @@ Cypress.Commands.add("goToGlossaryList", () => { cy.wait(3000); }); +Cypress.Commands.add("goToBusinessAttributeList", () => { + cy.visit("/business-attribute"); + cy.waitTextVisible("Business Attribute"); + cy.wait(3000); +}); + Cypress.Commands.add("goToDomainList", () => { cy.visit("/domains"); cy.waitTextVisible("Domains"); @@ -103,6 +109,20 @@ Cypress.Commands.add("goToDataset", (urn, dataset_name) => { cy.waitTextVisible(dataset_name); }); +Cypress.Commands.add("goToBusinessAttribute", (urn, attribute_name) => { + cy.visit( + "/business-attribute/" + urn + ); + cy.waitTextVisible(attribute_name); +}); + +Cypress.Commands.add("goToTag", (urn, tag_name) => { + cy.visit( + "/tag/" + urn + ); + cy.waitTextVisible(tag_name); +}); + Cypress.Commands.add("goToEntityLineageGraph", (entity_type, urn) => { cy.visit( `/${entity_type}/${urn}?is_lineage_mode=true` @@ -243,6 +263,24 @@ Cypress.Commands.add('addTermToDataset', (urn, dataset_name, term) => { cy.contains(term); }); +Cypress.Commands.add('addTermToBusinessAttribute', (urn, attribute_name, term) => { + cy.goToBusinessAttribute(urn, attribute_name); + cy.clickOptionWithText("Add Terms"); + cy.selectOptionInTagTermModal(term); + cy.contains(term); +}); + +Cypress.Commands.add('addAttributeToDataset', (urn, dataset_name, businessAttribute) => { + cy.goToDataset(urn, dataset_name); + cy.contains("Business Attributes"); + cy.mouseover('[data-testid="schema-field-event_name-businessAttribute"]'); + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => + cy.contains("Add Attribute").click() + ); + cy.selectOptionInAttributeModal(businessAttribute); + cy.contains(businessAttribute); +}); + Cypress.Commands.add('selectOptionInTagTermModal', (text) => { cy.enterTextInTestId("tag-term-modal-input", text); cy.clickOptionWithTestId("tag-term-option"); @@ -251,6 +289,14 @@ Cypress.Commands.add('selectOptionInTagTermModal', (text) => { cy.get(selectorWithtestId(btn_id)).should("not.exist"); }); +Cypress.Commands.add('selectOptionInAttributeModal', (text) => { + cy.enterTextInTestId("business-attribute-modal-input", text); + cy.clickOptionWithTestId("business-attribute-option"); + let btn_id = "add-attribute-from-modal-btn"; + cy.clickOptionWithTestId(btn_id); + cy.get(selectorWithtestId(btn_id)).should("not.exist"); +}); + Cypress.Commands.add("removeDomainFromDataset", (urn, dataset_name, domain_urn) => { cy.goToDataset(urn, dataset_name); cy.get('.sidebar-domain-section [href="/domain/' + domain_urn + '"] .anticon-close').click(); diff --git a/smoke-test/tests/cypress/data.json b/smoke-test/tests/cypress/data.json index 3b2ee1afaba586..22a3af15847713 100644 --- a/smoke-test/tests/cypress/data.json +++ b/smoke-test/tests/cypress/data.json @@ -2011,5 +2011,57 @@ "contentType": "application/json" }, "systemMetadata": null + }, + { + "auditHeader": null, + "entityType": "businessAttribute", + "entityUrn": "urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449", + "entityKeyAspect": null, + "changeType": "UPSERT", + "aspectName": "businessAttributeInfo", + "aspect": { + "value": "{\n \"fieldPath\": \"CypressAttribute\",\n \"description\": \"CypressAttribute\",\n \"globalTags\": {\n \"tags\": [\n {\n \"tag\": \"urn:li:tag:Cypress\"\n }\n ]\n },\n \"glossaryTerms\": {\n \"terms\": [\n {\n \"urn\": \"urn:li:glossaryTerm:CypressNode.CypressTerm\"\n }\n ],\n \"auditStamp\": {\n \"time\": 1706889592683,\n \"actor\": \"urn:li:corpuser:datahub\"\n }\n },\n \"customProperties\": {},\n \"created\": {\n \"time\": 1706690081803,\n \"actor\": \"urn:li:corpuser:datahub\"\n },\n \"lastModified\": {\n \"time\": 1706690081803,\n \"actor\": \"urn:li:corpuser:datahub\"\n },\n \"name\": \"CypressAttribute\",\n \"type\": {\n \"type\": {\n \"com.linkedin.schema.BooleanType\": {}\n }\n }\n }", + "contentType": "application/json" + }, + "systemMetadata": null + }, + { + "auditHeader": null, + "entityType": "businessAttribute", + "entityUrn": "urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449", + "entityKeyAspect": null, + "changeType": "UPSERT", + "aspectName": "ownership", + "aspect": { + "value": "{\n \"owners\": [\n {\n \"owner\": \"urn:li:corpuser:datahub\",\n \"type\": \"TECHNICAL_OWNER\",\n \"typeUrn\": \"urn:li:ownershipType:__system__technical_owner\",\n \"source\": {\n \"type\": \"MANUAL\"\n }\n }\n ]\n }", + "contentType": "application/json" + }, + "systemMetadata": null + }, + { + "auditHeader": null, + "entityType": "businessAttribute", + "entityUrn": "urn:li:businessAttribute:cypressTestAttribute", + "entityKeyAspect": null, + "changeType": "UPSERT", + "aspectName": "businessAttributeInfo", + "aspect": { + "value": "{\n \"fieldPath\": \"cypressTestAttribute\",\n \"description\": \"cypressTestAttribute\",\n \"customProperties\": {},\n \"created\": {\n \"time\": 1706690081803,\n \"actor\": \"urn:li:corpuser:datahub\"\n },\n \"lastModified\": {\n \"time\": 1706690081803,\n \"actor\": \"urn:li:corpuser:datahub\"\n },\n \"name\": \"cypressTestAttribute\",\n \"type\": {\n \"type\": {\n \"com.linkedin.schema.BooleanType\": {}\n }\n }\n }", + "contentType": "application/json" + }, + "systemMetadata": null + }, + { + "auditHeader": null, + "entityType": "businessAttribute", + "entityUrn": "urn:li:businessAttribute:cypressTestAttribute", + "entityKeyAspect": null, + "changeType": "UPSERT", + "aspectName": "ownership", + "aspect": { + "value": "{\n \"owners\": [\n {\n \"owner\": \"urn:li:corpuser:datahub\",\n \"type\": \"TECHNICAL_OWNER\",\n \"typeUrn\": \"urn:li:ownershipType:__system__technical_owner\",\n \"source\": {\n \"type\": \"MANUAL\"\n }\n }\n ]\n }", + "contentType": "application/json" + }, + "systemMetadata": null } -] \ No newline at end of file +] From 70054eaefe8e5bbad0131830343fed5ca084e5a2 Mon Sep 17 00:00:00 2001 From: aditigup Date: Mon, 5 Feb 2024 16:56:17 +0530 Subject: [PATCH 19/50] Enabling editing data type --- .../profile/BusinessAttributeDataTypeSection.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx index db7204abfd9336..da2b108c2d8d04 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/profile/BusinessAttributeDataTypeSection.tsx @@ -65,7 +65,6 @@ export const BusinessAttributeDataTypeSection = ({ readOnly }: Props) => { Date: Wed, 7 Feb 2024 21:02:21 +0530 Subject: [PATCH 20/50] businessattribute: resolve merge conflicts with master --- .../datahub/graphql/GmsGraphQLEngine.java | 77 ++-- .../datahub/graphql/resolvers/MeResolver.java | 4 +- .../AddBusinessAttributeResolver.java | 156 ++++--- .../BusinessAttributeAuthorizationUtils.java | 86 ++-- .../CreateBusinessAttributeResolver.java | 164 +++---- .../DeleteBusinessAttributeResolver.java | 70 +-- .../ListBusinessAttributesResolver.java | 72 +-- .../RemoveBusinessAttributeResolver.java | 138 +++--- .../UpdateBusinessAttributeResolver.java | 188 ++++---- .../resolvers/config/AppConfigResolver.java | 4 +- .../resolvers/mutate/DescriptionUtils.java | 22 +- .../mutate/UpdateDescriptionResolver.java | 172 ++++---- .../resolvers/mutate/UpdateNameResolver.java | 121 +++--- .../mutate/util/BusinessAttributeUtils.java | 168 +++---- .../resolvers/mutate/util/LabelUtils.java | 80 ++-- .../graphql/resolvers/search/SearchUtils.java | 1 - .../BusinessAttributeType.java | 160 ++++--- .../mappers/BusinessAttributeMapper.java | 160 +++---- .../mappers/BusinessAttributesMapper.java | 53 +-- .../graphql/types/dataset/DatasetType.java | 43 +- .../EditableSchemaFieldInfoMapper.java | 13 +- .../AddBusinessAttributeResolverTest.java | 338 ++++++++------- ...reateBusinessAttributeProposalMatcher.java | 50 +-- .../CreateBusinessAttributeResolverTest.java | 383 +++++++++------- .../DeleteBusinessAttributeResolverTest.java | 150 ++++--- .../RemoveBusinessAttributeResolverTest.java | 296 +++++++------ .../UpdateBusinessAttributeResolverTest.java | 409 ++++++++++-------- .../UpdateNameResolverTest.java | 244 ++++++----- .../java/com/linkedin/metadata/Constants.java | 2 +- .../metadata/search/utils/ESUtils.java | 28 +- ...sinessAttributeAssociationChangeEvent.java | 47 +- ...ributeAssociationChangeEventGenerator.java | 98 +++-- ...nessAttributeInfoChangeEventGenerator.java | 210 +++++---- ...bleSchemaMetadataChangeEventGenerator.java | 79 ++-- .../BusinessAttributeServiceFactory.java | 25 +- ...tyChangeEventGeneratorRegistryFactory.java | 8 +- .../delegates/EntityApiDelegateImpl.java | 1 + .../v2/delegates/EntityApiDelegateImpl.java | 38 ++ .../service/BusinessAttributeService.java | 42 +- .../authorization/PoliciesConfig.java | 125 +++--- 40 files changed, 2509 insertions(+), 2016 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 6bae9cdc5626e4..9f686af6c33b8a 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -33,6 +33,8 @@ import com.linkedin.datahub.graphql.generated.BrowsePathEntry; import com.linkedin.datahub.graphql.generated.BrowseResultGroupV2; import com.linkedin.datahub.graphql.generated.BrowseResults; +import com.linkedin.datahub.graphql.generated.BusinessAttribute; +import com.linkedin.datahub.graphql.generated.BusinessAttributeAssociation; import com.linkedin.datahub.graphql.generated.Chart; import com.linkedin.datahub.graphql.generated.ChartInfo; import com.linkedin.datahub.graphql.generated.Container; @@ -67,13 +69,13 @@ import com.linkedin.datahub.graphql.generated.InstitutionalMemoryMetadata; import com.linkedin.datahub.graphql.generated.LineageRelationship; import com.linkedin.datahub.graphql.generated.ListAccessTokenResult; +import com.linkedin.datahub.graphql.generated.ListBusinessAttributesResult; import com.linkedin.datahub.graphql.generated.ListDomainsResult; import com.linkedin.datahub.graphql.generated.ListGroupsResult; import com.linkedin.datahub.graphql.generated.ListOwnershipTypesResult; import com.linkedin.datahub.graphql.generated.ListQueriesResult; import com.linkedin.datahub.graphql.generated.ListTestsResult; import com.linkedin.datahub.graphql.generated.ListViewsResult; -import com.linkedin.datahub.graphql.generated.ListBusinessAttributesResult; import com.linkedin.datahub.graphql.generated.MLFeature; import com.linkedin.datahub.graphql.generated.MLFeatureProperties; import com.linkedin.datahub.graphql.generated.MLFeatureTable; @@ -106,8 +108,6 @@ import com.linkedin.datahub.graphql.generated.TestResult; import com.linkedin.datahub.graphql.generated.TypeQualifier; import com.linkedin.datahub.graphql.generated.UserUsageCounts; -import com.linkedin.datahub.graphql.generated.BusinessAttribute; -import com.linkedin.datahub.graphql.generated.BusinessAttributeAssociation; import com.linkedin.datahub.graphql.resolvers.MeResolver; import com.linkedin.datahub.graphql.resolvers.assertion.AssertionRunEventResolver; import com.linkedin.datahub.graphql.resolvers.assertion.DeleteAssertionResolver; @@ -477,8 +477,7 @@ public class GmsGraphQLEngine { private final BusinessAttributeType businessAttributeType; - /** - A list of GraphQL Plugins that extend the core engine */ + /** A list of GraphQL Plugins that extend the core engine */ private final List graphQLPlugins; /** Configures the graph objects that can be fetched primary key. */ @@ -992,9 +991,7 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) { .dataFetcher( "browseV2", new BrowseV2Resolver(this.entityClient, this.viewService, this.formService)) - .dataFetcher( - "businessAttribute", - getResolver(businessAttributeType)) + .dataFetcher("businessAttribute", getResolver(businessAttributeType)) .dataFetcher( "listBusinessAttributes", new ListBusinessAttributesResolver(this.entityClient))); @@ -1220,10 +1217,12 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) .dataFetcher( "createBusinessAttribute", - new CreateBusinessAttributeResolver(this.entityClient, this.entityService, this.businessAttributeService)) + new CreateBusinessAttributeResolver( + this.entityClient, this.entityService, this.businessAttributeService)) .dataFetcher( "updateBusinessAttribute", - new UpdateBusinessAttributeResolver(this.entityClient, this.businessAttributeService)) + new UpdateBusinessAttributeResolver( + this.entityClient, this.businessAttributeService)) .dataFetcher( "deleteBusinessAttribute", new DeleteBusinessAttributeResolver(this.entityClient)) @@ -1232,8 +1231,7 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { new AddBusinessAttributeResolver(this.entityClient, this.entityService)) .dataFetcher( "removeBusinessAttribute", - new RemoveBusinessAttributeResolver(this.entityClient, this.entityService)) - ); + new RemoveBusinessAttributeResolver(this.entityClient, this.entityService))); } private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder) { @@ -1682,7 +1680,8 @@ private void configureResolvedAuditStampResolvers(final RuntimeWiring.Builder bu typeWiring.dataFetcher( "actor", new LoadableTypeResolver<>( - corpUserType, (env) -> ((ResolvedAuditStamp) env.getSource()).getActor().getUrn()))); + corpUserType, + (env) -> ((ResolvedAuditStamp) env.getSource()).getActor().getUrn()))); } /** @@ -2689,23 +2688,39 @@ private void configureIngestionSourceResolvers(final RuntimeWiring.Builder build }))); } - private void configureBusinessAttributeResolver(final RuntimeWiring.Builder builder) { - builder.type("BusinessAttribute", typeWiring -> typeWiring - .dataFetcher("exists", new EntityExistsResolver(entityService)) - .dataFetcher("privileges", new EntityPrivilegesResolver(entityClient))) - .type("ListBusinessAttributesResult", typeWiring -> typeWiring - .dataFetcher("businessAttributes", new LoadableTypeBatchResolver<>( + private void configureBusinessAttributeResolver(final RuntimeWiring.Builder builder) { + builder + .type( + "BusinessAttribute", + typeWiring -> + typeWiring + .dataFetcher("exists", new EntityExistsResolver(entityService)) + .dataFetcher("privileges", new EntityPrivilegesResolver(entityClient))) + .type( + "ListBusinessAttributesResult", + typeWiring -> + typeWiring.dataFetcher( + "businessAttributes", + new LoadableTypeBatchResolver<>( + businessAttributeType, + (env) -> + ((ListBusinessAttributesResult) env.getSource()) + .getBusinessAttributes().stream() + .map(BusinessAttribute::getUrn) + .collect(Collectors.toList())))); + } + + private void configureBusinessAttributeAssociationResolver(final RuntimeWiring.Builder builder) { + builder.type( + "BusinessAttributeAssociation", + typeWiring -> + typeWiring.dataFetcher( + "businessAttribute", + new LoadableTypeResolver<>( businessAttributeType, - (env) -> ((ListBusinessAttributesResult) env.getSource()).getBusinessAttributes().stream() - .map(BusinessAttribute::getUrn) - .collect(Collectors.toList()))) - ); - } - private void configureBusinessAttributeAssociationResolver(final RuntimeWiring.Builder builder) { - builder.type("BusinessAttributeAssociation", typeWiring -> typeWiring - .dataFetcher("businessAttribute", - new LoadableTypeResolver<>(businessAttributeType, - (env) -> ((BusinessAttributeAssociation) env.getSource()).getBusinessAttribute().getUrn())) - ); - } + (env) -> + ((BusinessAttributeAssociation) env.getSource()) + .getBusinessAttribute() + .getUrn()))); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java index 431a87aed3b629..095f728387afc7 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java @@ -86,9 +86,9 @@ public CompletableFuture get(DataFetchingEnvironment environm platformPrivileges.setManageGlobalAnnouncements( AuthorizationUtils.canManageGlobalAnnouncements(context)); platformPrivileges.setCreateBusinessAttributes( - BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)); + BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)); platformPrivileges.setManageBusinessAttributes( - BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)); + BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)); // Construct and return authenticated user object. final AuthenticatedUser authUser = new AuthenticatedUser(); authUser.setCorpUser(corpUser); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java index d07e858dcde15d..a213dd224648fe 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java @@ -1,5 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; + import com.linkedin.businessattribute.BusinessAttributeAssociation; import com.linkedin.common.AuditStamp; import com.linkedin.common.urn.Urn; @@ -19,85 +24,106 @@ import com.linkedin.schema.EditableSchemaMetadata; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.CompletableFuture; - -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; -import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; - @Slf4j @RequiredArgsConstructor public class AddBusinessAttributeResolver implements DataFetcher> { - private final EntityClient _entityClient; - private final EntityService _entityService; - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - final QueryContext context = environment.getContext(); - AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); - Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); - ResourceRefInput resourceRefInput = input.getResourceUrn(); - if (!isAuthorizeToUpdateDataset(context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); - } - return CompletableFuture.supplyAsync(() -> { - try { - validateInputResource(resourceRefInput); - addBusinessAttribute(businessAttributeUrn, resourceRefInput, context); - return true; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to add Business Attribute with urn %s to dataset with urn %s", - businessAttributeUrn, resourceRefInput.getResourceUrn()), e); - } - }); - } + private final EntityClient _entityClient; + private final EntityService _entityService; - private void validateInputResource(ResourceRefInput resource) { - final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); - LabelUtils.validateResource(resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + AddBusinessAttributeInput input = + bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); + Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); + ResourceRefInput resourceRefInput = input.getResourceUrn(); + if (!isAuthorizeToUpdateDataset( + context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); } - - private void addBusinessAttribute(Urn businessAttributeUrn, ResourceRefInput resourceRefInput, QueryContext context) throws RemoteInvocationException { - _entityClient.ingestProposal( - buildAddBusinessAttributeToSubresourceProposal(businessAttributeUrn, resourceRefInput, context), - context.getAuthentication() - ); + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new RuntimeException( + String.format("This urn does not exist: %s", businessAttributeUrn)); } + return CompletableFuture.supplyAsync( + () -> { + try { + validateInputResource(resourceRefInput); + addBusinessAttribute(businessAttributeUrn, resourceRefInput, context); + return true; + } catch (Exception e) { + throw new RuntimeException( + String.format( + "Failed to add Business Attribute with urn %s to dataset with urn %s", + businessAttributeUrn, resourceRefInput.getResourceUrn()), + e); + } + }); + } - private MetadataChangeProposal buildAddBusinessAttributeToSubresourceProposal(Urn businessAttributeUrn, ResourceRefInput resource, QueryContext context) { - com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = - (com.linkedin.schema.EditableSchemaMetadata) EntityUtils.getAspectFromEntity( - resource.getResourceUrn(), Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - _entityService, new EditableSchemaMetadata() - ); + private void validateInputResource(ResourceRefInput resource) { + final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); + LabelUtils.validateResource( + resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); + } - EditableSchemaFieldInfo editableFieldInfo = getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); + private void addBusinessAttribute( + Urn businessAttributeUrn, ResourceRefInput resourceRefInput, QueryContext context) + throws RemoteInvocationException { + _entityClient.ingestProposal( + buildAddBusinessAttributeToSubresourceProposal( + businessAttributeUrn, resourceRefInput, context), + context.getAuthentication()); + } - if (editableFieldInfo == null) { - throw new IllegalArgumentException(String.format("Subresource %s does not exist in dataset %s", - resource.getSubResource(), resource.getResourceUrn() - )); - } + private MetadataChangeProposal buildAddBusinessAttributeToSubresourceProposal( + Urn businessAttributeUrn, ResourceRefInput resource, QueryContext context) { + com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = + (com.linkedin.schema.EditableSchemaMetadata) + EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + _entityService, + new EditableSchemaMetadata()); - if (editableFieldInfo.hasBusinessAttribute()) { - throw new RuntimeException(String.format("Schema field has already attached with business attribute")); - } - editableFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); - addBusinessAttribute(editableFieldInfo.getBusinessAttribute(), businessAttributeUrn, UrnUtils.getUrn(context.getActorUrn())); - return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, editableSchemaMetadata); + EditableSchemaFieldInfo editableFieldInfo = + getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); + + if (editableFieldInfo == null) { + throw new IllegalArgumentException( + String.format( + "Subresource %s does not exist in dataset %s", + resource.getSubResource(), resource.getResourceUrn())); } - private void addBusinessAttribute(BusinessAttributeAssociation businessAttributeAssociation, Urn businessAttributeUrn, Urn actorUrn) { - businessAttributeAssociation.setDestinationUrn(businessAttributeUrn); - AuditStamp nowAuditStamp = new AuditStamp().setTime(System.currentTimeMillis()).setActor(actorUrn); - businessAttributeAssociation.setCreated(nowAuditStamp); - businessAttributeAssociation.setLastModified(nowAuditStamp); + if (editableFieldInfo.hasBusinessAttribute()) { + throw new RuntimeException( + String.format("Schema field has already attached with business attribute")); } + editableFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); + addBusinessAttribute( + editableFieldInfo.getBusinessAttribute(), + businessAttributeUrn, + UrnUtils.getUrn(context.getActorUrn())); + return buildMetadataChangeProposalWithUrn( + UrnUtils.getUrn(resource.getResourceUrn()), + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + editableSchemaMetadata); + } + + private void addBusinessAttribute( + BusinessAttributeAssociation businessAttributeAssociation, + Urn businessAttributeUrn, + Urn actorUrn) { + businessAttributeAssociation.setDestinationUrn(businessAttributeUrn); + AuditStamp nowAuditStamp = + new AuditStamp().setTime(System.currentTimeMillis()).setActor(actorUrn); + businessAttributeAssociation.setCreated(nowAuditStamp); + businessAttributeAssociation.setLastModified(nowAuditStamp); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java index c30e26a3bf9e66..b545c08a622e3e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java @@ -1,5 +1,7 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.resolvers.AuthUtils.ALL_PRIVILEGES_GROUP; + import com.datahub.authorization.ConjunctivePrivilegeGroup; import com.datahub.authorization.DisjunctivePrivilegeGroup; import com.google.common.collect.ImmutableList; @@ -7,51 +9,49 @@ import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.metadata.authorization.PoliciesConfig; - import javax.annotation.Nonnull; -import static com.linkedin.datahub.graphql.resolvers.AuthUtils.ALL_PRIVILEGES_GROUP; - public class BusinessAttributeAuthorizationUtils { - private BusinessAttributeAuthorizationUtils() { - - } - - public static boolean canCreateBusinessAttribute(@Nonnull QueryContext context) { - final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( - new ConjunctivePrivilegeGroup(ImmutableList.of( - PoliciesConfig.CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())), - new ConjunctivePrivilegeGroup(ImmutableList.of( - PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())) - )); - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), - context.getActorUrn(), - orPrivilegeGroups); - } - - public static boolean canManageBusinessAttribute(@Nonnull QueryContext context) { - final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( - new ConjunctivePrivilegeGroup(ImmutableList.of( - PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())) - )); - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), - context.getActorUrn(), - orPrivilegeGroups); - } - - public static boolean isAuthorizeToUpdateDataset(QueryContext context, Urn targetUrn) { - final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup(ImmutableList.of( + private BusinessAttributeAuthorizationUtils() {} + + public static boolean canCreateBusinessAttribute(@Nonnull QueryContext context) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = + new DisjunctivePrivilegeGroup( + ImmutableList.of( + new ConjunctivePrivilegeGroup( + ImmutableList.of(PoliciesConfig.CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())), + new ConjunctivePrivilegeGroup( + ImmutableList.of( + PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())))); + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), context.getActorUrn(), orPrivilegeGroups); + } + + public static boolean canManageBusinessAttribute(@Nonnull QueryContext context) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = + new DisjunctivePrivilegeGroup( + ImmutableList.of( + new ConjunctivePrivilegeGroup( + ImmutableList.of( + PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())))); + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), context.getActorUrn(), orPrivilegeGroups); + } + + public static boolean isAuthorizeToUpdateDataset(QueryContext context, Urn targetUrn) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = + new DisjunctivePrivilegeGroup( + ImmutableList.of( ALL_PRIVILEGES_GROUP, - new ConjunctivePrivilegeGroup(ImmutableList.of(PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())) - )); - - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), - context.getActorUrn(), - targetUrn.getEntityType(), - targetUrn.toString(), - orPrivilegeGroups); - } + new ConjunctivePrivilegeGroup( + ImmutableList.of( + PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())))); + + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + targetUrn.getEntityType(), + targetUrn.toString(), + orPrivilegeGroups); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java index 8a4916b0e28567..2103d6d4eceef6 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java @@ -1,5 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithKey; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; + import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.businessattribute.BusinessAttributeKey; import com.linkedin.common.AuditStamp; @@ -13,7 +18,6 @@ import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.generated.CreateBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.OwnerEntityType; -import com.linkedin.datahub.graphql.generated.OwnershipType; import com.linkedin.datahub.graphql.resolvers.mutate.util.BusinessAttributeUtils; import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils; import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributeMapper; @@ -24,91 +28,101 @@ import com.linkedin.mxe.MetadataChangeProposal; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - import java.util.UUID; import java.util.concurrent.CompletableFuture; - -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithKey; -import static com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils.mapOwnershipTypeToEntity; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor -public class CreateBusinessAttributeResolver implements DataFetcher> { - private final EntityClient _entityClient; - private final EntityService _entityService; - private final BusinessAttributeService businessAttributeService; +public class CreateBusinessAttributeResolver + implements DataFetcher> { + private final EntityClient _entityClient; + private final EntityService _entityService; + private final BusinessAttributeService businessAttributeService; - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - final QueryContext context = environment.getContext(); - CreateBusinessAttributeInput input = bindArgument(environment.getArgument("input"), CreateBusinessAttributeInput.class); - if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - return CompletableFuture.supplyAsync(() -> { - try { - final BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); - businessAttributeKey.setId(UUID.randomUUID().toString()); + @Override + public CompletableFuture get(DataFetchingEnvironment environment) + throws Exception { + final QueryContext context = environment.getContext(); + CreateBusinessAttributeInput input = + bindArgument(environment.getArgument("input"), CreateBusinessAttributeInput.class); + if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + return CompletableFuture.supplyAsync( + () -> { + try { + final BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); + businessAttributeKey.setId(UUID.randomUUID().toString()); - if (_entityClient.exists(EntityKeyUtils.convertEntityKeyToUrn(businessAttributeKey, - BUSINESS_ATTRIBUTE_ENTITY_NAME), - context.getAuthentication())) { - throw new IllegalArgumentException("This Business Attribute already exists!"); - } + if (_entityClient.exists( + EntityKeyUtils.convertEntityKeyToUrn( + businessAttributeKey, BUSINESS_ATTRIBUTE_ENTITY_NAME), + context.getAuthentication())) { + throw new IllegalArgumentException("This Business Attribute already exists!"); + } - if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { - throw new DataHubGraphQLException( - String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", - input.getName()), DataHubGraphQLErrorCode.CONFLICT); - } + if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { + throw new DataHubGraphQLException( + String.format( + "\"%s\" already exists as Business Attribute. Please pick a unique name.", + input.getName()), + DataHubGraphQLErrorCode.CONFLICT); + } - // Create the MCP - final MetadataChangeProposal changeProposal = buildMetadataChangeProposalWithKey( - businessAttributeKey, BUSINESS_ATTRIBUTE_ENTITY_NAME, - BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, - mapBusinessAttributeInfo(input, context) - ); + // Create the MCP + final MetadataChangeProposal changeProposal = + buildMetadataChangeProposalWithKey( + businessAttributeKey, + BUSINESS_ATTRIBUTE_ENTITY_NAME, + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + mapBusinessAttributeInfo(input, context)); - // Ingest the MCP - Urn businessAttributeUrn = UrnUtils.getUrn(_entityClient.ingestProposal(changeProposal, context.getAuthentication())); - addOwnerToBusinessAttribute(context, businessAttributeUrn.toString()); - return BusinessAttributeMapper.map( - businessAttributeService.getBusinessAttributeEntityResponse( - businessAttributeUrn, context.getAuthentication() - ) - ); + // Ingest the MCP + Urn businessAttributeUrn = + UrnUtils.getUrn( + _entityClient.ingestProposal(changeProposal, context.getAuthentication())); + OwnerUtils.addCreatorAsOwner( + context, + businessAttributeUrn.toString(), + OwnerEntityType.CORP_USER, + _entityService); + return BusinessAttributeMapper.map( + businessAttributeService.getBusinessAttributeEntityResponse( + businessAttributeUrn, context.getAuthentication())); - } catch (DataHubGraphQLException e) { - throw e; - } catch (Exception e) { - log.error("Failed to create Business Attribute with name: {}: {}", input.getName(), e.getMessage()); - throw new RuntimeException(String.format("Failed to create Business Attribute with name: %s", input.getName()), e); - } + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + log.error( + "Failed to create Business Attribute with name: {}: {}", + input.getName(), + e.getMessage()); + throw new RuntimeException( + String.format("Failed to create Business Attribute with name: %s", input.getName()), + e); + } }); - } + } - private BusinessAttributeInfo mapBusinessAttributeInfo(CreateBusinessAttributeInput input, QueryContext context) { - final BusinessAttributeInfo info = new BusinessAttributeInfo(); - info.setFieldPath(input.getName(), SetMode.DISALLOW_NULL); - info.setName(input.getName(), SetMode.DISALLOW_NULL); - info.setDescription(input.getDescription(), SetMode.IGNORE_NULL); - info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(input.getType()), SetMode.IGNORE_NULL); - info.setCreated(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis())); - info.setLastModified(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis())); - return info; - } - - private void addOwnerToBusinessAttribute(QueryContext context, String businessAttributeUrn) { - OwnershipType ownershipType = OwnershipType.TECHNICAL_OWNER; - if (!_entityService.exists(UrnUtils.getUrn(mapOwnershipTypeToEntity(ownershipType.name())))) { - log.warn("Technical owner does not exist, defaulting to None ownership."); - ownershipType = OwnershipType.NONE; - } - OwnerUtils.addCreatorAsOwner(context, businessAttributeUrn, OwnerEntityType.CORP_USER, ownershipType, _entityService); - } + private BusinessAttributeInfo mapBusinessAttributeInfo( + CreateBusinessAttributeInput input, QueryContext context) { + final BusinessAttributeInfo info = new BusinessAttributeInfo(); + info.setFieldPath(input.getName(), SetMode.DISALLOW_NULL); + info.setName(input.getName(), SetMode.DISALLOW_NULL); + info.setDescription(input.getDescription(), SetMode.IGNORE_NULL); + info.setType( + BusinessAttributeUtils.mapSchemaFieldDataType(input.getType()), SetMode.IGNORE_NULL); + info.setCreated( + new AuditStamp() + .setActor(UrnUtils.getUrn(context.getActorUrn())) + .setTime(System.currentTimeMillis())); + info.setLastModified( + new AuditStamp() + .setActor(UrnUtils.getUrn(context.getActorUrn())) + .setTime(System.currentTimeMillis())); + return info; + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java index ebbe68e8ea4143..b397c27834392b 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolver.java @@ -7,44 +7,52 @@ import com.linkedin.entity.client.EntityClient; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.CompletableFuture; - -/** - * Resolver responsible for hard deleting a particular Business Attribute - */ +/** Resolver responsible for hard deleting a particular Business Attribute */ @Slf4j @RequiredArgsConstructor public class DeleteBusinessAttributeResolver implements DataFetcher> { - private final EntityClient _entityClient; + private final EntityClient _entityClient; - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - final QueryContext context = environment.getContext(); - final Urn businessAttributeUrn = UrnUtils.getUrn(environment.getArgument("urn")); - if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); - } - return CompletableFuture.supplyAsync(() -> { - try { - _entityClient.deleteEntity(businessAttributeUrn, context.getAuthentication()); - CompletableFuture.runAsync(() -> { - try { - _entityClient.deleteEntityReferences(businessAttributeUrn, context.getAuthentication()); - } catch (Exception e) { - log.error(String.format( - "Exception while attempting to clear all entity references for Business Attribute with urn %s", businessAttributeUrn), e); - } + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + final Urn businessAttributeUrn = UrnUtils.getUrn(environment.getArgument("urn")); + if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new RuntimeException( + String.format("This urn does not exist: %s", businessAttributeUrn)); + } + return CompletableFuture.supplyAsync( + () -> { + try { + _entityClient.deleteEntity(businessAttributeUrn, context.getAuthentication()); + CompletableFuture.runAsync( + () -> { + try { + _entityClient.deleteEntityReferences( + businessAttributeUrn, context.getAuthentication()); + } catch (Exception e) { + log.error( + String.format( + "Exception while attempting to clear all entity references for Business Attribute with urn %s", + businessAttributeUrn), + e); + } }); - return true; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to delete Business Attribute with urn %s", businessAttributeUrn), e); - } + return true; + } catch (Exception e) { + throw new RuntimeException( + String.format( + "Failed to delete Business Attribute with urn %s", businessAttributeUrn), + e); + } }); - } + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java index 6afedb7b2e3a57..23b17f999c98de 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java @@ -1,5 +1,7 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; + import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.BusinessAttribute; @@ -22,14 +24,10 @@ import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; - - -/** - * Resolver used for listing Business Attributes. - */ +/** Resolver used for listing Business Attributes. */ @Slf4j -public class ListBusinessAttributesResolver implements DataFetcher> { +public class ListBusinessAttributesResolver + implements DataFetcher> { private static final Integer DEFAULT_START = 0; private static final Integer DEFAULT_COUNT = 20; @@ -42,39 +40,45 @@ public ListBusinessAttributesResolver(@Nonnull final EntityClient entityClient) } @Override - public CompletableFuture get(final DataFetchingEnvironment environment) throws Exception { + public CompletableFuture get( + final DataFetchingEnvironment environment) throws Exception { final QueryContext context = environment.getContext(); - final ListBusinessAttributesInput input = bindArgument(environment.getArgument("input"), ListBusinessAttributesInput.class); + final ListBusinessAttributesInput input = + bindArgument(environment.getArgument("input"), ListBusinessAttributesInput.class); - return CompletableFuture.supplyAsync(() -> { - final Integer start = input.getStart() == null ? DEFAULT_START : input.getStart(); - final Integer count = input.getCount() == null ? DEFAULT_COUNT : input.getCount(); - final String query = input.getQuery() == null ? DEFAULT_QUERY : input.getQuery(); + return CompletableFuture.supplyAsync( + () -> { + final Integer start = input.getStart() == null ? DEFAULT_START : input.getStart(); + final Integer count = input.getCount() == null ? DEFAULT_COUNT : input.getCount(); + final String query = input.getQuery() == null ? DEFAULT_QUERY : input.getQuery(); - try { + try { - final SearchResult gmsResult = _entityClient.search( - Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, - query, - Collections.emptyMap(), - start, - count, - context.getAuthentication(), - new SearchFlags().setFulltext(true)); + final SearchResult gmsResult = + _entityClient.search( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + query, + Collections.emptyMap(), + start, + count, + context.getAuthentication(), + new SearchFlags().setFulltext(true)); - final ListBusinessAttributesResult result = new ListBusinessAttributesResult(); - result.setStart(gmsResult.getFrom()); - result.setCount(gmsResult.getPageSize()); - result.setTotal(gmsResult.getNumEntities()); - result.setBusinessAttributes(mapUnresolvedBusinessAttributes(gmsResult.getEntities().stream() - .map(SearchEntity::getEntity) - .collect(Collectors.toList()))); - return result; - } catch (Exception e) { - throw new RuntimeException("Failed to list Business Attributes", e); - } - }); + final ListBusinessAttributesResult result = new ListBusinessAttributesResult(); + result.setStart(gmsResult.getFrom()); + result.setCount(gmsResult.getPageSize()); + result.setTotal(gmsResult.getNumEntities()); + result.setBusinessAttributes( + mapUnresolvedBusinessAttributes( + gmsResult.getEntities().stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()))); + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to list Business Attributes", e); + } + }); } private List mapUnresolvedBusinessAttributes(final List entityUrns) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java index 8a3d936fc1250e..a434bb11afd4f0 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java @@ -1,5 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; + import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; @@ -17,83 +22,94 @@ import com.linkedin.schema.EditableSchemaMetadata; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.CompletableFuture; - -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; -import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; - @Slf4j @RequiredArgsConstructor public class RemoveBusinessAttributeResolver implements DataFetcher> { - private final EntityClient _entityClient; - private final EntityService _entityService; - + private final EntityClient _entityClient; + private final EntityService _entityService; - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - final QueryContext context = environment.getContext(); - AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); - Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); - ResourceRefInput resourceRefInput = input.getResourceUrn(); - if (!isAuthorizeToUpdateDataset(context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - return CompletableFuture.supplyAsync(() -> { - try { - if (!businessAttributeUrn.getEntityType().equals("businessAttribute")) { - log.error("Failed to remove {}. It is not a business attribute urn.", businessAttributeUrn.toString()); - return false; - } + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final QueryContext context = environment.getContext(); + AddBusinessAttributeInput input = + bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); + Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); + ResourceRefInput resourceRefInput = input.getResourceUrn(); + if (!isAuthorizeToUpdateDataset( + context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + return CompletableFuture.supplyAsync( + () -> { + try { + if (!businessAttributeUrn.getEntityType().equals("businessAttribute")) { + log.error( + "Failed to remove {}. It is not a business attribute urn.", + businessAttributeUrn.toString()); + return false; + } - validateInputResource(resourceRefInput, context); + validateInputResource(resourceRefInput, context); - removeBusinessAttribute(resourceRefInput, context); + removeBusinessAttribute(resourceRefInput, context); - return true; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to remove Business Attribute with urn %s to dataset with urn %s", - businessAttributeUrn, resourceRefInput.getResourceUrn()), e); - } + return true; + } catch (Exception e) { + throw new RuntimeException( + String.format( + "Failed to remove Business Attribute with urn %s to dataset with urn %s", + businessAttributeUrn, resourceRefInput.getResourceUrn()), + e); + } }); - } + } - private void validateInputResource(ResourceRefInput resource, QueryContext context) { - final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); - LabelUtils.validateResource(resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); - } + private void validateInputResource(ResourceRefInput resource, QueryContext context) { + final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); + LabelUtils.validateResource( + resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); + } - private void removeBusinessAttribute(ResourceRefInput resourceRefInput, QueryContext context) throws RemoteInvocationException { - _entityClient.ingestProposal( - buildRemoveBusinessAttributeToSubresourceProposal(resourceRefInput), - context.getAuthentication() - ); - } + private void removeBusinessAttribute(ResourceRefInput resourceRefInput, QueryContext context) + throws RemoteInvocationException { + _entityClient.ingestProposal( + buildRemoveBusinessAttributeToSubresourceProposal(resourceRefInput), + context.getAuthentication()); + } - private MetadataChangeProposal buildRemoveBusinessAttributeToSubresourceProposal(ResourceRefInput resource) { - com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = - (com.linkedin.schema.EditableSchemaMetadata) EntityUtils.getAspectFromEntity( - resource.getResourceUrn(), Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - _entityService, new EditableSchemaMetadata() - ); + private MetadataChangeProposal buildRemoveBusinessAttributeToSubresourceProposal( + ResourceRefInput resource) { + com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = + (com.linkedin.schema.EditableSchemaMetadata) + EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + _entityService, + new EditableSchemaMetadata()); - EditableSchemaFieldInfo editableFieldInfo = getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); + EditableSchemaFieldInfo editableFieldInfo = + getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); - if (editableFieldInfo == null) { - throw new IllegalArgumentException(String.format("Subresource %s does not exist in dataset %s", - resource.getSubResource(), resource.getResourceUrn() - )); - } + if (editableFieldInfo == null) { + throw new IllegalArgumentException( + String.format( + "Subresource %s does not exist in dataset %s", + resource.getSubResource(), resource.getResourceUrn())); + } - if (!editableFieldInfo.hasBusinessAttribute()) { - throw new RuntimeException(String.format("Schema field has not attached with business attribute")); - } - editableFieldInfo.removeBusinessAttribute(); - return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, editableSchemaMetadata); + if (!editableFieldInfo.hasBusinessAttribute()) { + throw new RuntimeException( + String.format("Schema field has not attached with business attribute")); } + editableFieldInfo.removeBusinessAttribute(); + return buildMetadataChangeProposalWithUrn( + UrnUtils.getUrn(resource.getResourceUrn()), + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + editableSchemaMetadata); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java index c1a31ef0ae05aa..eff3a213adb073 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java @@ -1,5 +1,7 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; + import com.datahub.authentication.Authentication; import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.AuditStamp; @@ -20,97 +22,125 @@ import com.linkedin.metadata.service.BusinessAttributeService; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.Objects; import java.util.concurrent.CompletableFuture; - -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor -public class UpdateBusinessAttributeResolver implements DataFetcher> { +public class UpdateBusinessAttributeResolver + implements DataFetcher> { - private final EntityClient _entityClient; - private final BusinessAttributeService businessAttributeService; + private final EntityClient _entityClient; + private final BusinessAttributeService businessAttributeService; - @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { - QueryContext context = environment.getContext(); - UpdateBusinessAttributeInput input = bindArgument(environment.getArgument("input"), UpdateBusinessAttributeInput.class); - final Urn businessAttributeUrn = UrnUtils.getUrn(environment.getArgument("urn")); - if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new RuntimeException(String.format("This urn does not exist: %s", businessAttributeUrn)); - } - return CompletableFuture.supplyAsync(() -> { - try { - Urn updatedBusinessAttributeUrn = updateBusinessAttribute(input, businessAttributeUrn, context); - return BusinessAttributeMapper.map( - businessAttributeService.getBusinessAttributeEntityResponse(updatedBusinessAttributeUrn, context.getAuthentication())); - } catch (DataHubGraphQLException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to update Business Attribute with urn %s", businessAttributeUrn), e); - } - }); + @Override + public CompletableFuture get(DataFetchingEnvironment environment) + throws Exception { + QueryContext context = environment.getContext(); + UpdateBusinessAttributeInput input = + bindArgument(environment.getArgument("input"), UpdateBusinessAttributeInput.class); + final Urn businessAttributeUrn = UrnUtils.getUrn(environment.getArgument("urn")); + if (!BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); } + if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { + throw new RuntimeException( + String.format("This urn does not exist: %s", businessAttributeUrn)); + } + return CompletableFuture.supplyAsync( + () -> { + try { + Urn updatedBusinessAttributeUrn = + updateBusinessAttribute(input, businessAttributeUrn, context); + return BusinessAttributeMapper.map( + businessAttributeService.getBusinessAttributeEntityResponse( + updatedBusinessAttributeUrn, context.getAuthentication())); + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException( + String.format( + "Failed to update Business Attribute with urn %s", businessAttributeUrn), + e); + } + }); + } - private Urn updateBusinessAttribute(UpdateBusinessAttributeInput input, Urn businessAttributeUrn, QueryContext context) { - try { - BusinessAttributeInfo businessAttributeInfo = getBusinessAttributeInfo(businessAttributeUrn, context.getAuthentication()); - // 1. Check whether the Business Attribute exists - if (businessAttributeInfo == null) { - throw new IllegalArgumentException( - String.format("Failed to update Business Attribute. Business Attribute with urn %s does not exist.", businessAttributeUrn)); - } - - // 2. Apply changes to existing Business Attribute - if (Objects.nonNull(input.getName())) { - if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { - throw new DataHubGraphQLException( - String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", input.getName()), - DataHubGraphQLErrorCode.CONFLICT); - } - businessAttributeInfo.setName(input.getName()); - businessAttributeInfo.setFieldPath(input.getName()); - } - if (Objects.nonNull(input.getDescription())) { - businessAttributeInfo.setDescription(input.getDescription()); - } - if (Objects.nonNull(input.getType())) { - businessAttributeInfo.setType(BusinessAttributeUtils.mapSchemaFieldDataType(input.getType())); - } - businessAttributeInfo.setLastModified(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis())); - // 3. Write changes to GMS - return UrnUtils.getUrn(_entityClient.ingestProposal( - AspectUtils.buildMetadataChangeProposal( - businessAttributeUrn, Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo), context.getAuthentication() - ) - ); + private Urn updateBusinessAttribute( + UpdateBusinessAttributeInput input, Urn businessAttributeUrn, QueryContext context) { + try { + BusinessAttributeInfo businessAttributeInfo = + getBusinessAttributeInfo(businessAttributeUrn, context.getAuthentication()); + // 1. Check whether the Business Attribute exists + if (businessAttributeInfo == null) { + throw new IllegalArgumentException( + String.format( + "Failed to update Business Attribute. Business Attribute with urn %s does not exist.", + businessAttributeUrn)); + } - } catch (DataHubGraphQLException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); + // 2. Apply changes to existing Business Attribute + if (Objects.nonNull(input.getName())) { + if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { + throw new DataHubGraphQLException( + String.format( + "\"%s\" already exists as Business Attribute. Please pick a unique name.", + input.getName()), + DataHubGraphQLErrorCode.CONFLICT); } - } + businessAttributeInfo.setName(input.getName()); + businessAttributeInfo.setFieldPath(input.getName()); + } + if (Objects.nonNull(input.getDescription())) { + businessAttributeInfo.setDescription(input.getDescription()); + } + if (Objects.nonNull(input.getType())) { + businessAttributeInfo.setType( + BusinessAttributeUtils.mapSchemaFieldDataType(input.getType())); + } + businessAttributeInfo.setLastModified( + new AuditStamp() + .setActor(UrnUtils.getUrn(context.getActorUrn())) + .setTime(System.currentTimeMillis())); + // 3. Write changes to GMS + return UrnUtils.getUrn( + _entityClient.ingestProposal( + AspectUtils.buildMetadataChangeProposal( + businessAttributeUrn, + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + businessAttributeInfo), + context.getAuthentication())); - @Nullable - public BusinessAttributeInfo getBusinessAttributeInfo(@Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { - Objects.requireNonNull(businessAttributeUrn, "businessAttributeUrn must not be null"); - Objects.requireNonNull(authentication, "authentication must not be null"); - final EntityResponse response = businessAttributeService.getBusinessAttributeEntityResponse(businessAttributeUrn, authentication); - if (response != null && response.getAspects().containsKey(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME)) { - return new BusinessAttributeInfo(response.getAspects().get(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME).getValue().data()); - } - // No aspect found - return null; + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); } + } + @Nullable + public BusinessAttributeInfo getBusinessAttributeInfo( + @Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { + Objects.requireNonNull(businessAttributeUrn, "businessAttributeUrn must not be null"); + Objects.requireNonNull(authentication, "authentication must not be null"); + final EntityResponse response = + businessAttributeService.getBusinessAttributeEntityResponse( + businessAttributeUrn, authentication); + if (response != null + && response.getAspects().containsKey(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME)) { + return new BusinessAttributeInfo( + response + .getAspects() + .get(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME) + .getValue() + .data()); + } + // No aspect found + return null; + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java index 1cffb3c585e9fe..5f2177994ffc0c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java @@ -262,7 +262,9 @@ private EntityType mapResourceTypeToEntityType(final String resourceType) { .getResourceType() .equals(resourceType)) { return EntityType.CORP_USER; - } else if (com.linkedin.metadata.authorization.PoliciesConfig.BUSINESS_ATTRIBUTE_PRIVILEGES.getResourceType().equals(resourceType)) { + } else if (com.linkedin.metadata.authorization.PoliciesConfig.BUSINESS_ATTRIBUTE_PRIVILEGES + .getResourceType() + .equals(resourceType)) { return EntityType.BUSINESS_ATTRIBUTE; } else { return null; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java index 9a36d0b70f1a6d..5f1ffb6a94b991 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/DescriptionUtils.java @@ -5,7 +5,6 @@ import com.datahub.authorization.ConjunctivePrivilegeGroup; import com.datahub.authorization.DisjunctivePrivilegeGroup; import com.google.common.collect.ImmutableList; - import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.urn.Urn; import com.linkedin.container.EditableContainerProperties; @@ -457,15 +456,22 @@ public static void updateDataProductDescription( } public static void updateBusinessAttributeDescription( - String newDescription, - Urn resourceUrn, - Urn actor, - EntityService entityService) { - BusinessAttributeInfo businessAttributeInfo = (BusinessAttributeInfo) EntityUtils.getAspectFromEntity( - resourceUrn.toString(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, entityService, new BusinessAttributeInfo()); + String newDescription, Urn resourceUrn, Urn actor, EntityService entityService) { + BusinessAttributeInfo businessAttributeInfo = + (BusinessAttributeInfo) + EntityUtils.getAspectFromEntity( + resourceUrn.toString(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, + new BusinessAttributeInfo()); if (businessAttributeInfo != null) { businessAttributeInfo.setDescription(newDescription); } - persistAspect(resourceUrn, Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo, actor, entityService); + persistAspect( + resourceUrn, + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + businessAttributeInfo, + actor, + entityService); } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java index 85fa5c34fae130..d1cf5ed9feb2ae 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateDescriptionResolver.java @@ -28,49 +28,49 @@ public class UpdateDescriptionResolver implements DataFetcher get(DataFetchingEnvironment environment) throws Exception { - final DescriptionUpdateInput input = + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final DescriptionUpdateInput input = bindArgument(environment.getArgument("input"), DescriptionUpdateInput.class); - Urn targetUrn = Urn.createFromString(input.getResourceUrn()); - log.info("Updating description. input: {}", input.toString()); - switch (targetUrn.getEntityType()) { - case Constants.DATASET_ENTITY_NAME: - return updateDatasetSchemaFieldDescription(targetUrn, input, environment.getContext()); - case Constants.CONTAINER_ENTITY_NAME: - 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.GLOSSARY_NODE_ENTITY_NAME: - return updateGlossaryNodeDescription(targetUrn, input, environment.getContext()); - case Constants.TAG_ENTITY_NAME: - return updateTagDescription(targetUrn, input, environment.getContext()); - case Constants.CORP_GROUP_ENTITY_NAME: - return updateCorpGroupDescription(targetUrn, input, environment.getContext()); - case Constants.NOTEBOOK_ENTITY_NAME: - return updateNotebookDescription(targetUrn, input, environment.getContext()); - case Constants.ML_MODEL_ENTITY_NAME: - return updateMlModelDescription(targetUrn, input, environment.getContext()); - case Constants.ML_MODEL_GROUP_ENTITY_NAME: - return updateMlModelGroupDescription(targetUrn, input, environment.getContext()); - case Constants.ML_FEATURE_TABLE_ENTITY_NAME: - return updateMlFeatureTableDescription(targetUrn, input, environment.getContext()); - case Constants.ML_FEATURE_ENTITY_NAME: - return updateMlFeatureDescription(targetUrn, input, environment.getContext()); - case Constants.ML_PRIMARY_KEY_ENTITY_NAME: - return updateMlPrimaryKeyDescription(targetUrn, input, environment.getContext()); - case Constants.DATA_PRODUCT_ENTITY_NAME: - return updateDataProductDescription(targetUrn, input, environment.getContext()); - case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME: - return updateBusinessAttributeDescription(targetUrn, input, environment.getContext()); - default: - throw new RuntimeException( - String.format( + Urn targetUrn = Urn.createFromString(input.getResourceUrn()); + log.info("Updating description. input: {}", input.toString()); + switch (targetUrn.getEntityType()) { + case Constants.DATASET_ENTITY_NAME: + return updateDatasetSchemaFieldDescription(targetUrn, input, environment.getContext()); + case Constants.CONTAINER_ENTITY_NAME: + 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.GLOSSARY_NODE_ENTITY_NAME: + return updateGlossaryNodeDescription(targetUrn, input, environment.getContext()); + case Constants.TAG_ENTITY_NAME: + return updateTagDescription(targetUrn, input, environment.getContext()); + case Constants.CORP_GROUP_ENTITY_NAME: + return updateCorpGroupDescription(targetUrn, input, environment.getContext()); + case Constants.NOTEBOOK_ENTITY_NAME: + return updateNotebookDescription(targetUrn, input, environment.getContext()); + case Constants.ML_MODEL_ENTITY_NAME: + return updateMlModelDescription(targetUrn, input, environment.getContext()); + case Constants.ML_MODEL_GROUP_ENTITY_NAME: + return updateMlModelGroupDescription(targetUrn, input, environment.getContext()); + case Constants.ML_FEATURE_TABLE_ENTITY_NAME: + return updateMlFeatureTableDescription(targetUrn, input, environment.getContext()); + case Constants.ML_FEATURE_ENTITY_NAME: + return updateMlFeatureDescription(targetUrn, input, environment.getContext()); + case Constants.ML_PRIMARY_KEY_ENTITY_NAME: + return updateMlPrimaryKeyDescription(targetUrn, input, environment.getContext()); + case Constants.DATA_PRODUCT_ENTITY_NAME: + return updateDataProductDescription(targetUrn, input, environment.getContext()); + case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME: + return updateBusinessAttributeDescription(targetUrn, input, environment.getContext()); + default: + throw new RuntimeException( + String.format( "Failed to update description. Unsupported resource type %s provided.", targetUrn)); - } } + } private CompletableFuture updateContainerDescription( Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { @@ -423,54 +423,54 @@ private CompletableFuture updateMlFeatureTableDescription( }); } - private CompletableFuture updateDataProductDescription(Urn targetUrn, DescriptionUpdateInput input, - QueryContext context) { - return CompletableFuture.supplyAsync( + private CompletableFuture updateDataProductDescription( + 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.updateDataProductDescription( - 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); - } + 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.updateDataProductDescription( + 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 updateBusinessAttributeDescription(Urn targetUrn, DescriptionUpdateInput input, - QueryContext context) { - return CompletableFuture.supplyAsync(() -> { - //check if user has the rights to update description for business attribute - if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn)) { - throw new AuthorizationException( - "Unauthorized to perform this action. Please contact your DataHub administrator."); - } - - //validate label input - DescriptionUtils.validateLabelInput(targetUrn, _entityService); - - try { - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - DescriptionUtils.updateBusinessAttributeDescription(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 updateBusinessAttributeDescription( + Urn targetUrn, DescriptionUpdateInput input, QueryContext context) { + return CompletableFuture.supplyAsync( + () -> { + // check if user has the rights to update description for business attribute + if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + + // validate label input + DescriptionUtils.validateLabelInput(targetUrn, _entityService); + + try { + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + DescriptionUtils.updateBusinessAttributeDescription( + 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); + } }); - } + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java index 40b3797929742d..e501ac7ae87e7a 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java @@ -55,25 +55,26 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw String.format("Failed to update %s. %s does not exist.", targetUrn, targetUrn)); } - switch (targetUrn.getEntityType()) { - case Constants.GLOSSARY_TERM_ENTITY_NAME: - return updateGlossaryTermName(targetUrn, input, environment.getContext()); - case Constants.GLOSSARY_NODE_ENTITY_NAME: - return updateGlossaryNodeName(targetUrn, input, environment.getContext()); - case Constants.DOMAIN_ENTITY_NAME: - return updateDomainName(targetUrn, input, environment.getContext()); - case Constants.CORP_GROUP_ENTITY_NAME: - return updateGroupName(targetUrn, input, environment.getContext()); - case Constants.DATA_PRODUCT_ENTITY_NAME: - return updateDataProductName(targetUrn, input, environment.getContext()); - case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME: - return updateBusinessAttributeName(targetUrn, input, environment.getContext()); - default: - throw new RuntimeException( - String.format("Failed to update name. Unsupported resource type %s provided.", targetUrn)); - } + switch (targetUrn.getEntityType()) { + case Constants.GLOSSARY_TERM_ENTITY_NAME: + return updateGlossaryTermName(targetUrn, input, environment.getContext()); + case Constants.GLOSSARY_NODE_ENTITY_NAME: + return updateGlossaryNodeName(targetUrn, input, environment.getContext()); + case Constants.DOMAIN_ENTITY_NAME: + return updateDomainName(targetUrn, input, environment.getContext()); + case Constants.CORP_GROUP_ENTITY_NAME: + return updateGroupName(targetUrn, input, environment.getContext()); + case Constants.DATA_PRODUCT_ENTITY_NAME: + return updateDataProductName(targetUrn, input, environment.getContext()); + case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME: + return updateBusinessAttributeName(targetUrn, input, environment.getContext()); + default: + throw new RuntimeException( + String.format( + "Failed to update name. Unsupported resource type %s provided.", targetUrn)); + } }); - } + } private Boolean updateGlossaryTermName( Urn targetUrn, UpdateNameInput input, QueryContext context) { @@ -257,53 +258,63 @@ private Boolean updateDataProductName( } } - dataProductProperties.setName(input.getName()); - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - persistAspect( + dataProductProperties.setName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect( targetUrn, Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME, dataProductProperties, actor, _entityService); - return true; - } catch (Exception e) { - throw new RuntimeException( + return true; + } catch (Exception e) { + throw new RuntimeException( String.format("Failed to perform update against input %s", input), e); - } } + } - private Boolean updateBusinessAttributeName( - Urn targetUrn, - UpdateNameInput input, - QueryContext context - ) { - if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { - throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); - } - try { - BusinessAttributeInfo businessAttributeInfo = (BusinessAttributeInfo) EntityUtils.getAspectFromEntity( - targetUrn.toString(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, _entityService, null); - if (businessAttributeInfo == null) { - throw new IllegalArgumentException("Business Attribute does not exist"); - } + private Boolean updateBusinessAttributeName( + Urn targetUrn, UpdateNameInput input, QueryContext context) { + if (!BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + try { + BusinessAttributeInfo businessAttributeInfo = + (BusinessAttributeInfo) + EntityUtils.getAspectFromEntity( + targetUrn.toString(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + _entityService, + null); + if (businessAttributeInfo == null) { + throw new IllegalArgumentException("Business Attribute does not exist"); + } - if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { - throw new DataHubGraphQLException( - String.format("\"%s\" already exists as Business Attribute. Please pick a unique name.", input.getName()), - DataHubGraphQLErrorCode.CONFLICT - ); - } + if (BusinessAttributeUtils.hasNameConflict(input.getName(), context, _entityClient)) { + throw new DataHubGraphQLException( + String.format( + "\"%s\" already exists as Business Attribute. Please pick a unique name.", + input.getName()), + DataHubGraphQLErrorCode.CONFLICT); + } - businessAttributeInfo.setFieldPath(input.getName()); - businessAttributeInfo.setName(input.getName()); - Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); - persistAspect(targetUrn, Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo, actor, _entityService); - return true; - } catch (DataHubGraphQLException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); - } + businessAttributeInfo.setFieldPath(input.getName()); + businessAttributeInfo.setName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect( + targetUrn, + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + businessAttributeInfo, + actor, + _entityService); + return true; + } catch (DataHubGraphQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException( + String.format("Failed to perform update against input %s", input), e); } + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java index d3fab88e91e2a5..a01fe020fd8bdc 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java @@ -1,6 +1,5 @@ package com.linkedin.datahub.graphql.resolvers.mutate.util; - import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; @@ -13,103 +12,104 @@ import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; import com.linkedin.r2.RemoteInvocationException; +import com.linkedin.schema.ArrayType; +import com.linkedin.schema.BooleanType; +import com.linkedin.schema.BytesType; +import com.linkedin.schema.DateType; import com.linkedin.schema.EnumType; import com.linkedin.schema.FixedType; import com.linkedin.schema.MapType; -import com.linkedin.schema.BooleanType; -import com.linkedin.schema.StringType; -import com.linkedin.schema.ArrayType; -import com.linkedin.schema.BytesType; import com.linkedin.schema.NumberType; -import com.linkedin.schema.TimeType; -import com.linkedin.schema.DateType; import com.linkedin.schema.SchemaFieldDataType; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nonnull; +import com.linkedin.schema.StringType; +import com.linkedin.schema.TimeType; import java.util.Objects; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; @Slf4j public class BusinessAttributeUtils { - private static final Integer DEFAULT_START = 0; - private static final Integer DEFAULT_COUNT = 1000; - private static final String DEFAULT_QUERY = ""; - private static final String NAME_INDEX_FIELD_NAME = "name"; + private static final Integer DEFAULT_START = 0; + private static final Integer DEFAULT_COUNT = 1000; + private static final String DEFAULT_QUERY = ""; + private static final String NAME_INDEX_FIELD_NAME = "name"; - private BusinessAttributeUtils() { - } + private BusinessAttributeUtils() {} - public static boolean hasNameConflict(String name, QueryContext context, EntityClient entityClient) { - Filter filter = buildNameFilter(name); - try { - final SearchResult gmsResult = entityClient.search( - Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, - DEFAULT_QUERY, - filter, - null, - DEFAULT_START, - DEFAULT_COUNT, - context.getAuthentication(), - new SearchFlags().setFulltext(true)); - return gmsResult.getNumEntities() > 0; - } catch (RemoteInvocationException e) { - throw new RuntimeException("Failed to fetch Business Attributes", e); - } + public static boolean hasNameConflict( + String name, QueryContext context, EntityClient entityClient) { + Filter filter = buildNameFilter(name); + try { + final SearchResult gmsResult = + entityClient.search( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + DEFAULT_QUERY, + filter, + null, + DEFAULT_START, + DEFAULT_COUNT, + context.getAuthentication(), + new SearchFlags().setFulltext(true)); + return gmsResult.getNumEntities() > 0; + } catch (RemoteInvocationException e) { + throw new RuntimeException("Failed to fetch Business Attributes", e); } + } - private static Filter buildNameFilter(String name) { - return new Filter().setOr( - new ConjunctiveCriterionArray( - new ConjunctiveCriterion().setAnd(buildNameCriterion(name)) - ) - ); - } + private static Filter buildNameFilter(String name) { + return new Filter() + .setOr( + new ConjunctiveCriterionArray( + new ConjunctiveCriterion().setAnd(buildNameCriterion(name)))); + } - private static CriterionArray buildNameCriterion(@Nonnull final String name) { - return new CriterionArray(new Criterion() - .setField(NAME_INDEX_FIELD_NAME) - .setValue(name) - .setCondition(Condition.EQUAL)); - } + private static CriterionArray buildNameCriterion(@Nonnull final String name) { + return new CriterionArray( + new Criterion() + .setField(NAME_INDEX_FIELD_NAME) + .setValue(name) + .setCondition(Condition.EQUAL)); + } - public static SchemaFieldDataType mapSchemaFieldDataType(com.linkedin.datahub.graphql.generated.SchemaFieldDataType type) { - if (Objects.isNull(type)) { - return null; - } - SchemaFieldDataType schemaFieldDataType = new SchemaFieldDataType(); - switch (type) { - case BYTES: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new BytesType())); - return schemaFieldDataType; - case FIXED: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new FixedType())); - return schemaFieldDataType; - case ENUM: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new EnumType())); - return schemaFieldDataType; - case MAP: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new MapType())); - return schemaFieldDataType; - case TIME: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new TimeType())); - return schemaFieldDataType; - case BOOLEAN: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new BooleanType())); - return schemaFieldDataType; - case STRING: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new StringType())); - return schemaFieldDataType; - case NUMBER: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new NumberType())); - return schemaFieldDataType; - case DATE: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new DateType())); - return schemaFieldDataType; - case ARRAY: - schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new ArrayType())); - return schemaFieldDataType; - default: - return null; - } + public static SchemaFieldDataType mapSchemaFieldDataType( + com.linkedin.datahub.graphql.generated.SchemaFieldDataType type) { + if (Objects.isNull(type)) { + return null; + } + SchemaFieldDataType schemaFieldDataType = new SchemaFieldDataType(); + switch (type) { + case BYTES: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new BytesType())); + return schemaFieldDataType; + case FIXED: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new FixedType())); + return schemaFieldDataType; + case ENUM: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new EnumType())); + return schemaFieldDataType; + case MAP: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new MapType())); + return schemaFieldDataType; + case TIME: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new TimeType())); + return schemaFieldDataType; + case BOOLEAN: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new BooleanType())); + return schemaFieldDataType; + case STRING: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new StringType())); + return schemaFieldDataType; + case NUMBER: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new NumberType())); + return schemaFieldDataType; + case DATE: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new DateType())); + return schemaFieldDataType; + case ARRAY: + schemaFieldDataType.setType(SchemaFieldDataType.Type.create(new ArrayType())); + return schemaFieldDataType; + default: + return null; } + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java index 72bfb10463c8e2..963d90c2e5692c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java @@ -499,7 +499,8 @@ private static MetadataChangeProposal buildRemoveTermsProposal( // Case 1: Removing terms from a top-level entity Urn targetUrn = Urn.createFromString(resource.getResourceUrn()); if (targetUrn.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { - return buildRemoveTermsToBusinessAttributeProposal(termUrns, resource, actor, entityService); + return buildRemoveTermsToBusinessAttributeProposal( + termUrns, resource, actor, entityService); } return buildRemoveTermsToEntityProposal(termUrns, resource, actor, entityService); } else { @@ -634,68 +635,83 @@ private static GlossaryTermAssociationArray removeTermsIfExists( } private static MetadataChangeProposal buildAddTagsToBusinessAttributeProposal( - List tagUrns, - ResourceRefInput resource, - Urn actor, - EntityService entityService - ) throws URISyntaxException { + List tagUrns, ResourceRefInput resource, Urn actor, EntityService entityService) + throws URISyntaxException { BusinessAttributeInfo businessAttributeInfo = - (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, - entityService, new GlobalTags()); + (BusinessAttributeInfo) + EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, + new GlobalTags()); if (!businessAttributeInfo.hasGlobalTags()) { businessAttributeInfo.setGlobalTags(new GlobalTags()); } addTagsIfNotExists(businessAttributeInfo.getGlobalTags(), tagUrns); - return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + return buildMetadataChangeProposalWithUrn( + UrnUtils.getUrn(resource.getResourceUrn()), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + businessAttributeInfo); } private static MetadataChangeProposal buildAddTermsToBusinessAttributeProposal( - List termUrns, - ResourceRefInput resource, - Urn actor, - EntityService entityService - ) throws URISyntaxException { + List termUrns, ResourceRefInput resource, Urn actor, EntityService entityService) + throws URISyntaxException { BusinessAttributeInfo businessAttributeInfo = - (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, - entityService, new GlossaryTerms()); + (BusinessAttributeInfo) + EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, + new GlossaryTerms()); if (!businessAttributeInfo.hasGlossaryTerms()) { businessAttributeInfo.setGlossaryTerms(new GlossaryTerms()); } businessAttributeInfo.getGlossaryTerms().setAuditStamp(EntityUtils.getAuditStamp(actor)); addTermsIfNotExists(businessAttributeInfo.getGlossaryTerms(), termUrns); - return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + return buildMetadataChangeProposalWithUrn( + UrnUtils.getUrn(resource.getResourceUrn()), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + businessAttributeInfo); } private static MetadataChangeProposal buildRemoveTagsToBusinessAttributeProposal( - List tagUrns, - ResourceRefInput resource, - Urn actor, - EntityService entityService) { + List tagUrns, ResourceRefInput resource, Urn actor, EntityService entityService) { BusinessAttributeInfo businessAttributeInfo = - (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, - entityService, new GlobalTags()); + (BusinessAttributeInfo) + EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, + new GlobalTags()); if (!businessAttributeInfo.hasGlobalTags()) { businessAttributeInfo.setGlobalTags(new GlobalTags()); } removeTagsIfExists(businessAttributeInfo.getGlobalTags(), tagUrns); - return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + return buildMetadataChangeProposalWithUrn( + UrnUtils.getUrn(resource.getResourceUrn()), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + businessAttributeInfo); } private static MetadataChangeProposal buildRemoveTermsToBusinessAttributeProposal( - List termUrns, - ResourceRefInput resource, - Urn actor, - EntityService entityService) { + List termUrns, ResourceRefInput resource, Urn actor, EntityService entityService) { BusinessAttributeInfo businessAttributeInfo = - (BusinessAttributeInfo) EntityUtils.getAspectFromEntity(resource.getResourceUrn(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, - entityService, new GlossaryTerms()); + (BusinessAttributeInfo) + EntityUtils.getAspectFromEntity( + resource.getResourceUrn(), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + entityService, + new GlossaryTerms()); if (!businessAttributeInfo.hasGlossaryTerms()) { businessAttributeInfo.setGlossaryTerms(new GlossaryTerms()); } removeTermsIfExists(businessAttributeInfo.getGlossaryTerms(), termUrns); - return buildMetadataChangeProposalWithUrn(UrnUtils.getUrn(resource.getResourceUrn()), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, businessAttributeInfo); + return buildMetadataChangeProposalWithUrn( + UrnUtils.getUrn(resource.getResourceUrn()), + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + businessAttributeInfo); } - } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java index ee8a23ce4bb2a0..43b8c60454fc44 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java @@ -73,7 +73,6 @@ private SearchUtils() {} EntityType.NOTEBOOK, EntityType.BUSINESS_ATTRIBUTE); - /** Entities that are part of autocomplete by default in Auto Complete Across Entities */ public static final List AUTO_COMPLETE_ENTITY_TYPES = ImmutableList.of( diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java index 964c943369ef3d..063e29c70648a9 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java @@ -1,5 +1,12 @@ package com.linkedin.datahub.graphql.types.businessattribute; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME; +import static com.linkedin.metadata.Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME; +import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME; +import static com.linkedin.metadata.Constants.STATUS_ASPECT_NAME; + import com.google.common.collect.ImmutableSet; import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; @@ -22,10 +29,6 @@ import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; import graphql.execution.DataFetcherResult; -import lombok.RequiredArgsConstructor; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -33,81 +36,102 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; - -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME; -import static com.linkedin.metadata.Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME; -import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME; -import static com.linkedin.metadata.Constants.STATUS_ASPECT_NAME; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class BusinessAttributeType implements SearchableEntityType { - public static final Set ASPECTS_TO_FETCH = ImmutableSet.of( - BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, - BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME, - OWNERSHIP_ASPECT_NAME, - INSTITUTIONAL_MEMORY_ASPECT_NAME, - STATUS_ASPECT_NAME - ); - private static final Set FACET_FIELDS = ImmutableSet.of(""); - private final EntityClient _entityClient; + public static final Set ASPECTS_TO_FETCH = + ImmutableSet.of( + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME, + OWNERSHIP_ASPECT_NAME, + INSTITUTIONAL_MEMORY_ASPECT_NAME, + STATUS_ASPECT_NAME); + private static final Set FACET_FIELDS = ImmutableSet.of(""); + private final EntityClient _entityClient; - @Override - public EntityType type() { - return EntityType.BUSINESS_ATTRIBUTE; - } + @Override + public EntityType type() { + return EntityType.BUSINESS_ATTRIBUTE; + } - @Override - public Function getKeyProvider() { - return Entity::getUrn; - } + @Override + public Function getKeyProvider() { + return Entity::getUrn; + } - @Override - public Class objectClass() { - return BusinessAttribute.class; - } + @Override + public Class objectClass() { + return BusinessAttribute.class; + } - @Override - public List> batchLoad(@Nonnull List urns, @Nonnull QueryContext context) throws Exception { - final List businessAttributeUrns = urns.stream() - .map(UrnUtils::getUrn) - .collect(Collectors.toList()); + @Override + public List> batchLoad( + @Nonnull List urns, @Nonnull QueryContext context) throws Exception { + final List businessAttributeUrns = + urns.stream().map(UrnUtils::getUrn).collect(Collectors.toList()); - try { - final Map businessAttributeMap = _entityClient.batchGetV2(BUSINESS_ATTRIBUTE_ENTITY_NAME, - new HashSet<>(businessAttributeUrns), ASPECTS_TO_FETCH, context.getAuthentication()); + try { + final Map businessAttributeMap = + _entityClient.batchGetV2( + BUSINESS_ATTRIBUTE_ENTITY_NAME, + new HashSet<>(businessAttributeUrns), + ASPECTS_TO_FETCH, + context.getAuthentication()); - final List gmsResults = new ArrayList<>(); - for (Urn urn : businessAttributeUrns) { - gmsResults.add(businessAttributeMap.getOrDefault(urn, null)); - } - return gmsResults.stream() - .map(gmsResult -> gmsResult == null ? null - : DataFetcherResult.newResult() - .data(BusinessAttributeMapper.map(gmsResult)) - .build()) - .collect(Collectors.toList()); - } catch (Exception e) { - throw new RuntimeException("Failed to batch load Business Attributes", e); - } + final List gmsResults = new ArrayList<>(); + for (Urn urn : businessAttributeUrns) { + gmsResults.add(businessAttributeMap.getOrDefault(urn, null)); + } + return gmsResults.stream() + .map( + gmsResult -> + gmsResult == null + ? null + : DataFetcherResult.newResult() + .data(BusinessAttributeMapper.map(gmsResult)) + .build()) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Failed to batch load Business Attributes", e); } + } - @Override - public SearchResults search(@Nonnull String query, @Nullable List filters, - int start, int count, @Nonnull QueryContext context) throws Exception { - final Map facetFilters = ResolverUtils.buildFacetFilters(filters, FACET_FIELDS); - final SearchResult searchResult = _entityClient.search( - "businessAttribute", query, facetFilters, start, count, context.getAuthentication(), new SearchFlags().setFulltext(true)); - return UrnSearchResultsMapper.map(searchResult); - } + @Override + public SearchResults search( + @Nonnull String query, + @Nullable List filters, + int start, + int count, + @Nonnull QueryContext context) + throws Exception { + final Map facetFilters = ResolverUtils.buildFacetFilters(filters, FACET_FIELDS); + final SearchResult searchResult = + _entityClient.search( + "businessAttribute", + query, + facetFilters, + start, + count, + context.getAuthentication(), + new SearchFlags().setFulltext(true)); + return UrnSearchResultsMapper.map(searchResult); + } - @Override - public AutoCompleteResults autoComplete(@Nonnull String query, @Nullable String field, - @Nullable Filter filters, int limit, @Nonnull QueryContext context) throws Exception { - final AutoCompleteResult result = _entityClient.autoComplete( - "businessAttribute", query, filters, limit, context.getAuthentication()); - return AutoCompleteResultsMapper.map(result); - } + @Override + public AutoCompleteResults autoComplete( + @Nonnull String query, + @Nullable String field, + @Nullable Filter filters, + int limit, + @Nonnull QueryContext context) + throws Exception { + final AutoCompleteResult result = + _entityClient.autoComplete( + "businessAttribute", query, filters, limit, context.getAuthentication()); + return AutoCompleteResultsMapper.map(result); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java index e881a3a24594bc..59815900e1dffd 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -1,5 +1,8 @@ package com.linkedin.datahub.graphql.types.businessattribute.mappers; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME; + import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.Ownership; import com.linkedin.common.urn.Urn; @@ -16,90 +19,97 @@ import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper; import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EnvelopedAspectMap; - import javax.annotation.Nonnull; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; -import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME; - public class BusinessAttributeMapper implements ModelMapper { - public static final BusinessAttributeMapper INSTANCE = new BusinessAttributeMapper(); + public static final BusinessAttributeMapper INSTANCE = new BusinessAttributeMapper(); - public static BusinessAttribute map(@Nonnull final EntityResponse entityResponse) { - return INSTANCE.apply(entityResponse); - } + public static BusinessAttribute map(@Nonnull final EntityResponse entityResponse) { + return INSTANCE.apply(entityResponse); + } - @Override - public BusinessAttribute apply(@Nonnull final EntityResponse entityResponse) { - BusinessAttribute result = new BusinessAttribute(); - result.setUrn(entityResponse.getUrn().toString()); - result.setType(EntityType.BUSINESS_ATTRIBUTE); + @Override + public BusinessAttribute apply(@Nonnull final EntityResponse entityResponse) { + BusinessAttribute result = new BusinessAttribute(); + result.setUrn(entityResponse.getUrn().toString()); + result.setType(EntityType.BUSINESS_ATTRIBUTE); - EnvelopedAspectMap aspectMap = entityResponse.getAspects(); - MappingHelper mappingHelper = new MappingHelper<>(aspectMap, result); - mappingHelper.mapToResult(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, ((businessAttribute, dataMap) -> - mapBusinessAttributeInfo(businessAttribute, dataMap, entityResponse.getUrn()))); - mappingHelper.mapToResult(OWNERSHIP_ASPECT_NAME, (businessAttribute, dataMap) -> - businessAttribute.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityResponse.getUrn()))); - return mappingHelper.getResult(); - } + EnvelopedAspectMap aspectMap = entityResponse.getAspects(); + MappingHelper mappingHelper = new MappingHelper<>(aspectMap, result); + mappingHelper.mapToResult( + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + ((businessAttribute, dataMap) -> + mapBusinessAttributeInfo(businessAttribute, dataMap, entityResponse.getUrn()))); + mappingHelper.mapToResult( + OWNERSHIP_ASPECT_NAME, + (businessAttribute, dataMap) -> + businessAttribute.setOwnership( + OwnershipMapper.map(new Ownership(dataMap), entityResponse.getUrn()))); + return mappingHelper.getResult(); + } - private void mapBusinessAttributeInfo(BusinessAttribute businessAttribute, DataMap dataMap, Urn entityUrn) { - BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(dataMap); - com.linkedin.datahub.graphql.generated.BusinessAttributeInfo attributeInfo = new com.linkedin.datahub.graphql.generated.BusinessAttributeInfo(); - if (businessAttributeInfo.hasFieldPath()) { - attributeInfo.setName(businessAttributeInfo.getFieldPath()); - } - if (businessAttributeInfo.hasDescription()) { - attributeInfo.setDescription(businessAttributeInfo.getDescription()); - } - if (businessAttributeInfo.hasCreated()) { - attributeInfo.setCreated(AuditStampMapper.map(businessAttributeInfo.getCreated())); - } - if (businessAttributeInfo.hasLastModified()) { - attributeInfo.setLastModified(AuditStampMapper.map(businessAttributeInfo.getLastModified())); - } - if (businessAttributeInfo.hasGlobalTags()) { - attributeInfo.setTags(GlobalTagsMapper.map(businessAttributeInfo.getGlobalTags(), entityUrn)); - } - if (businessAttributeInfo.hasGlossaryTerms()) { - attributeInfo.setGlossaryTerms(GlossaryTermsMapper.map(businessAttributeInfo.getGlossaryTerms(), entityUrn)); - } - if (businessAttributeInfo.hasType()) { - attributeInfo.setType(mapSchemaFieldDataType(businessAttributeInfo.getType())); - } - if (businessAttributeInfo.hasCustomProperties()) { - attributeInfo.setCustomProperties(CustomPropertiesMapper.map(businessAttributeInfo.getCustomProperties(), entityUrn)); - } - businessAttribute.setProperties(attributeInfo); + private void mapBusinessAttributeInfo( + BusinessAttribute businessAttribute, DataMap dataMap, Urn entityUrn) { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(dataMap); + com.linkedin.datahub.graphql.generated.BusinessAttributeInfo attributeInfo = + new com.linkedin.datahub.graphql.generated.BusinessAttributeInfo(); + if (businessAttributeInfo.hasFieldPath()) { + attributeInfo.setName(businessAttributeInfo.getFieldPath()); + } + if (businessAttributeInfo.hasDescription()) { + attributeInfo.setDescription(businessAttributeInfo.getDescription()); + } + if (businessAttributeInfo.hasCreated()) { + attributeInfo.setCreated(AuditStampMapper.map(businessAttributeInfo.getCreated())); + } + if (businessAttributeInfo.hasLastModified()) { + attributeInfo.setLastModified(AuditStampMapper.map(businessAttributeInfo.getLastModified())); + } + if (businessAttributeInfo.hasGlobalTags()) { + attributeInfo.setTags(GlobalTagsMapper.map(businessAttributeInfo.getGlobalTags(), entityUrn)); + } + if (businessAttributeInfo.hasGlossaryTerms()) { + attributeInfo.setGlossaryTerms( + GlossaryTermsMapper.map(businessAttributeInfo.getGlossaryTerms(), entityUrn)); + } + if (businessAttributeInfo.hasType()) { + attributeInfo.setType(mapSchemaFieldDataType(businessAttributeInfo.getType())); + } + if (businessAttributeInfo.hasCustomProperties()) { + attributeInfo.setCustomProperties( + CustomPropertiesMapper.map(businessAttributeInfo.getCustomProperties(), entityUrn)); } + businessAttribute.setProperties(attributeInfo); + } - private SchemaFieldDataType mapSchemaFieldDataType(@Nonnull final com.linkedin.schema.SchemaFieldDataType dataTypeUnion) { - final com.linkedin.schema.SchemaFieldDataType.Type type = dataTypeUnion.getType(); - if (type.isBytesType()) { - return SchemaFieldDataType.BYTES; - } else if (type.isFixedType()) { - return SchemaFieldDataType.FIXED; - } else if (type.isBooleanType()) { - return SchemaFieldDataType.BOOLEAN; - } else if (type.isStringType()) { - return SchemaFieldDataType.STRING; - } else if (type.isNumberType()) { - return SchemaFieldDataType.NUMBER; - } else if (type.isDateType()) { - return SchemaFieldDataType.DATE; - } else if (type.isTimeType()) { - return SchemaFieldDataType.TIME; - } else if (type.isEnumType()) { - return SchemaFieldDataType.ENUM; - } else if (type.isArrayType()) { - return SchemaFieldDataType.ARRAY; - } else if (type.isMapType()) { - return SchemaFieldDataType.MAP; - } else { - throw new RuntimeException(String.format("Unrecognized SchemaFieldDataType provided %s", - type.memberType().toString())); - } + private SchemaFieldDataType mapSchemaFieldDataType( + @Nonnull final com.linkedin.schema.SchemaFieldDataType dataTypeUnion) { + final com.linkedin.schema.SchemaFieldDataType.Type type = dataTypeUnion.getType(); + if (type.isBytesType()) { + return SchemaFieldDataType.BYTES; + } else if (type.isFixedType()) { + return SchemaFieldDataType.FIXED; + } else if (type.isBooleanType()) { + return SchemaFieldDataType.BOOLEAN; + } else if (type.isStringType()) { + return SchemaFieldDataType.STRING; + } else if (type.isNumberType()) { + return SchemaFieldDataType.NUMBER; + } else if (type.isDateType()) { + return SchemaFieldDataType.DATE; + } else if (type.isTimeType()) { + return SchemaFieldDataType.TIME; + } else if (type.isEnumType()) { + return SchemaFieldDataType.ENUM; + } else if (type.isArrayType()) { + return SchemaFieldDataType.ARRAY; + } else if (type.isMapType()) { + return SchemaFieldDataType.MAP; + } else { + throw new RuntimeException( + String.format( + "Unrecognized SchemaFieldDataType provided %s", type.memberType().toString())); } + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java index 71f5390d139015..c374d6a99aedb1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java @@ -5,35 +5,36 @@ import com.linkedin.datahub.graphql.generated.BusinessAttributeAssociation; import com.linkedin.datahub.graphql.generated.BusinessAttributes; import com.linkedin.datahub.graphql.generated.EntityType; +import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; - public class BusinessAttributesMapper { - private static final Logger _logger = LoggerFactory.getLogger(BusinessAttributesMapper.class.getName()); - public static final BusinessAttributesMapper INSTANCE = new BusinessAttributesMapper(); - - public static BusinessAttributes map( - @Nonnull final com.linkedin.businessattribute.BusinessAttributeAssociation businessAttribute, - @Nonnull final Urn entityUrn - ) { - return INSTANCE.apply(businessAttribute, entityUrn); - } - - private BusinessAttributes apply(@Nonnull com.linkedin.businessattribute.BusinessAttributeAssociation businessAttributes, @Nonnull Urn entityUrn) { - final BusinessAttributeAssociation businessAttributeAssociation = new BusinessAttributeAssociation(); - final BusinessAttributes result = new BusinessAttributes(); - final BusinessAttribute businessAttribute = new BusinessAttribute(); - businessAttribute.setUrn(businessAttributes.getDestinationUrn().toString()); - businessAttribute.setType(EntityType.BUSINESS_ATTRIBUTE); - - businessAttributeAssociation.setBusinessAttribute(businessAttribute); - - businessAttributeAssociation.setAssociatedUrn(entityUrn.toString()); - result.setBusinessAttribute(businessAttributeAssociation); - return result; - } - + private static final Logger _logger = + LoggerFactory.getLogger(BusinessAttributesMapper.class.getName()); + public static final BusinessAttributesMapper INSTANCE = new BusinessAttributesMapper(); + + public static BusinessAttributes map( + @Nonnull final com.linkedin.businessattribute.BusinessAttributeAssociation businessAttribute, + @Nonnull final Urn entityUrn) { + return INSTANCE.apply(businessAttribute, entityUrn); + } + + private BusinessAttributes apply( + @Nonnull com.linkedin.businessattribute.BusinessAttributeAssociation businessAttributes, + @Nonnull Urn entityUrn) { + final BusinessAttributeAssociation businessAttributeAssociation = + new BusinessAttributeAssociation(); + final BusinessAttributes result = new BusinessAttributes(); + final BusinessAttribute businessAttribute = new BusinessAttribute(); + businessAttribute.setUrn(businessAttributes.getDestinationUrn().toString()); + businessAttribute.setType(EntityType.BUSINESS_ATTRIBUTE); + + businessAttributeAssociation.setBusinessAttribute(businessAttribute); + + businessAttributeAssociation.setAssociatedUrn(entityUrn.toString()); + result.setBusinessAttribute(businessAttributeAssociation); + return result; + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java index 5ad5451de27bf7..0a6b5302e74174 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java @@ -293,27 +293,28 @@ private DisjunctivePrivilegeGroup getAuthorizedPrivileges(final DatasetUpdateInp new ConjunctivePrivilegeGroup( ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType())); - List specificPrivileges = new ArrayList<>(); - if (updateInput.getInstitutionalMemory() != null) { - specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_DOC_LINKS_PRIVILEGE.getType()); - } - if (updateInput.getOwnership() != null) { - specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_OWNERS_PRIVILEGE.getType()); - } - if (updateInput.getDeprecation() != null) { - specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_STATUS_PRIVILEGE.getType()); - } - if (updateInput.getEditableProperties() != null) { - specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_DOCS_PRIVILEGE.getType()); - } - if (updateInput.getGlobalTags() != null) { - specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_TAGS_PRIVILEGE.getType()); - } - if (updateInput.getEditableSchemaMetadata() != null) { - specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_TAGS_PRIVILEGE.getType()); - specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_DESCRIPTION_PRIVILEGE.getType()); - specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType()); - } + List specificPrivileges = new ArrayList<>(); + if (updateInput.getInstitutionalMemory() != null) { + specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_DOC_LINKS_PRIVILEGE.getType()); + } + if (updateInput.getOwnership() != null) { + specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_OWNERS_PRIVILEGE.getType()); + } + if (updateInput.getDeprecation() != null) { + specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_STATUS_PRIVILEGE.getType()); + } + if (updateInput.getEditableProperties() != null) { + specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_DOCS_PRIVILEGE.getType()); + } + if (updateInput.getGlobalTags() != null) { + specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_TAGS_PRIVILEGE.getType()); + } + if (updateInput.getEditableSchemaMetadata() != null) { + specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_TAGS_PRIVILEGE.getType()); + specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_DESCRIPTION_PRIVILEGE.getType()); + specificPrivileges.add( + PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType()); + } final ConjunctivePrivilegeGroup specificPrivilegeGroup = new ConjunctivePrivilegeGroup(specificPrivileges); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java index 0bf025d9243f74..c452316894f2ba 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java @@ -5,13 +5,13 @@ import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper; import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper; import com.linkedin.schema.EditableSchemaFieldInfo; +import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; - public class EditableSchemaFieldInfoMapper { - private static final Logger _logger = LoggerFactory.getLogger(EditableSchemaFieldInfoMapper.class.getName()); + private static final Logger _logger = + LoggerFactory.getLogger(EditableSchemaFieldInfoMapper.class.getName()); public static final EditableSchemaFieldInfoMapper INSTANCE = new EditableSchemaFieldInfoMapper(); @@ -38,8 +38,9 @@ public com.linkedin.datahub.graphql.generated.EditableSchemaFieldInfo apply( result.setGlossaryTerms(GlossaryTermsMapper.map(input.getGlossaryTerms(), entityUrn)); } if (input.hasBusinessAttribute()) { - result.setBusinessAttributes(BusinessAttributesMapper.map(input.getBusinessAttribute(), entityUrn)); - } - return result; + result.setBusinessAttributes( + BusinessAttributesMapper.map(input.getBusinessAttribute(), entityUrn)); } + return result; + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java index 7064d12bca322d..9fcda136d2d057 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java @@ -1,5 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + import com.datahub.authentication.Authentication; import com.linkedin.businessattribute.BusinessAttributeAssociation; import com.linkedin.common.urn.Urn; @@ -19,162 +24,187 @@ import com.linkedin.schema.SchemaFieldArray; import com.linkedin.schema.SchemaMetadata; import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.ExecutionException; import org.mockito.Mockito; import org.testng.annotations.Test; -import java.util.concurrent.ExecutionException; - -import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; -import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; - public class AddBusinessAttributeResolverTest { - private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; - private static final String RESOURCE_URN = "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; - private static final String SUB_RESOURCE = "name"; - private EntityClient mockClient; - private EntityService mockService; - private QueryContext mockContext; - private DataFetchingEnvironment mockEnv; - private Authentication mockAuthentication; - private void init() { - mockClient = Mockito.mock(EntityClient.class); - mockService = getMockEntityService(); - mockEnv = Mockito.mock(DataFetchingEnvironment.class); - mockAuthentication = Mockito.mock(Authentication.class); - } - private void setupAllowContext() { - mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - @Test - public void testSuccess() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - - - AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); - addBusinessAttributeResolver.get(mockEnv).get(); - - Mockito.verify(mockClient, Mockito.times(1)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - - } - - @Test - public void testBusinessAttributeAlreadyAdded() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - Mockito.when(EntityUtils.getAspectFromEntity( - RESOURCE_URN, - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - mockService, null) - ).thenReturn(editableSchemaMetadata()); - - - AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); - ExecutionException exception = expectThrows(ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); - assertTrue(exception.getCause().getMessage().equals( - String.format("Failed to add Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN - ))); - - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - } - - @Test - public void testBusinessAttributeNotExists() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(false); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - - AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); - RuntimeException exception = expectThrows(RuntimeException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); - assertTrue(exception.getMessage().equals( - String.format("This urn does not exist: %s", BUSINESS_ATTRIBUTE_URN))); - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - } - - @Test - public void testResourceNotExists() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(false); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - - AddBusinessAttributeResolver addBusinessAttributeResolver = new AddBusinessAttributeResolver(mockClient, mockService); - ExecutionException exception = expectThrows(ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); - assertTrue(exception.getCause().getMessage().equals( - String.format("Failed to add Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN - ))); - - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - } - - @Test - public void testNotAuthorized() throws Exception { - - } - public AddBusinessAttributeInput addBusinessAttributeInput() { - AddBusinessAttributeInput addBusinessAttributeInput = new AddBusinessAttributeInput(); - addBusinessAttributeInput.setBusinessAttributeUrn(BUSINESS_ATTRIBUTE_URN); - addBusinessAttributeInput.setResourceUrn(resourceRefInput()); - return addBusinessAttributeInput; - } - - private ResourceRefInput resourceRefInput() { - ResourceRefInput resourceRefInput = new ResourceRefInput(); - resourceRefInput.setResourceUrn(RESOURCE_URN); - resourceRefInput.setSubResource(SUB_RESOURCE); - resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); - return resourceRefInput; - } - - private SchemaMetadata schemaMetadata() { - SchemaMetadata schemaMetadata = new SchemaMetadata(); - SchemaFieldArray schemaFields = new SchemaFieldArray(); - SchemaField schemaField = new SchemaField(); - schemaField.setFieldPath(SUB_RESOURCE); - schemaFields.add(schemaField); - schemaMetadata.setFields(schemaFields); - return schemaMetadata; - } - - private EditableSchemaMetadata editableSchemaMetadata() { - EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); - EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); - EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); - editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); - editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); - editableSchemaFieldInfos.add(editableSchemaFieldInfo); - editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); - return editableSchemaMetadata; - } + private static final String BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final String RESOURCE_URN = + "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; + private static final String SUB_RESOURCE = "name"; + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when( + mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + .thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + Mockito.when( + mockService.getAspect( + Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + AddBusinessAttributeResolver addBusinessAttributeResolver = + new AddBusinessAttributeResolver(mockClient, mockService); + addBusinessAttributeResolver.get(mockEnv).get(); + + Mockito.verify(mockClient, Mockito.times(1)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + } + + @Test + public void testBusinessAttributeAlreadyAdded() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when( + mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + .thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + Mockito.when( + mockService.getAspect( + Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + Mockito.when( + EntityUtils.getAspectFromEntity( + RESOURCE_URN, Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, mockService, null)) + .thenReturn(editableSchemaMetadata()); + + AddBusinessAttributeResolver addBusinessAttributeResolver = + new AddBusinessAttributeResolver(mockClient, mockService); + ExecutionException exception = + expectThrows( + ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); + assertTrue( + exception + .getCause() + .getMessage() + .equals( + String.format( + "Failed to add Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + } + + @Test + public void testBusinessAttributeNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when( + mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + .thenReturn(false); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + Mockito.when( + mockService.getAspect( + Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + AddBusinessAttributeResolver addBusinessAttributeResolver = + new AddBusinessAttributeResolver(mockClient, mockService); + RuntimeException exception = + expectThrows(RuntimeException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); + assertTrue( + exception + .getMessage() + .equals(String.format("This urn does not exist: %s", BUSINESS_ATTRIBUTE_URN))); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + } + + @Test + public void testResourceNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when( + mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + .thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(false); + Mockito.when( + mockService.getAspect( + Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + AddBusinessAttributeResolver addBusinessAttributeResolver = + new AddBusinessAttributeResolver(mockClient, mockService); + ExecutionException exception = + expectThrows( + ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); + assertTrue( + exception + .getCause() + .getMessage() + .equals( + String.format( + "Failed to add Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + } + + @Test + public void testNotAuthorized() throws Exception {} + + public AddBusinessAttributeInput addBusinessAttributeInput() { + AddBusinessAttributeInput addBusinessAttributeInput = new AddBusinessAttributeInput(); + addBusinessAttributeInput.setBusinessAttributeUrn(BUSINESS_ATTRIBUTE_URN); + addBusinessAttributeInput.setResourceUrn(resourceRefInput()); + return addBusinessAttributeInput; + } + + private ResourceRefInput resourceRefInput() { + ResourceRefInput resourceRefInput = new ResourceRefInput(); + resourceRefInput.setResourceUrn(RESOURCE_URN); + resourceRefInput.setSubResource(SUB_RESOURCE); + resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); + return resourceRefInput; + } + + private SchemaMetadata schemaMetadata() { + SchemaMetadata schemaMetadata = new SchemaMetadata(); + SchemaFieldArray schemaFields = new SchemaFieldArray(); + SchemaField schemaField = new SchemaField(); + schemaField.setFieldPath(SUB_RESOURCE); + schemaFields.add(schemaField); + schemaMetadata.setFields(schemaFields); + return schemaMetadata; + } + + private EditableSchemaMetadata editableSchemaMetadata() { + EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); + EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); + EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); + editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); + editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); + editableSchemaFieldInfos.add(editableSchemaFieldInfo); + editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); + return editableSchemaMetadata; + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java index c59afc0d6134b9..abed58aa883760 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeProposalMatcher.java @@ -6,34 +6,32 @@ import com.linkedin.mxe.MetadataChangeProposal; import org.mockito.ArgumentMatcher; -public class CreateBusinessAttributeProposalMatcher implements ArgumentMatcher { - private MetadataChangeProposal left; - public CreateBusinessAttributeProposalMatcher(MetadataChangeProposal left) { - this.left = left; - } +public class CreateBusinessAttributeProposalMatcher + implements ArgumentMatcher { + private MetadataChangeProposal left; - @Override - public boolean matches(MetadataChangeProposal right) { - return left.getEntityType().equals(right.getEntityType()) - && left.getAspectName().equals(right.getAspectName()) - && left.getChangeType().equals(right.getChangeType()) - && businessAttributeInfoMatch(left.getAspect(), right.getAspect()); - } + public CreateBusinessAttributeProposalMatcher(MetadataChangeProposal left) { + this.left = left; + } - private boolean businessAttributeInfoMatch(GenericAspect left, GenericAspect right) { - BusinessAttributeInfo leftProps = GenericRecordUtils.deserializeAspect( - left.getValue(), - "application/json", - BusinessAttributeInfo.class - ); + @Override + public boolean matches(MetadataChangeProposal right) { + return left.getEntityType().equals(right.getEntityType()) + && left.getAspectName().equals(right.getAspectName()) + && left.getChangeType().equals(right.getChangeType()) + && businessAttributeInfoMatch(left.getAspect(), right.getAspect()); + } - BusinessAttributeInfo rightProps = GenericRecordUtils.deserializeAspect( - right.getValue(), - "application/json", - BusinessAttributeInfo.class - ); + private boolean businessAttributeInfoMatch(GenericAspect left, GenericAspect right) { + BusinessAttributeInfo leftProps = + GenericRecordUtils.deserializeAspect( + left.getValue(), "application/json", BusinessAttributeInfo.class); - return leftProps.getName().equals(rightProps.getName()) - && leftProps.getDescription().equals(rightProps.getDescription()); - } + BusinessAttributeInfo rightProps = + GenericRecordUtils.deserializeAspect( + right.getValue(), "application/json", BusinessAttributeInfo.class); + + return leftProps.getName().equals(rightProps.getName()) + && leftProps.getDescription().equals(rightProps.getDescription()); + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java index b6cad4c57b2866..a5fb7fbe54cff0 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java @@ -1,5 +1,13 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + import com.datahub.authentication.Authentication; import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.businessattribute.BusinessAttributeKey; @@ -24,174 +32,219 @@ import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.schema.BooleanType; import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.ExecutionException; import org.mockito.Mockito; import org.testng.annotations.Test; -import java.util.concurrent.ExecutionException; - -import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; -import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; -import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; - public class CreateBusinessAttributeResolverTest { - private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; - private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; - private static final CreateBusinessAttributeInput TEST_INPUT = new CreateBusinessAttributeInput( - TEST_BUSINESS_ATTRIBUTE_NAME, - TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, - SchemaFieldDataType.BOOLEAN - ); - private static final CreateBusinessAttributeInput TEST_INPUT_NULL_NAME = new CreateBusinessAttributeInput( - null, - TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, - SchemaFieldDataType.BOOLEAN - ); - private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; - private EntityClient mockClient; - private EntityService mockService; - private QueryContext mockContext; - private DataFetchingEnvironment mockEnv; - private BusinessAttributeService businessAttributeService; - private Authentication mockAuthentication; - private SearchResult searchResult; - - private void init() { - mockClient = Mockito.mock(EntityClient.class); - mockService = getMockEntityService(); - mockEnv = Mockito.mock(DataFetchingEnvironment.class); - businessAttributeService = Mockito.mock(BusinessAttributeService.class); - mockAuthentication = Mockito.mock(Authentication.class); - searchResult = Mockito.mock(SearchResult.class); - } - - @Test - public void testSuccess() throws Exception { - //Mock - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); - Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))).thenReturn(false); - Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), - Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) - )).thenReturn(searchResult); - Mockito.when(searchResult.getNumEntities()).thenReturn(0); - Mockito.when(mockClient.ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication))).thenReturn(BUSINESS_ATTRIBUTE_URN); - Mockito.when( - businessAttributeService.getBusinessAttributeEntityResponse(Mockito.any(Urn.class), Mockito.eq(mockAuthentication)) - ).thenReturn(getBusinessAttributeEntityResponse()); - - //Execute - CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); - resolver.get(mockEnv).get(); - - //verify - Mockito.verify(mockClient, Mockito.times(1)).ingestProposal( - Mockito.argThat(new CreateBusinessAttributeProposalMatcher(metadataChangeProposal())), - Mockito.any(Authentication.class) - ); - - } - - @Test - public void testNameIsNull() throws Exception { - //Mock - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_NULL_NAME); - Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))).thenReturn(false); - - //Execute - CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); - ExecutionException actualException = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); - - //verify - assertTrue(actualException.getCause().getMessage().equals("Failed to create Business Attribute with name: null")); - - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( - Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) - ); - } - - @Test - public void testNameAlreadyExists() throws Exception { - //Mock - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); - Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))).thenReturn(false); - Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), - Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) - )).thenReturn(searchResult); - Mockito.when(searchResult.getNumEntities()).thenReturn(1); - - //Execute - CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); - ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); - - //Verify - assertTrue(exception.getCause().getMessage().equals("\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( - Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) - ); - } - @Test - public void testUnauthorized() throws Exception { - init(); - setupDenyContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); - - CreateBusinessAttributeResolver resolver = new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); - AuthorizationException exception = expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv)); - - assertTrue(exception.getMessage().equals("Unauthorized to perform this action. Please contact your DataHub administrator.")); - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( - Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) - ); - } - private EntityResponse getBusinessAttributeEntityResponse() throws Exception { - EnvelopedAspectMap map = new EnvelopedAspectMap(); - BusinessAttributeInfo businessAttributeInfo = businessAttributeInfo(); - map.put(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(businessAttributeInfo.data()))); - EntityResponse entityResponse = new EntityResponse(); - entityResponse.setAspects(map); - entityResponse.setUrn(Urn.createFromString(BUSINESS_ATTRIBUTE_URN)); - return entityResponse; - } - - private MetadataChangeProposal metadataChangeProposal() { - BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); - BusinessAttributeInfo info = new BusinessAttributeInfo(); - info.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); - info.setName(TEST_BUSINESS_ATTRIBUTE_NAME); - info.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); - info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(SchemaFieldDataType.BOOLEAN), SetMode.IGNORE_NULL); - return MutationUtils.buildMetadataChangeProposalWithKey(businessAttributeKey, BUSINESS_ATTRIBUTE_ENTITY_NAME, - BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, info); - } - private void setupAllowContext() { - mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - - private void setupDenyContext() { - mockContext = getMockDenyContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - private BusinessAttributeInfo businessAttributeInfo() { - BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); - businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); - businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); - businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); - com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = new com.linkedin.schema.SchemaFieldDataType(); - schemaFieldDataType.setType(com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); - businessAttributeInfo.setType(schemaFieldDataType); - return businessAttributeInfo; - } + private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; + private static final CreateBusinessAttributeInput TEST_INPUT = + new CreateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, + SchemaFieldDataType.BOOLEAN); + private static final CreateBusinessAttributeInput TEST_INPUT_NULL_NAME = + new CreateBusinessAttributeInput( + null, TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, SchemaFieldDataType.BOOLEAN); + private static final String BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private BusinessAttributeService businessAttributeService; + private Authentication mockAuthentication; + private SearchResult searchResult; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + businessAttributeService = Mockito.mock(BusinessAttributeService.class); + mockAuthentication = Mockito.mock(Authentication.class); + searchResult = Mockito.mock(SearchResult.class); + } + + @Test + public void testSuccess() throws Exception { + // Mock + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))) + .thenReturn(false); + Mockito.when( + mockClient.search( + Mockito.any(String.class), + Mockito.any(String.class), + Mockito.any(Filter.class), + Mockito.isNull(), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.eq(mockAuthentication), + Mockito.any(SearchFlags.class))) + .thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(0); + Mockito.when( + mockClient.ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication))) + .thenReturn(BUSINESS_ATTRIBUTE_URN); + Mockito.when( + businessAttributeService.getBusinessAttributeEntityResponse( + Mockito.any(Urn.class), Mockito.eq(mockAuthentication))) + .thenReturn(getBusinessAttributeEntityResponse()); + + // Execute + CreateBusinessAttributeResolver resolver = + new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + resolver.get(mockEnv).get(); + + // verify + Mockito.verify(mockClient, Mockito.times(1)) + .ingestProposal( + Mockito.argThat(new CreateBusinessAttributeProposalMatcher(metadataChangeProposal())), + Mockito.any(Authentication.class)); + } + + @Test + public void testNameIsNull() throws Exception { + // Mock + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT_NULL_NAME); + Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))) + .thenReturn(false); + + // Execute + CreateBusinessAttributeResolver resolver = + new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + ExecutionException actualException = + expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + // verify + assertTrue( + actualException + .getCause() + .getMessage() + .equals("Failed to create Business Attribute with name: null")); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class)); + } + + @Test + public void testNameAlreadyExists() throws Exception { + // Mock + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))) + .thenReturn(false); + Mockito.when( + mockClient.search( + Mockito.any(String.class), + Mockito.any(String.class), + Mockito.any(Filter.class), + Mockito.isNull(), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.eq(mockAuthentication), + Mockito.any(SearchFlags.class))) + .thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(1); + + // Execute + CreateBusinessAttributeResolver resolver = + new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + ExecutionException exception = + expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + // Verify + assertTrue( + exception + .getCause() + .getMessage() + .equals( + "\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class)); + } + + @Test + public void testUnauthorized() throws Exception { + init(); + setupDenyContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); + + CreateBusinessAttributeResolver resolver = + new CreateBusinessAttributeResolver(mockClient, mockService, businessAttributeService); + AuthorizationException exception = + expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv)); + + assertTrue( + exception + .getMessage() + .equals( + "Unauthorized to perform this action. Please contact your DataHub administrator.")); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class)); + } + + private EntityResponse getBusinessAttributeEntityResponse() throws Exception { + EnvelopedAspectMap map = new EnvelopedAspectMap(); + BusinessAttributeInfo businessAttributeInfo = businessAttributeInfo(); + map.put( + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + new EnvelopedAspect().setValue(new Aspect(businessAttributeInfo.data()))); + EntityResponse entityResponse = new EntityResponse(); + entityResponse.setAspects(map); + entityResponse.setUrn(Urn.createFromString(BUSINESS_ATTRIBUTE_URN)); + return entityResponse; + } + + private MetadataChangeProposal metadataChangeProposal() { + BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); + BusinessAttributeInfo info = new BusinessAttributeInfo(); + info.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + info.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + info.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + info.setType( + BusinessAttributeUtils.mapSchemaFieldDataType(SchemaFieldDataType.BOOLEAN), + SetMode.IGNORE_NULL); + return MutationUtils.buildMetadataChangeProposalWithKey( + businessAttributeKey, + BUSINESS_ATTRIBUTE_ENTITY_NAME, + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + info); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private void setupDenyContext() { + mockContext = getMockDenyContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private BusinessAttributeInfo businessAttributeInfo() { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); + businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = + new com.linkedin.schema.SchemaFieldDataType(); + schemaFieldDataType.setType( + com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); + businessAttributeInfo.setType(schemaFieldDataType); + return businessAttributeInfo; + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java index a76250031f4298..114402a5b24dbf 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/DeleteBusinessAttributeResolverTest.java @@ -1,5 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + import com.datahub.authentication.Authentication; import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.QueryContext; @@ -9,82 +14,93 @@ import org.mockito.Mockito; import org.testng.annotations.Test; -import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; -import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; - public class DeleteBusinessAttributeResolverTest { - private static final String TEST_BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; - private EntityClient mockClient; - private QueryContext mockContext; - private DataFetchingEnvironment mockEnv; - private Authentication mockAuthentication; - private void init() { - mockClient = Mockito.mock(EntityClient.class); - mockEnv = Mockito.mock(DataFetchingEnvironment.class); - mockAuthentication = Mockito.mock(Authentication.class); - } - private void setupAllowContext() { - mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } + private static final String TEST_BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private EntityClient mockClient; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } - private void setupDenyContext() { - mockContext = getMockDenyContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - @Test - public void testSuccess() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockClient.exists(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), mockAuthentication)).thenReturn(true); + private void setupDenyContext() { + mockContext = getMockDenyContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } - DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); - resolver.get(mockEnv).get(); + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when( + mockClient.exists( + Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), mockAuthentication)) + .thenReturn(true); - Mockito.verify(mockClient, Mockito.times(1)).deleteEntity( - Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), - Mockito.any(Authentication.class) - ); - } + DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); + resolver.get(mockEnv).get(); - @Test - public void testUnauthorized() throws Exception { - init(); - setupDenyContext(); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.verify(mockClient, Mockito.times(1)) + .deleteEntity( + Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), + Mockito.any(Authentication.class)); + } - DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); - AuthorizationException actualException = expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv).get()); - assertTrue(actualException.getMessage().equals("Unauthorized to perform this action. Please contact your DataHub administrator.")); + @Test + public void testUnauthorized() throws Exception { + init(); + setupDenyContext(); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.verify(mockClient, Mockito.times(0)).deleteEntity( - Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), - Mockito.any(Authentication.class) - ); - } + DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); + AuthorizationException actualException = + expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv).get()); + assertTrue( + actualException + .getMessage() + .equals( + "Unauthorized to perform this action. Please contact your DataHub administrator.")); - @Test - public void testEntityNotExists() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockClient.exists(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), mockAuthentication)).thenReturn(false); + Mockito.verify(mockClient, Mockito.times(0)) + .deleteEntity( + Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), + Mockito.any(Authentication.class)); + } - DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); - RuntimeException actualException = expectThrows(RuntimeException.class, () -> resolver.get(mockEnv).get()); - assertTrue(actualException.getMessage() - .equals(String.format("This urn does not exist: %s", TEST_BUSINESS_ATTRIBUTE_URN) - )); + @Test + public void testEntityNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when( + mockClient.exists( + Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), mockAuthentication)) + .thenReturn(false); - Mockito.verify(mockClient, Mockito.times(0)).deleteEntity( - Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), - Mockito.any(Authentication.class) - ); + DeleteBusinessAttributeResolver resolver = new DeleteBusinessAttributeResolver(mockClient); + RuntimeException actualException = + expectThrows(RuntimeException.class, () -> resolver.get(mockEnv).get()); + assertTrue( + actualException + .getMessage() + .equals(String.format("This urn does not exist: %s", TEST_BUSINESS_ATTRIBUTE_URN))); - } + Mockito.verify(mockClient, Mockito.times(0)) + .deleteEntity( + Mockito.eq(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)), + Mockito.any(Authentication.class)); + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java index d6f664169c90a5..b0b53c2d772136 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java @@ -1,5 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + import com.datahub.authentication.Authentication; import com.linkedin.businessattribute.BusinessAttributeAssociation; import com.linkedin.common.urn.Urn; @@ -19,151 +24,156 @@ import com.linkedin.schema.SchemaFieldArray; import com.linkedin.schema.SchemaMetadata; import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.ExecutionException; import org.mockito.Mockito; import org.testng.annotations.Test; -import java.util.concurrent.ExecutionException; - -import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; -import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; - public class RemoveBusinessAttributeResolverTest { - private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; - private static final String RESOURCE_URN = "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; - private static final String SUB_RESOURCE = "name"; - private EntityClient mockClient; - private EntityService mockService; - private QueryContext mockContext; - private DataFetchingEnvironment mockEnv; - private Authentication mockAuthentication; - private void init() { - mockClient = Mockito.mock(EntityClient.class); - mockService = getMockEntityService(); - mockEnv = Mockito.mock(DataFetchingEnvironment.class); - mockAuthentication = Mockito.mock(Authentication.class); - } - private void setupAllowContext() { - mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - @Test - public void testSuccess() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - Mockito.when(EntityUtils.getAspectFromEntity( - RESOURCE_URN, - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - mockService, null) - ).thenReturn(editableSchemaMetadata()); - - RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); - resolver.get(mockEnv).get(); - - Mockito.verify(mockClient, Mockito.times(1)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - } - - @Test - public void testBusinessAttributeNotExists() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(false); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); - RuntimeException exception = expectThrows(RuntimeException.class, () -> resolver.get(mockEnv).get()); - assertTrue(exception.getMessage().equals( - String.format("This urn does not exist: %s", BUSINESS_ATTRIBUTE_URN))); - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - } - - @Test - public void testBusinessAttributeNotAdded() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(true); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - - RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); - ExecutionException actualException = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); - assertTrue(actualException.getCause().getMessage().equals(String.format("Failed to remove Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); - - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - - } - - @Test - public void testResourceNotExists() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when(mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)).thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN))).thenReturn(false); - Mockito.when(mockService.getAspect(Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - - RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockClient, mockService); - ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); - assertTrue(exception.getCause().getMessage().equals( - String.format("Failed to remove Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN - ))); - - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), - Mockito.eq(mockAuthentication)); - } - public AddBusinessAttributeInput addBusinessAttributeInput() { - AddBusinessAttributeInput addBusinessAttributeInput = new AddBusinessAttributeInput(); - addBusinessAttributeInput.setBusinessAttributeUrn(BUSINESS_ATTRIBUTE_URN); - addBusinessAttributeInput.setResourceUrn(resourceRefInput()); - return addBusinessAttributeInput; - } - private ResourceRefInput resourceRefInput() { - ResourceRefInput resourceRefInput = new ResourceRefInput(); - resourceRefInput.setResourceUrn(RESOURCE_URN); - resourceRefInput.setSubResource(SUB_RESOURCE); - resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); - return resourceRefInput; - } - - private SchemaMetadata schemaMetadata() { - SchemaMetadata schemaMetadata = new SchemaMetadata(); - SchemaFieldArray schemaFields = new SchemaFieldArray(); - SchemaField schemaField = new SchemaField(); - schemaField.setFieldPath(SUB_RESOURCE); - schemaFields.add(schemaField); - schemaMetadata.setFields(schemaFields); - return schemaMetadata; - } - - private EditableSchemaMetadata editableSchemaMetadata() { - EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); - EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); - EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); - editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); - editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); - editableSchemaFieldInfos.add(editableSchemaFieldInfo); - editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); - return editableSchemaMetadata; - } + private static final String BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final String RESOURCE_URN = + "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; + private static final String SUB_RESOURCE = "name"; + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when( + mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + .thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + Mockito.when( + mockService.getAspect( + Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + Mockito.when( + EntityUtils.getAspectFromEntity( + RESOURCE_URN, Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, mockService, null)) + .thenReturn(editableSchemaMetadata()); + + RemoveBusinessAttributeResolver resolver = + new RemoveBusinessAttributeResolver(mockClient, mockService); + resolver.get(mockEnv).get(); + + Mockito.verify(mockClient, Mockito.times(1)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + } + + @Test + public void testBusinessAttributeNotAdded() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when( + mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + .thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + Mockito.when( + mockService.getAspect( + Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + RemoveBusinessAttributeResolver resolver = + new RemoveBusinessAttributeResolver(mockClient, mockService); + ExecutionException actualException = + expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + assertTrue( + actualException + .getCause() + .getMessage() + .equals( + String.format( + "Failed to remove Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + } + + @Test + public void testResourceNotExists() throws Exception { + init(); + setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); + Mockito.when( + mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + .thenReturn(true); + Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(false); + Mockito.when( + mockService.getAspect( + Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) + .thenReturn(schemaMetadata()); + + RemoveBusinessAttributeResolver resolver = + new RemoveBusinessAttributeResolver(mockClient, mockService); + ExecutionException exception = + expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + assertTrue( + exception + .getCause() + .getMessage() + .equals( + String.format( + "Failed to remove Business Attribute with urn %s to dataset with urn %s", + BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + } + + public AddBusinessAttributeInput addBusinessAttributeInput() { + AddBusinessAttributeInput addBusinessAttributeInput = new AddBusinessAttributeInput(); + addBusinessAttributeInput.setBusinessAttributeUrn(BUSINESS_ATTRIBUTE_URN); + addBusinessAttributeInput.setResourceUrn(resourceRefInput()); + return addBusinessAttributeInput; + } + + private ResourceRefInput resourceRefInput() { + ResourceRefInput resourceRefInput = new ResourceRefInput(); + resourceRefInput.setResourceUrn(RESOURCE_URN); + resourceRefInput.setSubResource(SUB_RESOURCE); + resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); + return resourceRefInput; + } + + private SchemaMetadata schemaMetadata() { + SchemaMetadata schemaMetadata = new SchemaMetadata(); + SchemaFieldArray schemaFields = new SchemaFieldArray(); + SchemaField schemaField = new SchemaField(); + schemaField.setFieldPath(SUB_RESOURCE); + schemaFields.add(schemaField); + schemaMetadata.setFields(schemaFields); + return schemaMetadata; + } + + private EditableSchemaMetadata editableSchemaMetadata() { + EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); + EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); + EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); + editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); + editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); + editableSchemaFieldInfos.add(editableSchemaFieldInfo); + editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); + return editableSchemaMetadata; + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java index ccff7b1c9f630c..7535576a0bdce5 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java @@ -1,5 +1,11 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + import com.datahub.authentication.Authentication; import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.urn.Urn; @@ -23,187 +29,234 @@ import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.schema.BooleanType; import graphql.schema.DataFetchingEnvironment; -import org.mockito.Mockito; -import org.testng.annotations.Test; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; - -import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; -import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; -import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; +import org.mockito.Mockito; +import org.testng.annotations.Test; public class UpdateBusinessAttributeResolverTest { - private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; - private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; - private static final String TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED = "test-business-attribute-updated"; - private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED = "test-description-updated"; - private static final String TEST_BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; - private static final Urn TEST_BUSINESS_ATTRIBUTE_URN_OBJ = UrnUtils.getUrn(TEST_BUSINESS_ATTRIBUTE_URN); - private EntityClient mockClient; - private QueryContext mockContext; - private DataFetchingEnvironment mockEnv; - private BusinessAttributeService businessAttributeService; - private Authentication mockAuthentication; - private SearchResult searchResult; - - private void init() { - mockClient = Mockito.mock(EntityClient.class); - mockEnv = Mockito.mock(DataFetchingEnvironment.class); - businessAttributeService = Mockito.mock(BusinessAttributeService.class); - mockAuthentication = Mockito.mock(Authentication.class); - searchResult = Mockito.mock(SearchResult.class); - } - - @Test - public void testSuccess() throws Exception { - init(); - setupAllowContext(); - final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( - TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, - TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, - SchemaFieldDataType.NUMBER - ); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)).thenReturn(true); - Mockito.when(businessAttributeService.getBusinessAttributeEntityResponse(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) - .thenReturn(getBusinessAttributeEntityResponse()); - Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), - Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) - )).thenReturn(searchResult); - Mockito.when(searchResult.getNumEntities()).thenReturn(0); - Mockito.when(mockClient.ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication))) - .thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - - UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); - resolver.get(mockEnv).get(); - - //verify - Mockito.verify(mockClient, Mockito.times(1)).ingestProposal( - Mockito.argThat(new CreateBusinessAttributeProposalMatcher(updatedMetadataChangeProposal())), - Mockito.any(Authentication.class) - ); - } - - @Test - public void testNotExists() throws Exception { - init(); - setupAllowContext(); - final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( - TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, - TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, - SchemaFieldDataType.NUMBER - ); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)).thenReturn(false); - - UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); - RuntimeException expectedException = expectThrows(RuntimeException.class, () -> resolver.get(mockEnv)); - assertTrue(expectedException.getMessage().equals(String.format("This urn does not exist: %s", TEST_BUSINESS_ATTRIBUTE_URN))); - - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( - Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) - ); - } - - @Test - public void testNameConflict() throws Exception { - init(); - setupAllowContext(); - final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( - TEST_BUSINESS_ATTRIBUTE_NAME, - TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, - SchemaFieldDataType.NUMBER - ); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)).thenReturn(true); - Mockito.when(businessAttributeService.getBusinessAttributeEntityResponse(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) - .thenReturn(getBusinessAttributeEntityResponse()); - Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), - Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) - )).thenReturn(searchResult); - Mockito.when(searchResult.getNumEntities()).thenReturn(1); - - UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); - - ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); - - //Verify - assertTrue(exception.getCause().getMessage().equals("\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( - Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) - ); - - } - @Test - public void testNotAuthorized() throws Exception { - init(); - setupDenyContext(); - final UpdateBusinessAttributeInput testInput = new UpdateBusinessAttributeInput( - TEST_BUSINESS_ATTRIBUTE_NAME, - TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, - SchemaFieldDataType.NUMBER - ); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - - UpdateBusinessAttributeResolver resolver = new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); - AuthorizationException exception = expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv)); - - assertTrue(exception.getMessage().equals("Unauthorized to perform this action. Please contact your DataHub administrator.")); - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( - Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) - ); - - } - - private EntityResponse getBusinessAttributeEntityResponse() throws Exception { - Map result = new HashMap<>(); - EnvelopedAspectMap map = new EnvelopedAspectMap(); - BusinessAttributeInfo businessAttributeInfo = businessAttributeInfo(); - map.put(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(businessAttributeInfo.data()))); - EntityResponse entityResponse = new EntityResponse(); - entityResponse.setAspects(map); - entityResponse.setUrn(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)); - return entityResponse; - } - - private MetadataChangeProposal updatedMetadataChangeProposal() { - BusinessAttributeInfo info = new BusinessAttributeInfo(); - info.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); - info.setName(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); - info.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED); - info.setType(BusinessAttributeUtils.mapSchemaFieldDataType(SchemaFieldDataType.BOOLEAN), SetMode.IGNORE_NULL); - return AspectUtils.buildMetadataChangeProposal(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, - BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, info); - } - - private void setupAllowContext() { - mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - - private void setupDenyContext() { - mockContext = getMockDenyContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - - private BusinessAttributeInfo businessAttributeInfo() { - BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); - businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); - businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); - businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); - com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = new com.linkedin.schema.SchemaFieldDataType(); - schemaFieldDataType.setType(com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); - businessAttributeInfo.setType(schemaFieldDataType); - return businessAttributeInfo; - } + private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; + private static final String TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED = + "test-business-attribute-updated"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED = + "test-description-updated"; + private static final String TEST_BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final Urn TEST_BUSINESS_ATTRIBUTE_URN_OBJ = + UrnUtils.getUrn(TEST_BUSINESS_ATTRIBUTE_URN); + private EntityClient mockClient; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private BusinessAttributeService businessAttributeService; + private Authentication mockAuthentication; + private SearchResult searchResult; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + businessAttributeService = Mockito.mock(BusinessAttributeService.class); + mockAuthentication = Mockito.mock(Authentication.class); + searchResult = Mockito.mock(SearchResult.class); + } + + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + final UpdateBusinessAttributeInput testInput = + new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) + .thenReturn(true); + Mockito.when( + businessAttributeService.getBusinessAttributeEntityResponse( + TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) + .thenReturn(getBusinessAttributeEntityResponse()); + Mockito.when( + mockClient.search( + Mockito.any(String.class), + Mockito.any(String.class), + Mockito.any(Filter.class), + Mockito.isNull(), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.eq(mockAuthentication), + Mockito.any(SearchFlags.class))) + .thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(0); + Mockito.when( + mockClient.ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication))) + .thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + + UpdateBusinessAttributeResolver resolver = + new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + resolver.get(mockEnv).get(); + + // verify + Mockito.verify(mockClient, Mockito.times(1)) + .ingestProposal( + Mockito.argThat( + new CreateBusinessAttributeProposalMatcher(updatedMetadataChangeProposal())), + Mockito.any(Authentication.class)); + } + + @Test + public void testNotExists() throws Exception { + init(); + setupAllowContext(); + final UpdateBusinessAttributeInput testInput = + new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) + .thenReturn(false); + + UpdateBusinessAttributeResolver resolver = + new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + RuntimeException expectedException = + expectThrows(RuntimeException.class, () -> resolver.get(mockEnv)); + assertTrue( + expectedException + .getMessage() + .equals(String.format("This urn does not exist: %s", TEST_BUSINESS_ATTRIBUTE_URN))); + + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class)); + } + + @Test + public void testNameConflict() throws Exception { + init(); + setupAllowContext(); + final UpdateBusinessAttributeInput testInput = + new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockClient.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) + .thenReturn(true); + Mockito.when( + businessAttributeService.getBusinessAttributeEntityResponse( + TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) + .thenReturn(getBusinessAttributeEntityResponse()); + Mockito.when( + mockClient.search( + Mockito.any(String.class), + Mockito.any(String.class), + Mockito.any(Filter.class), + Mockito.isNull(), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.eq(mockAuthentication), + Mockito.any(SearchFlags.class))) + .thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(1); + + UpdateBusinessAttributeResolver resolver = + new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + + ExecutionException exception = + expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + // Verify + assertTrue( + exception + .getCause() + .getMessage() + .equals( + "\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class)); + } + + @Test + public void testNotAuthorized() throws Exception { + init(); + setupDenyContext(); + final UpdateBusinessAttributeInput testInput = + new UpdateBusinessAttributeInput( + TEST_BUSINESS_ATTRIBUTE_NAME, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED, + SchemaFieldDataType.NUMBER); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + + UpdateBusinessAttributeResolver resolver = + new UpdateBusinessAttributeResolver(mockClient, businessAttributeService); + AuthorizationException exception = + expectThrows(AuthorizationException.class, () -> resolver.get(mockEnv)); + + assertTrue( + exception + .getMessage() + .equals( + "Unauthorized to perform this action. Please contact your DataHub administrator.")); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class)); + } + + private EntityResponse getBusinessAttributeEntityResponse() throws Exception { + Map result = new HashMap<>(); + EnvelopedAspectMap map = new EnvelopedAspectMap(); + BusinessAttributeInfo businessAttributeInfo = businessAttributeInfo(); + map.put( + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + new EnvelopedAspect().setValue(new Aspect(businessAttributeInfo.data()))); + EntityResponse entityResponse = new EntityResponse(); + entityResponse.setAspects(map); + entityResponse.setUrn(Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN)); + return entityResponse; + } + + private MetadataChangeProposal updatedMetadataChangeProposal() { + BusinessAttributeInfo info = new BusinessAttributeInfo(); + info.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + info.setName(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + info.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION_UPDATED); + info.setType( + BusinessAttributeUtils.mapSchemaFieldDataType(SchemaFieldDataType.BOOLEAN), + SetMode.IGNORE_NULL); + return AspectUtils.buildMetadataChangeProposal( + TEST_BUSINESS_ATTRIBUTE_URN_OBJ, BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, info); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private void setupDenyContext() { + mockContext = getMockDenyContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private BusinessAttributeInfo businessAttributeInfo() { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); + businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = + new com.linkedin.schema.SchemaFieldDataType(); + schemaFieldDataType.setType( + com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); + businessAttributeInfo.setType(schemaFieldDataType); + return businessAttributeInfo; + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java index 970ef07525ea7d..c3267e060801d9 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java @@ -1,5 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.expectThrows; + import com.datahub.authentication.Authentication; import com.linkedin.businessattribute.BusinessAttributeInfo; import com.linkedin.common.AuditStamp; @@ -19,122 +24,145 @@ import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.schema.BooleanType; import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.ExecutionException; import org.mockito.Mockito; import org.testng.annotations.Test; -import java.util.concurrent.ExecutionException; - -import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; -import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.expectThrows; - public class UpdateNameResolverTest { - private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; - private static final String TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED = "test-business-attribute-updated"; - private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; - private static final String TEST_BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; - private static final Urn TEST_BUSINESS_ATTRIBUTE_URN_OBJ = UrnUtils.getUrn(TEST_BUSINESS_ATTRIBUTE_URN); - private EntityClient mockClient; - private EntityService mockService; - private QueryContext mockContext; - private DataFetchingEnvironment mockEnv; - private Authentication mockAuthentication; - private SearchResult searchResult; - - private void init() { - mockClient = Mockito.mock(EntityClient.class); - mockService = getMockEntityService(); - mockEnv = Mockito.mock(DataFetchingEnvironment.class); - mockAuthentication = Mockito.mock(Authentication.class); - searchResult = Mockito.mock(SearchResult.class); - } - - @Test - public void testSuccess() throws Exception { - init(); - setupAllowContext(); - UpdateNameInput testInput = new UpdateNameInput(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockService.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ)).thenReturn(true); - Mockito.when(EntityUtils.getAspectFromEntity( + private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; + private static final String TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED = + "test-business-attribute-updated"; + private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; + private static final String TEST_BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + private static final Urn TEST_BUSINESS_ATTRIBUTE_URN_OBJ = + UrnUtils.getUrn(TEST_BUSINESS_ATTRIBUTE_URN); + private EntityClient mockClient; + private EntityService mockService; + private QueryContext mockContext; + private DataFetchingEnvironment mockEnv; + private Authentication mockAuthentication; + private SearchResult searchResult; + + private void init() { + mockClient = Mockito.mock(EntityClient.class); + mockService = getMockEntityService(); + mockEnv = Mockito.mock(DataFetchingEnvironment.class); + mockAuthentication = Mockito.mock(Authentication.class); + searchResult = Mockito.mock(SearchResult.class); + } + + @Test + public void testSuccess() throws Exception { + init(); + setupAllowContext(); + UpdateNameInput testInput = + new UpdateNameInput(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED, TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockService.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, true)).thenReturn(true); + Mockito.when( + EntityUtils.getAspectFromEntity( TEST_BUSINESS_ATTRIBUTE_URN_OBJ.toString(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, mockService, - null - )).thenReturn(businessAttributeInfo()); - - Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), - Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) - )).thenReturn(searchResult); - Mockito.when(searchResult.getNumEntities()).thenReturn(0); - - BusinessAttributeInfo updatedBusinessAttributeInfo = businessAttributeInfo(); - updatedBusinessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); - updatedBusinessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); - MetadataChangeProposal proposal = MutationUtils.buildMetadataChangeProposalWithUrn( - TEST_BUSINESS_ATTRIBUTE_URN_OBJ, - Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, - updatedBusinessAttributeInfo - ); - - UpdateNameResolver resolver = new UpdateNameResolver(mockService, mockClient); - resolver.get(mockEnv).get(); - - //verify - Mockito.verify(mockService, Mockito.times(1)).ingestProposal( - Mockito.argThat(new CreateBusinessAttributeProposalMatcher(proposal)), - Mockito.any(AuditStamp.class), - Mockito.eq(false) - ); - } - - @Test - public void testNameConflict() throws Exception { - init(); - setupAllowContext(); - UpdateNameInput testInput = new UpdateNameInput(TEST_BUSINESS_ATTRIBUTE_NAME, TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); - Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); - Mockito.when(mockService.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ)).thenReturn(true); - Mockito.when(EntityUtils.getAspectFromEntity( + null)) + .thenReturn(businessAttributeInfo()); + + Mockito.when( + mockClient.search( + Mockito.any(String.class), + Mockito.any(String.class), + Mockito.any(Filter.class), + Mockito.isNull(), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.eq(mockAuthentication), + Mockito.any(SearchFlags.class))) + .thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(0); + + BusinessAttributeInfo updatedBusinessAttributeInfo = businessAttributeInfo(); + updatedBusinessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + updatedBusinessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME_UPDATED); + MetadataChangeProposal proposal = + MutationUtils.buildMetadataChangeProposalWithUrn( + TEST_BUSINESS_ATTRIBUTE_URN_OBJ, + Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, + updatedBusinessAttributeInfo); + + UpdateNameResolver resolver = new UpdateNameResolver(mockService, mockClient); + resolver.get(mockEnv).get(); + + // verify + Mockito.verify(mockService, Mockito.times(1)) + .ingestProposal( + Mockito.argThat(new CreateBusinessAttributeProposalMatcher(proposal)), + Mockito.any(AuditStamp.class), + Mockito.eq(false)); + } + + @Test + public void testNameConflict() throws Exception { + init(); + setupAllowContext(); + UpdateNameInput testInput = + new UpdateNameInput(TEST_BUSINESS_ATTRIBUTE_NAME, TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(testInput); + Mockito.when(mockEnv.getArgument("urn")).thenReturn(TEST_BUSINESS_ATTRIBUTE_URN); + Mockito.when(mockService.exists(TEST_BUSINESS_ATTRIBUTE_URN_OBJ, true)).thenReturn(true); + Mockito.when( + EntityUtils.getAspectFromEntity( TEST_BUSINESS_ATTRIBUTE_URN_OBJ.toString(), Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, mockService, - null - )).thenReturn(businessAttributeInfo()); - - Mockito.when(mockClient.search(Mockito.any(String.class), Mockito.any(String.class), Mockito.any(Filter.class), - Mockito.isNull(), Mockito.eq(0), Mockito.eq(1000), Mockito.eq(mockAuthentication), Mockito.any(SearchFlags.class) - )).thenReturn(searchResult); - Mockito.when(searchResult.getNumEntities()).thenReturn(1); - - UpdateNameResolver resolver = new UpdateNameResolver(mockService, mockClient); - ExecutionException exception = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); - - assertTrue(exception.getCause().getMessage().equals("\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); - Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( - Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class) - ); - } - - private void setupAllowContext() { - mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); - Mockito.when(mockEnv.getContext()).thenReturn(mockContext); - } - - private BusinessAttributeInfo businessAttributeInfo() { - BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); - businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); - businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); - businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); - com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = new com.linkedin.schema.SchemaFieldDataType(); - schemaFieldDataType.setType(com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); - businessAttributeInfo.setType(schemaFieldDataType); - return businessAttributeInfo; - } - - + null)) + .thenReturn(businessAttributeInfo()); + + Mockito.when( + mockClient.search( + Mockito.any(String.class), + Mockito.any(String.class), + Mockito.any(Filter.class), + Mockito.isNull(), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.eq(mockAuthentication), + Mockito.any(SearchFlags.class))) + .thenReturn(searchResult); + Mockito.when(searchResult.getNumEntities()).thenReturn(1); + + UpdateNameResolver resolver = new UpdateNameResolver(mockService, mockClient); + ExecutionException exception = + expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); + + assertTrue( + exception + .getCause() + .getMessage() + .equals( + "\"test-business-attribute\" already exists as Business Attribute. Please pick a unique name.")); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal( + Mockito.any(MetadataChangeProposal.class), Mockito.any(Authentication.class)); + } + + private void setupAllowContext() { + mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + } + + private BusinessAttributeInfo businessAttributeInfo() { + BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(); + businessAttributeInfo.setName(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setFieldPath(TEST_BUSINESS_ATTRIBUTE_NAME); + businessAttributeInfo.setDescription(TEST_BUSINESS_ATTRIBUTE_DESCRIPTION); + com.linkedin.schema.SchemaFieldDataType schemaFieldDataType = + new com.linkedin.schema.SchemaFieldDataType(); + schemaFieldDataType.setType( + com.linkedin.schema.SchemaFieldDataType.Type.create(new BooleanType())); + businessAttributeInfo.setType(schemaFieldDataType); + return businessAttributeInfo; + } } diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index f6d247e5a1d280..11418caa198576 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -360,7 +360,7 @@ public class Constants { public static final String DATA_PROCESS_INSTANCE_RELATIONSHIPS_ASPECT_NAME = "dataProcessInstanceRelationships"; - //Business Attribute + // Business Attribute public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index 26705fdb363efb..72f0149df23f23 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -91,15 +91,25 @@ public class ESUtils { // we use this to make sure we filter for editable & non-editable fields. Also expands out // top-level properties // to field level properties - public static final Map> FIELDS_TO_EXPANDED_FIELDS_LIST = new HashMap>() {{ - put("tags", ImmutableList.of("tags", "fieldTags", "editedFieldTags")); - put("glossaryTerms", ImmutableList.of("glossaryTerms", "fieldGlossaryTerms", "editedFieldGlossaryTerms")); - put("fieldTags", ImmutableList.of("fieldTags", "editedFieldTags")); - put("fieldGlossaryTerms", ImmutableList.of("fieldGlossaryTerms", "editedFieldGlossaryTerms")); - put("fieldDescriptions", ImmutableList.of("fieldDescriptions", "editedFieldDescriptions")); - put("description", ImmutableList.of("description", "editedDescription")); - put("businessAttribute", ImmutableList.of("editedFieldBusinessAttribute", "businessAttribute")); - } + public static final Map> FIELDS_TO_EXPANDED_FIELDS_LIST = + new HashMap>() { + { + put("tags", ImmutableList.of("tags", "fieldTags", "editedFieldTags")); + put( + "glossaryTerms", + ImmutableList.of("glossaryTerms", "fieldGlossaryTerms", "editedFieldGlossaryTerms")); + put("fieldTags", ImmutableList.of("fieldTags", "editedFieldTags")); + put( + "fieldGlossaryTerms", + ImmutableList.of("fieldGlossaryTerms", "editedFieldGlossaryTerms")); + put( + "fieldDescriptions", + ImmutableList.of("fieldDescriptions", "editedFieldDescriptions")); + put("description", ImmutableList.of("description", "editedDescription")); + put( + "businessAttribute", + ImmutableList.of("editedFieldBusinessAttribute", "businessAttribute")); + } }; /* diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java index 65e6afd7ba66c4..6749f44b3ee52e 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/data/entity/BusinessAttributeAssociationChangeEvent.java @@ -7,40 +7,37 @@ import com.linkedin.metadata.timeline.data.ChangeEvent; import com.linkedin.metadata.timeline.data.ChangeOperation; import com.linkedin.metadata.timeline.data.SemanticChangeType; +import java.util.Map; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Value; import lombok.experimental.NonFinal; -import java.util.Map; - @EqualsAndHashCode(callSuper = true) @Value @NonFinal @Getter public class BusinessAttributeAssociationChangeEvent extends ChangeEvent { - @Builder(builderMethodName = "entityBusinessAttributeAssociationChangeEventBuilder") - public BusinessAttributeAssociationChangeEvent(String entityUrn, - ChangeCategory category, - ChangeOperation operation, - String modifier, - Map parameters, - AuditStamp auditStamp, - SemanticChangeType semVerChange, - String description, - Urn businessAttributeUrn) { - super( - entityUrn, - category, - operation, - modifier, - ImmutableMap.of( - "businessAttributeUrn", businessAttributeUrn.toString() - ), - auditStamp, - semVerChange, - description - ); - } + @Builder(builderMethodName = "entityBusinessAttributeAssociationChangeEventBuilder") + public BusinessAttributeAssociationChangeEvent( + String entityUrn, + ChangeCategory category, + ChangeOperation operation, + String modifier, + Map parameters, + AuditStamp auditStamp, + SemanticChangeType semVerChange, + String description, + Urn businessAttributeUrn) { + super( + entityUrn, + category, + operation, + modifier, + ImmutableMap.of("businessAttributeUrn", businessAttributeUrn.toString()), + auditStamp, + semVerChange, + description); + } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java index f2775f0b3478aa..03a30c95477ab1 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java @@ -8,54 +8,74 @@ import com.linkedin.metadata.timeline.data.ChangeOperation; import com.linkedin.metadata.timeline.data.SemanticChangeType; import com.linkedin.metadata.timeline.data.entity.BusinessAttributeAssociationChangeEvent; - -import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import javax.annotation.Nonnull; -public class BusinessAttributeAssociationChangeEventGenerator extends EntityChangeEventGenerator { +public class BusinessAttributeAssociationChangeEventGenerator + extends EntityChangeEventGenerator { - private static final String BUSINESS_ATTRIBUTE_ADDED_FORMAT = "BusinessAttribute '%s' added to entity '%s'."; - private static final String BUSINESS_ATTRIBUTE_REMOVED_FORMAT = "BusinessAttribute '%s' removed from entity '%s'."; + private static final String BUSINESS_ATTRIBUTE_ADDED_FORMAT = + "BusinessAttribute '%s' added to entity '%s'."; + private static final String BUSINESS_ATTRIBUTE_REMOVED_FORMAT = + "BusinessAttribute '%s' removed from entity '%s'."; - public static List computeDiffs(BusinessAttributeAssociation baseAssociation, - BusinessAttributeAssociation targetAssociation, - String urn, AuditStamp auditStamp) { - List changeEvents = new ArrayList<>(); + public static List computeDiffs( + BusinessAttributeAssociation baseAssociation, + BusinessAttributeAssociation targetAssociation, + String urn, + AuditStamp auditStamp) { + List changeEvents = new ArrayList<>(); - if (Objects.nonNull(baseAssociation) && Objects.isNull(targetAssociation)) { - changeEvents.add(createChangeEvent(baseAssociation, urn, ChangeOperation.REMOVE, - BUSINESS_ATTRIBUTE_REMOVED_FORMAT, auditStamp)); + if (Objects.nonNull(baseAssociation) && Objects.isNull(targetAssociation)) { + changeEvents.add( + createChangeEvent( + baseAssociation, + urn, + ChangeOperation.REMOVE, + BUSINESS_ATTRIBUTE_REMOVED_FORMAT, + auditStamp)); - } else if (Objects.isNull(baseAssociation) && Objects.nonNull(targetAssociation)) { - changeEvents.add(createChangeEvent(targetAssociation, urn, ChangeOperation.ADD, - BUSINESS_ATTRIBUTE_ADDED_FORMAT, auditStamp)); - } - return changeEvents; + } else if (Objects.isNull(baseAssociation) && Objects.nonNull(targetAssociation)) { + changeEvents.add( + createChangeEvent( + targetAssociation, + urn, + ChangeOperation.ADD, + BUSINESS_ATTRIBUTE_ADDED_FORMAT, + auditStamp)); } + return changeEvents; + } - private static ChangeEvent createChangeEvent(BusinessAttributeAssociation association, String entityUrn, ChangeOperation operation, - String format, AuditStamp auditStamp) { - return BusinessAttributeAssociationChangeEvent.entityBusinessAttributeAssociationChangeEventBuilder() - .modifier(association.getDestinationUrn().toString()) - .entityUrn(entityUrn) - .category(ChangeCategory.BUSINESS_ATTRIBUTE) - .operation(operation) - .semVerChange(SemanticChangeType.MINOR) - .description(String.format(format, association.getDestinationUrn().getId(), entityUrn)) - .businessAttributeUrn(association.getDestinationUrn()) - .auditStamp(auditStamp) - .build(); - } + private static ChangeEvent createChangeEvent( + BusinessAttributeAssociation association, + String entityUrn, + ChangeOperation operation, + String format, + AuditStamp auditStamp) { + return BusinessAttributeAssociationChangeEvent + .entityBusinessAttributeAssociationChangeEventBuilder() + .modifier(association.getDestinationUrn().toString()) + .entityUrn(entityUrn) + .category(ChangeCategory.BUSINESS_ATTRIBUTE) + .operation(operation) + .semVerChange(SemanticChangeType.MINOR) + .description(String.format(format, association.getDestinationUrn().getId(), entityUrn)) + .businessAttributeUrn(association.getDestinationUrn()) + .auditStamp(auditStamp) + .build(); + } - @Override - public List getChangeEvents(@Nonnull Urn urn, - @Nonnull String entity, - @Nonnull String aspect, - @Nonnull Aspect from, - @Nonnull Aspect to, - @Nonnull AuditStamp auditStamp) { - return computeDiffs(from.getValue(), to.getValue(), urn.toString(), auditStamp); - } + @Override + public List getChangeEvents( + @Nonnull Urn urn, + @Nonnull String entity, + @Nonnull String aspect, + @Nonnull Aspect from, + @Nonnull Aspect to, + @Nonnull AuditStamp auditStamp) { + return computeDiffs(from.getValue(), to.getValue(), urn.toString(), auditStamp); + } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java index 5c4abde5c1e2b2..d797c2d1668d9d 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeInfoChangeEventGenerator.java @@ -9,98 +9,140 @@ import com.linkedin.metadata.timeline.data.ChangeEvent; import com.linkedin.metadata.timeline.data.ChangeOperation; import com.linkedin.metadata.timeline.data.SemanticChangeType; - -import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.annotation.Nonnull; -public class BusinessAttributeInfoChangeEventGenerator extends EntityChangeEventGenerator { - - public static final String ATTRIBUTE_DOCUMENTATION_ADDED_FORMAT = - "Documentation for the businessAttribute '%s' has been added: '%s'"; - public static final String ATTRIBUTE_DOCUMENTATION_REMOVED_FORMAT = - "Documentation for the businessAttribute '%s' has been removed: '%s'"; - public static final String ATTRIBUTE_DOCUMENTATION_UPDATED_FORMAT = - "Documentation for the businessAttribute '%s' has been updated from '%s' to '%s'."; - - @Override - public List getChangeEvents(@Nonnull Urn urn, - @Nonnull String entity, - @Nonnull String aspect, - @Nonnull Aspect from, - @Nonnull Aspect to, - @Nonnull AuditStamp auditStamp) { - final List changeEvents = new ArrayList<>(); - changeEvents.addAll(getDocumentationChangeEvent(from.getValue(), to.getValue(), urn.toString(), auditStamp)); - changeEvents.addAll(getGlossaryTermChangeEvents(from.getValue(), to.getValue(), urn.toString(), auditStamp)); - changeEvents.addAll(getTagChangeEvents(from.getValue(), to.getValue(), urn.toString(), auditStamp)); - return changeEvents; - } - - private List getDocumentationChangeEvent(BusinessAttributeInfo baseBusinessAttributeInfo, - BusinessAttributeInfo targetBusinessAttributeInfo, - String entityUrn, AuditStamp auditStamp) { - String baseDescription = (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getDescription() : null; - String targetDescription = (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getDescription() : null; - List changeEvents = new ArrayList<>(); - if (baseDescription == null && targetDescription != null) { - changeEvents.add(createChangeEvent(targetBusinessAttributeInfo, entityUrn, - ChangeOperation.ADD, ATTRIBUTE_DOCUMENTATION_ADDED_FORMAT, auditStamp, targetDescription)); - } - - if (baseDescription != null && targetDescription == null) { - changeEvents.add(createChangeEvent(baseBusinessAttributeInfo, entityUrn, - ChangeOperation.REMOVE, ATTRIBUTE_DOCUMENTATION_REMOVED_FORMAT, auditStamp, baseDescription)); - } - - if (baseDescription != null && !baseDescription.equals(targetDescription)) { - changeEvents.add(createChangeEvent(targetBusinessAttributeInfo, entityUrn, - ChangeOperation.MODIFY, ATTRIBUTE_DOCUMENTATION_UPDATED_FORMAT, auditStamp, baseDescription, targetDescription)); - } - - return changeEvents; +public class BusinessAttributeInfoChangeEventGenerator + extends EntityChangeEventGenerator { + + public static final String ATTRIBUTE_DOCUMENTATION_ADDED_FORMAT = + "Documentation for the businessAttribute '%s' has been added: '%s'"; + public static final String ATTRIBUTE_DOCUMENTATION_REMOVED_FORMAT = + "Documentation for the businessAttribute '%s' has been removed: '%s'"; + public static final String ATTRIBUTE_DOCUMENTATION_UPDATED_FORMAT = + "Documentation for the businessAttribute '%s' has been updated from '%s' to '%s'."; + + @Override + public List getChangeEvents( + @Nonnull Urn urn, + @Nonnull String entity, + @Nonnull String aspect, + @Nonnull Aspect from, + @Nonnull Aspect to, + @Nonnull AuditStamp auditStamp) { + final List changeEvents = new ArrayList<>(); + changeEvents.addAll( + getDocumentationChangeEvent(from.getValue(), to.getValue(), urn.toString(), auditStamp)); + changeEvents.addAll( + getGlossaryTermChangeEvents(from.getValue(), to.getValue(), urn.toString(), auditStamp)); + changeEvents.addAll( + getTagChangeEvents(from.getValue(), to.getValue(), urn.toString(), auditStamp)); + return changeEvents; + } + + private List getDocumentationChangeEvent( + BusinessAttributeInfo baseBusinessAttributeInfo, + BusinessAttributeInfo targetBusinessAttributeInfo, + String entityUrn, + AuditStamp auditStamp) { + String baseDescription = + (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getDescription() : null; + String targetDescription = + (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getDescription() : null; + List changeEvents = new ArrayList<>(); + if (baseDescription == null && targetDescription != null) { + changeEvents.add( + createChangeEvent( + targetBusinessAttributeInfo, + entityUrn, + ChangeOperation.ADD, + ATTRIBUTE_DOCUMENTATION_ADDED_FORMAT, + auditStamp, + targetDescription)); } - private List getGlossaryTermChangeEvents(BusinessAttributeInfo baseBusinessAttributeInfo, - BusinessAttributeInfo targetBusinessAttributeInfo, - String entityUrn, AuditStamp auditStamp) { - GlossaryTerms baseGlossaryTerms = (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getGlossaryTerms() : null; - GlossaryTerms targetGlossaryTerms = (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getGlossaryTerms() : null; - - List entityGlossaryTermsChangeEvents = - GlossaryTermsChangeEventGenerator.computeDiffs(baseGlossaryTerms, targetGlossaryTerms, - entityUrn.toString(), auditStamp); - - return entityGlossaryTermsChangeEvents; + if (baseDescription != null && targetDescription == null) { + changeEvents.add( + createChangeEvent( + baseBusinessAttributeInfo, + entityUrn, + ChangeOperation.REMOVE, + ATTRIBUTE_DOCUMENTATION_REMOVED_FORMAT, + auditStamp, + baseDescription)); } - private List getTagChangeEvents(BusinessAttributeInfo baseBusinessAttributeInfo, - BusinessAttributeInfo targetBusinessAttributeInfo, - String entityUrn, AuditStamp auditStamp) { - GlobalTags baseGlobalTags = (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getGlobalTags() : null; - GlobalTags targetGlobalTags = (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getGlobalTags() : null; - - List entityTagChangeEvents = - GlobalTagsChangeEventGenerator.computeDiffs(baseGlobalTags, targetGlobalTags, entityUrn.toString(), - auditStamp); - - return entityTagChangeEvents; + if (baseDescription != null && !baseDescription.equals(targetDescription)) { + changeEvents.add( + createChangeEvent( + targetBusinessAttributeInfo, + entityUrn, + ChangeOperation.MODIFY, + ATTRIBUTE_DOCUMENTATION_UPDATED_FORMAT, + auditStamp, + baseDescription, + targetDescription)); } - private ChangeEvent createChangeEvent(BusinessAttributeInfo businessAttributeInfo, String entityUrn, - ChangeOperation operation, String format, AuditStamp auditStamp, String... descriptions) { - List args = new ArrayList<>(); - args.add(0, businessAttributeInfo.getFieldPath()); - Arrays.stream(descriptions).forEach(val -> args.add(val)); - return ChangeEvent.builder() - .modifier(businessAttributeInfo.getFieldPath()) - .entityUrn(entityUrn) - .category(ChangeCategory.DOCUMENTATION) - .operation(operation) - .semVerChange(SemanticChangeType.MINOR) - .description(String.format(format, args.toArray())) - .auditStamp(auditStamp) - .build(); - } + return changeEvents; + } + + private List getGlossaryTermChangeEvents( + BusinessAttributeInfo baseBusinessAttributeInfo, + BusinessAttributeInfo targetBusinessAttributeInfo, + String entityUrn, + AuditStamp auditStamp) { + GlossaryTerms baseGlossaryTerms = + (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getGlossaryTerms() : null; + GlossaryTerms targetGlossaryTerms = + (targetBusinessAttributeInfo != null) + ? targetBusinessAttributeInfo.getGlossaryTerms() + : null; + + List entityGlossaryTermsChangeEvents = + GlossaryTermsChangeEventGenerator.computeDiffs( + baseGlossaryTerms, targetGlossaryTerms, entityUrn.toString(), auditStamp); + + return entityGlossaryTermsChangeEvents; + } + + private List getTagChangeEvents( + BusinessAttributeInfo baseBusinessAttributeInfo, + BusinessAttributeInfo targetBusinessAttributeInfo, + String entityUrn, + AuditStamp auditStamp) { + GlobalTags baseGlobalTags = + (baseBusinessAttributeInfo != null) ? baseBusinessAttributeInfo.getGlobalTags() : null; + GlobalTags targetGlobalTags = + (targetBusinessAttributeInfo != null) ? targetBusinessAttributeInfo.getGlobalTags() : null; + + List entityTagChangeEvents = + GlobalTagsChangeEventGenerator.computeDiffs( + baseGlobalTags, targetGlobalTags, entityUrn.toString(), auditStamp); + + return entityTagChangeEvents; + } + + private ChangeEvent createChangeEvent( + BusinessAttributeInfo businessAttributeInfo, + String entityUrn, + ChangeOperation operation, + String format, + AuditStamp auditStamp, + String... descriptions) { + List args = new ArrayList<>(); + args.add(0, businessAttributeInfo.getFieldPath()); + Arrays.stream(descriptions).forEach(val -> args.add(val)); + return ChangeEvent.builder() + .modifier(businessAttributeInfo.getFieldPath()) + .entityUrn(entityUrn) + .category(ChangeCategory.DOCUMENTATION) + .operation(operation) + .semVerChange(SemanticChangeType.MINOR) + .description(String.format(format, args.toArray())) + .auditStamp(auditStamp) + .build(); + } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java index f6d60d0b4a3c9f..a7c4cf2e863d60 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java @@ -78,7 +78,9 @@ private static List getAllChangeEvents( getGlossaryTermChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); } if (changeCategory == ChangeCategory.BUSINESS_ATTRIBUTE) { - changeEvents.addAll(getBusinessAttributeAssociationChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); + changeEvents.addAll( + getBusinessAttributeAssociationChangeEvents( + baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); } return changeEvents; } @@ -264,24 +266,28 @@ private static List getTagChangeEvents( return Collections.emptyList(); } - private static List getBusinessAttributeAssociationChangeEvents(EditableSchemaFieldInfo baseFieldInfo, - EditableSchemaFieldInfo targetFieldInfo, - Urn datasetFieldUrn, AuditStamp auditStamp) { - BusinessAttributeAssociation baseBusinessAttributeAssociation = (baseFieldInfo != null) ? baseFieldInfo.getBusinessAttribute() : null; - BusinessAttributeAssociation targetBusinessAttributeAssociation = (targetFieldInfo != null) ? targetFieldInfo.getBusinessAttribute() : null; - - // 1. Get EntityBusinessAttributeAssociationChangeEvent, then rebind into a SchemaFieldBusinessAttributeAssociationChangeEvent. - List entityBusinessAttributeAssociationChangeEvents = - BusinessAttributeAssociationChangeEventGenerator.computeDiffs(baseBusinessAttributeAssociation, - targetBusinessAttributeAssociation, datasetFieldUrn.toString(), - auditStamp); - - return entityBusinessAttributeAssociationChangeEvents; - } + private static List getBusinessAttributeAssociationChangeEvents( + EditableSchemaFieldInfo baseFieldInfo, + EditableSchemaFieldInfo targetFieldInfo, + Urn datasetFieldUrn, + AuditStamp auditStamp) { + BusinessAttributeAssociation baseBusinessAttributeAssociation = + (baseFieldInfo != null) ? baseFieldInfo.getBusinessAttribute() : null; + BusinessAttributeAssociation targetBusinessAttributeAssociation = + (targetFieldInfo != null) ? targetFieldInfo.getBusinessAttribute() : null; + + // 1. Get EntityBusinessAttributeAssociationChangeEvent, then rebind into a + // SchemaFieldBusinessAttributeAssociationChangeEvent. + List entityBusinessAttributeAssociationChangeEvents = + BusinessAttributeAssociationChangeEventGenerator.computeDiffs( + baseBusinessAttributeAssociation, + targetBusinessAttributeAssociation, + datasetFieldUrn.toString(), + auditStamp); + + return entityBusinessAttributeAssociationChangeEvents; + } - @Override - public ChangeTransaction getSemanticDiff(EntityAspect previousValue, EntityAspect currentValue, - ChangeCategory element, JsonPatch rawDiff, boolean rawDiffsRequested) { @Override public ChangeTransaction getSemanticDiff( EntityAspect previousValue, @@ -331,48 +337,41 @@ public ChangeTransaction getSemanticDiff( .build(); } - @Override - public List getChangeEvents( - @Nonnull Urn urn, - @Nonnull String entity, - @Nonnull String aspect, - @Nonnull Aspect from, - @Nonnull Aspect to, - @Nonnull AuditStamp auditStamp) { - final List changeEvents = new ArrayList<>(); - changeEvents.addAll( + @Override + public List getChangeEvents( + @Nonnull Urn urn, + @Nonnull String entity, + @Nonnull String aspect, + @Nonnull Aspect from, + @Nonnull Aspect to, + @Nonnull AuditStamp auditStamp) { + final List changeEvents = new ArrayList<>(); + changeEvents.addAll( computeDiffs( from.getValue(), to.getValue(), urn.toString(), ChangeCategory.DOCUMENTATION, auditStamp)); - changeEvents.addAll( + changeEvents.addAll( computeDiffs( from.getValue(), to.getValue(), urn.toString(), ChangeCategory.TAG, auditStamp)); - changeEvents.addAll( + changeEvents.addAll( computeDiffs( from.getValue(), to.getValue(), urn.toString(), ChangeCategory.TECHNICAL_SCHEMA, auditStamp)); - changeEvents.addAll( + changeEvents.addAll( computeDiffs( from.getValue(), to.getValue(), urn.toString(), ChangeCategory.GLOSSARY_TERM, auditStamp)); - changeEvents.addAll( - computeDiffs( - from.getValue(), - to.getValue(), - urn.toString(), - ChangeCategory.BUSINESS_ATTRIBUTE, - auditStamp)); - return changeEvents; - } + return changeEvents; + } private static Urn getDatasetFieldUrn( final EditableSchemaFieldInfo previous, diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java index 1eee928e734c7c..a9381af48b3d26 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/businessattribute/BusinessAttributeServiceFactory.java @@ -2,24 +2,25 @@ import com.linkedin.metadata.client.JavaEntityClient; import com.linkedin.metadata.service.BusinessAttributeService; +import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; -import javax.annotation.Nonnull; - @Component public class BusinessAttributeServiceFactory { - private final JavaEntityClient entityClient; + private final JavaEntityClient entityClient; + + public BusinessAttributeServiceFactory( + @Qualifier("javaEntityClient") JavaEntityClient entityClient) { + this.entityClient = entityClient; + } - public BusinessAttributeServiceFactory(@Qualifier("javaEntityClient") JavaEntityClient entityClient) { - this.entityClient = entityClient; - } - @Bean(name = "businessAttributeService") - @Scope("singleton") - @Nonnull - protected BusinessAttributeService getINSTANCE() throws Exception { - return new BusinessAttributeService(entityClient); - } + @Bean(name = "businessAttributeService") + @Scope("singleton") + @Nonnull + protected BusinessAttributeService getINSTANCE() throws Exception { + return new BusinessAttributeService(entityClient); + } } diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/eventgenerator/EntityChangeEventGeneratorRegistryFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/eventgenerator/EntityChangeEventGeneratorRegistryFactory.java index ba554e14b6c39e..7d0c291ecd7ebb 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/eventgenerator/EntityChangeEventGeneratorRegistryFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/timeline/eventgenerator/EntityChangeEventGeneratorRegistryFactory.java @@ -5,6 +5,7 @@ import com.linkedin.entity.client.SystemEntityClient; import com.linkedin.metadata.timeline.eventgenerator.AssertionRunEventChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.BusinessAttributeAssociationChangeEventGenerator; +import com.linkedin.metadata.timeline.eventgenerator.BusinessAttributeInfoChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.DataProcessInstanceRunEventChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.DatasetPropertiesChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.DeprecationChangeEventGenerator; @@ -20,7 +21,6 @@ import com.linkedin.metadata.timeline.eventgenerator.SchemaMetadataChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.SingleDomainChangeEventGenerator; import com.linkedin.metadata.timeline.eventgenerator.StatusChangeEventGenerator; -import com.linkedin.metadata.timeline.eventgenerator.BusinessAttributeInfoChangeEventGenerator; import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -53,8 +53,10 @@ protected EntityChangeEventGeneratorRegistry entityChangeEventGeneratorRegistry( registry.register( EDITABLE_DATASET_PROPERTIES_ASPECT_NAME, new EditableDatasetPropertiesChangeEventGenerator()); - registry.register(BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, new BusinessAttributeInfoChangeEventGenerator()); - registry.register(BUSINESS_ATTRIBUTE_ASSOCIATION, new BusinessAttributeAssociationChangeEventGenerator()); + registry.register( + BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, new BusinessAttributeInfoChangeEventGenerator()); + registry.register( + BUSINESS_ATTRIBUTE_ASSOCIATION, new BusinessAttributeAssociationChangeEventGenerator()); // Entity Lifecycle Differs registry.register(DATASET_KEY_ASPECT_NAME, new EntityKeyChangeEventGenerator<>()); diff --git a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java index e69de29bb2d1d6..8b137891791fe9 100644 --- a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java +++ b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/delegates/EntityApiDelegateImpl.java @@ -0,0 +1 @@ + diff --git a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java index 39a7e4722988e1..4d2bb2f3faf644 100644 --- a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java +++ b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java @@ -25,6 +25,8 @@ import io.datahubproject.openapi.exception.UnauthorizedException; import io.datahubproject.openapi.generated.BrowsePathsV2AspectRequestV2; import io.datahubproject.openapi.generated.BrowsePathsV2AspectResponseV2; +import io.datahubproject.openapi.generated.BusinessAttributeInfoAspectRequestV2; +import io.datahubproject.openapi.generated.BusinessAttributeInfoAspectResponseV2; import io.datahubproject.openapi.generated.ChartInfoAspectRequestV2; import io.datahubproject.openapi.generated.ChartInfoAspectResponseV2; import io.datahubproject.openapi.generated.DataProductPropertiesAspectRequestV2; @@ -845,4 +847,40 @@ public ResponseEntity deleteFormInfo(String urn) { walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return deleteAspect(urn, methodNameToAspectName(methodName)); } + + public ResponseEntity createBusinessAttributeInfo( + BusinessAttributeInfoAspectRequestV2 body, String urn) { + String methodName = + walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); + return createAspect( + urn, + methodNameToAspectName(methodName), + body, + BusinessAttributeInfoAspectRequestV2.class, + BusinessAttributeInfoAspectResponseV2.class); + } + + public ResponseEntity deleteBusinessAttributeInfo(String urn) { + String methodName = + walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); + return deleteAspect(urn, methodNameToAspectName(methodName)); + } + + public ResponseEntity getBusinessAttributeInfo( + String urn, Boolean systemMetadata) { + String methodName = + walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); + return getAspect( + urn, + systemMetadata, + methodNameToAspectName(methodName), + _respClazz, + BusinessAttributeInfoAspectResponseV2.class); + } + + public ResponseEntity headBusinessAttributeInfo(String urn) { + String methodName = + walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); + return headAspect(urn, methodNameToAspectName(methodName)); + } } diff --git a/metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java b/metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java index 5aa10eef0603b6..9cb47ded0819ec 100644 --- a/metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java +++ b/metadata-service/services/src/main/java/com/linkedin/metadata/service/BusinessAttributeService.java @@ -5,31 +5,35 @@ import com.linkedin.entity.EntityResponse; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nonnull; import java.util.Objects; import java.util.Set; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; @Slf4j public class BusinessAttributeService { - private final EntityClient _entityClient; + private final EntityClient _entityClient; - public BusinessAttributeService(EntityClient entityClient) { - _entityClient = entityClient; - } + public BusinessAttributeService(EntityClient entityClient) { + _entityClient = entityClient; + } - public EntityResponse getBusinessAttributeEntityResponse(@Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { - Objects.requireNonNull(businessAttributeUrn, "business attribute must not be null"); - Objects.requireNonNull(authentication, "authentication must not be null"); - try { - return _entityClient.batchGetV2( - Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, - Set.of(businessAttributeUrn), - Set.of(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME), - authentication).get(businessAttributeUrn); - } catch (Exception e) { - throw new RuntimeException(String.format("Failed to retrieve Business Attribute with urn %s", businessAttributeUrn), e); - } + public EntityResponse getBusinessAttributeEntityResponse( + @Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { + Objects.requireNonNull(businessAttributeUrn, "business attribute must not be null"); + Objects.requireNonNull(authentication, "authentication must not be null"); + try { + return _entityClient + .batchGetV2( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + Set.of(businessAttributeUrn), + Set.of(Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME), + authentication) + .get(businessAttributeUrn); + } catch (Exception e) { + throw new RuntimeException( + String.format("Failed to retrieve Business Attribute with urn %s", businessAttributeUrn), + e); } + } } diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index f0941a3202f1d6..cc14b1841405dc 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -112,39 +112,41 @@ public class PoliciesConfig { "Manage Ownership Types", "Create, update and delete Ownership Types."); - public static final Privilege CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE = Privilege.of( - "CREATE_BUSINESS_ATTRIBUTE", - "Create Business Attribute", - "Create new Business Attribute."); - - public static final Privilege MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE = Privilege.of( - "MANAGE_BUSINESS_ATTRIBUTE", - "Manage Business Attribute", - "Create, update, delete Business Attribute"); - - public static final List PLATFORM_PRIVILEGES = ImmutableList.of( - MANAGE_POLICIES_PRIVILEGE, - MANAGE_USERS_AND_GROUPS_PRIVILEGE, - VIEW_ANALYTICS_PRIVILEGE, - GET_ANALYTICS_PRIVILEGE, - MANAGE_DOMAINS_PRIVILEGE, - MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE, - MANAGE_INGESTION_PRIVILEGE, - MANAGE_SECRETS_PRIVILEGE, - GENERATE_PERSONAL_ACCESS_TOKENS_PRIVILEGE, - MANAGE_ACCESS_TOKENS, - MANAGE_TESTS_PRIVILEGE, - MANAGE_GLOSSARIES_PRIVILEGE, - MANAGE_USER_CREDENTIALS_PRIVILEGE, - MANAGE_TAGS_PRIVILEGE, - CREATE_TAGS_PRIVILEGE, - CREATE_DOMAINS_PRIVILEGE, - CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE, - MANAGE_GLOBAL_VIEWS, - MANAGE_GLOBAL_OWNERSHIP_TYPES, - CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE, - MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE - ); + public static final Privilege CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE = + Privilege.of( + "CREATE_BUSINESS_ATTRIBUTE", + "Create Business Attribute", + "Create new Business Attribute."); + + public static final Privilege MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE = + Privilege.of( + "MANAGE_BUSINESS_ATTRIBUTE", + "Manage Business Attribute", + "Create, update, delete Business Attribute"); + + public static final List PLATFORM_PRIVILEGES = + ImmutableList.of( + MANAGE_POLICIES_PRIVILEGE, + MANAGE_USERS_AND_GROUPS_PRIVILEGE, + VIEW_ANALYTICS_PRIVILEGE, + GET_ANALYTICS_PRIVILEGE, + MANAGE_DOMAINS_PRIVILEGE, + MANAGE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE, + MANAGE_INGESTION_PRIVILEGE, + MANAGE_SECRETS_PRIVILEGE, + GENERATE_PERSONAL_ACCESS_TOKENS_PRIVILEGE, + MANAGE_ACCESS_TOKENS, + MANAGE_TESTS_PRIVILEGE, + MANAGE_GLOSSARIES_PRIVILEGE, + MANAGE_USER_CREDENTIALS_PRIVILEGE, + MANAGE_TAGS_PRIVILEGE, + CREATE_TAGS_PRIVILEGE, + CREATE_DOMAINS_PRIVILEGE, + CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE, + MANAGE_GLOBAL_VIEWS, + MANAGE_GLOBAL_OWNERSHIP_TYPES, + CREATE_BUSINESS_ATTRIBUTE_PRIVILEGE, + MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE); // Resource Privileges // @@ -394,11 +396,11 @@ public class PoliciesConfig { "Produce Platform Event API", "The ability to produce Platform Events using the API."); - public static final Privilege EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE = Privilege.of( + public static final Privilege EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE = + Privilege.of( "EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE", "Edit Dataset Column Business Attribute", - "The ability to edit the column (field) business attribute associated with a dataset schema." - ); + "The ability to edit the column (field) business attribute associated with a dataset schema."); public static final ResourcePrivileges DATASET_PRIVILEGES = ResourcePrivileges.of( @@ -416,7 +418,8 @@ public class PoliciesConfig { EDIT_ENTITY_ASSERTIONS_PRIVILEGE, EDIT_LINEAGE_PRIVILEGE, EDIT_ENTITY_EMBED_PRIVILEGE, - EDIT_QUERIES_PRIVILEGE, EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE)) + EDIT_QUERIES_PRIVILEGE, + EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE)) .flatMap(Collection::stream) .collect(Collectors.toList())); @@ -580,31 +583,35 @@ public class PoliciesConfig { EDIT_USER_PROFILE_PRIVILEGE, EDIT_ENTITY_PRIVILEGE)); - public static final ResourcePrivileges BUSINESS_ATTRIBUTE_PRIVILEGES = ResourcePrivileges.of( + public static final ResourcePrivileges BUSINESS_ATTRIBUTE_PRIVILEGES = + ResourcePrivileges.of( "businessAttribute", "Business Attribute", "Business Attribute created on Datahub", - ImmutableList.of(VIEW_ENTITY_PAGE_PRIVILEGE, EDIT_ENTITY_OWNERS_PRIVILEGE, EDIT_ENTITY_DOCS_PRIVILEGE, EDIT_ENTITY_TAGS_PRIVILEGE, - EDIT_ENTITY_GLOSSARY_TERMS_PRIVILEGE) - ); - - public static final List ENTITY_RESOURCE_PRIVILEGES = ImmutableList.of( - DATASET_PRIVILEGES, - DASHBOARD_PRIVILEGES, - CHART_PRIVILEGES, - DATA_FLOW_PRIVILEGES, - DATA_JOB_PRIVILEGES, - TAG_PRIVILEGES, - CONTAINER_PRIVILEGES, - DOMAIN_PRIVILEGES, - GLOSSARY_TERM_PRIVILEGES, - GLOSSARY_NODE_PRIVILEGES, - CORP_GROUP_PRIVILEGES, - CORP_USER_PRIVILEGES, - NOTEBOOK_PRIVILEGES, - DATA_PRODUCT_PRIVILEGES, - BUSINESS_ATTRIBUTE_PRIVILEGES - ); + ImmutableList.of( + VIEW_ENTITY_PAGE_PRIVILEGE, + EDIT_ENTITY_OWNERS_PRIVILEGE, + EDIT_ENTITY_DOCS_PRIVILEGE, + EDIT_ENTITY_TAGS_PRIVILEGE, + EDIT_ENTITY_GLOSSARY_TERMS_PRIVILEGE)); + + public static final List ENTITY_RESOURCE_PRIVILEGES = + ImmutableList.of( + DATASET_PRIVILEGES, + DASHBOARD_PRIVILEGES, + CHART_PRIVILEGES, + DATA_FLOW_PRIVILEGES, + DATA_JOB_PRIVILEGES, + TAG_PRIVILEGES, + CONTAINER_PRIVILEGES, + DOMAIN_PRIVILEGES, + GLOSSARY_TERM_PRIVILEGES, + GLOSSARY_NODE_PRIVILEGES, + CORP_GROUP_PRIVILEGES, + CORP_USER_PRIVILEGES, + NOTEBOOK_PRIVILEGES, + DATA_PRODUCT_PRIVILEGES, + BUSINESS_ATTRIBUTE_PRIVILEGES); // Merge all entity specific resource privileges to create a superset of all resource privileges public static final ResourcePrivileges ALL_RESOURCE_PRIVILEGES = From 8f5ea637429a1fa8874a4cbd6ad1fd58634e4893 Mon Sep 17 00:00:00 2001 From: "Bharti, Aakash" Date: Sat, 3 Feb 2024 02:31:50 +0530 Subject: [PATCH 21/50] business-attribute: businessattributes change propagation platform event hook --- .../BusinessAttributeUpdateService.java | 122 ++++++++++++++++++ .../datahub/event/PlatformEventProcessor.java | 13 +- .../hook/BusinessAttributeUpdateHook.java | 34 +++++ .../datahub/event/hook/PlatformEventHook.java | 11 +- 4 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java create mode 100644 metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java diff --git a/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java b/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java new file mode 100644 index 00000000000000..677b7a011a82ce --- /dev/null +++ b/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java @@ -0,0 +1,122 @@ +package com.linkedin.metadata.service; + +import com.linkedin.common.urn.Urn; +import com.linkedin.dataset.EditableDatasetProperties; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.graph.GraphService; +import com.linkedin.metadata.graph.RelatedEntitiesResult; +import com.linkedin.metadata.graph.RelatedEntity; +import com.linkedin.metadata.models.AspectSpec; +import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.PlatformEvent; +import com.linkedin.platform.event.v1.EntityChangeEvent; +import com.linkedin.common.AuditStamp; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import com.google.common.collect.ImmutableSet; +import javax.annotation.Nonnull; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.HashSet; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import static com.linkedin.metadata.search.utils.QueryUtils.newFilter; +import static com.linkedin.metadata.search.utils.QueryUtils.newRelationshipFilter; +import static com.linkedin.metadata.search.utils.QueryUtils.EMPTY_FILTER; + +@Slf4j +@Component +public class BusinessAttributeUpdateService { + private static final String EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE = "EditableSchemaFieldWithBusinessAttribute"; + + private final GraphService _graphService; + private final EntityService _entityService; + private final EntityRegistry _entityRegistry; + + public static final String TAG = "TAG"; + public static final String GLOSSARY_TERM = "GLOSSARY_TERM"; + public static final String DOCUMENTATION = "DOCUMENTATION"; + + public BusinessAttributeUpdateService(GraphService graphService, EntityService entityService, + EntityRegistry entityRegistry) { + this._graphService = graphService; + this._entityService = entityService; + this._entityRegistry = entityRegistry; + } + + public void handleChangeEvent(@Nonnull final PlatformEvent event) { + final EntityChangeEvent entityChangeEvent = + GenericRecordUtils.deserializePayload(event.getPayload().getValue(), EntityChangeEvent.class); + + if (!entityChangeEvent.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { + log.info("Skipping MCL event for invalid event entity type: " + entityChangeEvent.getEntityType()); + return; + } + + final Set businessAttributeCategories = + ImmutableSet.of(TAG, GLOSSARY_TERM, DOCUMENTATION); + if (!businessAttributeCategories.contains(entityChangeEvent.getCategory())) { + log.info("Skipping MCL event for invalid event category: " + entityChangeEvent.getCategory()); + return; + } + + Urn urn = entityChangeEvent.getEntityUrn(); + log.info("Business Attribute update hook invoked for :" + urn.toString()); + + RelatedEntitiesResult relatedEntitiesResult = _graphService.findRelatedEntities( + null, newFilter("urn", urn.toString()), null, + EMPTY_FILTER, Arrays.asList(EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE), + newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING), 0, 100000); + + for (RelatedEntity relatedEntity : relatedEntitiesResult.getEntities()) { + String datasetUrnStr = relatedEntity.getUrn(); + Map datasetEntityResponses; + try { + Urn datasetUrn = new Urn(datasetUrnStr); + final AspectSpec datasetAspectSpec = _entityRegistry.getEntitySpec(Constants.DATASET_ENTITY_NAME) + .getAspectSpec(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME); + datasetEntityResponses = _entityService.getEntitiesV2(Constants.DATASET_ENTITY_NAME, + new HashSet<>(Arrays.asList(datasetUrn)), + Collections.singleton(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME) + ); + + EntityResponse datasetEntityResponse = datasetEntityResponses.get(datasetUrn); + EditableDatasetProperties datasetProperties = mapTermInfo(datasetEntityResponse); + final AuditStamp auditStamp = + new AuditStamp().setActor(Urn.createFromString(Constants.SYSTEM_ACTOR)).setTime(System.currentTimeMillis()); + + _entityService.alwaysProduceMCLAsync( + datasetUrn, + Constants.DATASET_ENTITY_NAME, + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + datasetAspectSpec, + null, + datasetProperties, + null, + null, + auditStamp, + ChangeType.RESTATE).getFirst(); + + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + } + + private EditableDatasetProperties mapTermInfo(EntityResponse entityResponse) { + EnvelopedAspectMap aspectMap = entityResponse.getAspects(); + if (!aspectMap.containsKey(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME)) { + return null; + } + return new EditableDatasetProperties(aspectMap.get(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME).getValue().data()); + } +} diff --git a/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/PlatformEventProcessor.java b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/PlatformEventProcessor.java index 955d5c67c09a78..3d3ea4461bf607 100644 --- a/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/PlatformEventProcessor.java +++ b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/PlatformEventProcessor.java @@ -9,9 +9,10 @@ import com.linkedin.metadata.utils.metrics.MetricUtils; import com.linkedin.mxe.PlatformEvent; import com.linkedin.mxe.Topics; -import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import lombok.Getter; import org.apache.avro.generic.GenericRecord; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.beans.factory.annotation.Autowired; @@ -20,22 +21,26 @@ import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; +import com.datahub.event.hook.BusinessAttributeUpdateHook; @Slf4j @Component @Conditional(PlatformEventProcessorCondition.class) -@Import({KafkaEventConsumerFactory.class}) +@Import({BusinessAttributeUpdateHook.class, KafkaEventConsumerFactory.class}) @EnableKafka public class PlatformEventProcessor { + @Getter private final List hooks; private final Histogram kafkaLagStats = MetricUtils.get().histogram(MetricRegistry.name(this.getClass(), "kafkaLag")); @Autowired - public PlatformEventProcessor() { + public PlatformEventProcessor(List platformEventHooks) { log.info("Creating Platform Event Processor"); - this.hooks = Collections.emptyList(); // No event hooks (yet) + this.hooks = platformEventHooks.stream() + .filter(PlatformEventHook::isEnabled) + .collect(Collectors.toList()); this.hooks.forEach(PlatformEventHook::init); } diff --git a/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java new file mode 100644 index 00000000000000..cb26a65fd82f77 --- /dev/null +++ b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java @@ -0,0 +1,34 @@ +package com.datahub.event.hook; + +import com.linkedin.gms.factory.common.GraphServiceFactory; +import com.linkedin.gms.factory.entity.EntityServiceFactory; +import com.linkedin.gms.factory.entityregistry.EntityRegistryFactory; +import com.linkedin.metadata.service.BusinessAttributeUpdateService; +import com.linkedin.mxe.PlatformEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Import; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; + +@Slf4j +@Component +@Import({EntityServiceFactory.class, EntityRegistryFactory.class, GraphServiceFactory.class}) +public class BusinessAttributeUpdateHook implements PlatformEventHook { + + protected final BusinessAttributeUpdateService _businessAttributeUpdateService; + + public BusinessAttributeUpdateHook(BusinessAttributeUpdateService businessAttributeUpdateService) { + this._businessAttributeUpdateService = businessAttributeUpdateService; + } + + /** + * Invoke the hook when a PlatformEvent is received + * + * @param event + */ + @Override + public void invoke(@Nonnull PlatformEvent event) { + _businessAttributeUpdateService.handleChangeEvent(event); + } +} diff --git a/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/PlatformEventHook.java b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/PlatformEventHook.java index 3083642c5bfb6f..d9f2ecdfebc616 100644 --- a/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/PlatformEventHook.java +++ b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/PlatformEventHook.java @@ -15,6 +15,15 @@ public interface PlatformEventHook { /** Initialize the hook */ default void init() {} - /** Invoke the hook when a PlatformEvent is received */ + /** + * Return whether the hook is enabled or not. If not enabled, the below invoke method is not triggered + */ + default boolean isEnabled() { + return true; + } + + /** + * Invoke the hook when a PlatformEvent is received + */ void invoke(@Nonnull PlatformEvent event); } From 7877912d74653a8dc56a503fa2ba5a5a4df172d1 Mon Sep 17 00:00:00 2001 From: aditigup Date: Mon, 12 Feb 2024 13:39:23 +0530 Subject: [PATCH 22/50] Business attribute UI changes merged with master --- .../src/app/buildEntityRegistry.ts | 4 +- .../components/SchemaDescriptionField.tsx | 72 +++++++++++++++ .../AboutSection/DescriptionSection.tsx | 92 +++++++++++++++---- .../tabs/Dataset/Schema/SchemaTable.tsx | 19 +--- .../SchemaFieldDrawer/FieldAttribute.tsx | 30 ++++++ .../SchemaFieldDrawer/FieldDescription.tsx | 4 +- .../SchemaFieldDrawer/SchemaFieldDrawer.tsx | 4 +- .../utils/useBusinessAttributeRenderer.tsx | 28 +++--- .../Schema/utils/useDescriptionRenderer.tsx | 8 ++ .../Schema/utils/useTagsAndTermsRenderer.tsx | 4 +- .../BusinessAttributeServiceFactory.java | 12 +-- .../businessAttribute/businessAttribute.js | 3 +- .../tests/cypress/cypress/e2e/home/home.js | 2 +- .../cypress/e2e/mutations/mutations.js | 17 ++-- .../tests/cypress/cypress/support/commands.js | 4 +- 15 files changed, 226 insertions(+), 77 deletions(-) create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx diff --git a/datahub-web-react/src/app/buildEntityRegistry.ts b/datahub-web-react/src/app/buildEntityRegistry.ts index 4f746815708029..f97268d3d24b5f 100644 --- a/datahub-web-react/src/app/buildEntityRegistry.ts +++ b/datahub-web-react/src/app/buildEntityRegistry.ts @@ -20,6 +20,7 @@ import { DataPlatformEntity } from './entity/dataPlatform/DataPlatformEntity'; import { DataProductEntity } from './entity/dataProduct/DataProductEntity'; import { DataPlatformInstanceEntity } from './entity/dataPlatformInstance/DataPlatformInstanceEntity'; import { RoleEntity } from './entity/Access/RoleEntity'; +import {BusinessAttributeEntity} from "./entity/businessAttribute/BusinessAttributeEntity"; export default function buildEntityRegistry() { const registry = new EntityRegistry(); @@ -44,5 +45,6 @@ export default function buildEntityRegistry() { registry.register(new DataPlatformEntity()); registry.register(new DataProductEntity()); registry.register(new DataPlatformInstanceEntity()); + registry.register(new BusinessAttributeEntity()); return registry; -} \ No newline at end of file +} diff --git a/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx b/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx index 2cd4cbd6dcb6ca..ce8d03fbdc9602 100644 --- a/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx +++ b/datahub-web-react/src/app/entity/dataset/profile/schema/components/SchemaDescriptionField.tsx @@ -11,6 +11,7 @@ import SchemaEditableContext from '../../../../../shared/SchemaEditableContext'; import { useEntityData } from '../../../../shared/EntityContext'; import analytics, { EventType, EntityActionType } from '../../../../../analytics'; import { Editor } from '../../../../shared/tabs/Documentation/components/editor/Editor'; +import { ANTD_GRAY } from '../../../../shared/constants'; const EditIcon = styled(EditOutlined)` cursor: pointer; @@ -77,9 +78,25 @@ const StyledViewer = styled(Editor)` } `; +const AttributeDescription = styled.div` + margin-top: 8px; + color: ${ANTD_GRAY[7]}; +`; + +const StyledAttributeViewer = styled(Editor)` + padding-right: 8px; + display: block; + .remirror-editor.ProseMirror { + padding: 0; + color: ${ANTD_GRAY[7]}; + } +`; + type Props = { onExpanded: (expanded: boolean) => void; + onBAExpanded?: (expanded: boolean) => void; expanded: boolean; + baExpanded?: boolean; description: string; original?: string | null; onUpdate: ( @@ -87,24 +104,31 @@ type Props = { ) => Promise, Record> | void>; isEdited?: boolean; isReadOnly?: boolean; + businessAttributeDescription?: string; }; const ABBREVIATED_LIMIT = 80; export default function DescriptionField({ expanded, + baExpanded, onExpanded: handleExpanded, + onBAExpanded: handleBAExpanded, description, onUpdate, isEdited = false, original, isReadOnly, + businessAttributeDescription, }: Props) { const [showAddModal, setShowAddModal] = useState(false); const overLimit = removeMarkdown(description).length > 80; const isSchemaEditable = React.useContext(SchemaEditableContext) && !isReadOnly; const onCloseModal = () => setShowAddModal(false); const { urn, entityType } = useEntityData(); + const attributeDescriptionOverLimit = businessAttributeDescription + ? removeMarkdown(businessAttributeDescription).length > 80 + : false; const sendAnalytics = () => { analytics.event({ @@ -199,6 +223,54 @@ export default function DescriptionField({ + Add Description )} + + {baExpanded || !attributeDescriptionOverLimit ? ( + <> + {!!businessAttributeDescription && ( + + )} + {!!businessAttributeDescription && ( + + {attributeDescriptionOverLimit && ( + { + e.stopPropagation(); + if (handleBAExpanded) { + handleBAExpanded(false); + } + }} + > + Read Less + + )} + + )} + + ) : ( + <> + + { + e.stopPropagation(); + if (handleBAExpanded) { + handleBAExpanded(true); + } + }} + > + Read More + + + } + shouldWrap + > + {businessAttributeDescription} + + + )} + ); } diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/AboutSection/DescriptionSection.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/AboutSection/DescriptionSection.tsx index a9c406306880d2..8263467290cbbe 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/AboutSection/DescriptionSection.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/AboutSection/DescriptionSection.tsx @@ -6,6 +6,10 @@ import MarkdownViewer, { MarkdownView } from '../../../../components/legacy/Mark import NoMarkdownViewer, { removeMarkdown } from '../../../../components/styled/StripMarkdownText'; import { useRouteToTab } from '../../../../EntityContext'; import { useIsOnTab } from '../../utils'; +import { ANTD_GRAY } from '../../../../constants'; +import { EntityType } from '../../../../../../../types.generated'; +import { useEntityRegistry } from '../../../../../../useEntityRegistry'; +import { useHistory } from 'react-router'; const ABBREVIATED_LIMIT = 150; @@ -17,18 +21,35 @@ const ContentWrapper = styled.div` } `; +const BaContentWrapper = styled.div` + margin-top: 8px; + color: ${ANTD_GRAY[7]}; + margin-bottom: 8px; + font-size: 14px; + ${MarkdownView} { + font-size: 14px; + } + color: ${ANTD_GRAY[7]}; +`; + interface Props { description: string; + baDescription?: string; isExpandable?: boolean; limit?: number; + baUrn?: string; } -export default function DescriptionSection({ description, isExpandable, limit }: Props) { +export default function DescriptionSection({ description, baDescription, isExpandable, limit, baUrn }: Props) { + const history = useHistory(); const isOverLimit = description && removeMarkdown(description).length > ABBREVIATED_LIMIT; + const isBaOverLimit = baDescription && removeMarkdown(baDescription).length > ABBREVIATED_LIMIT; const [isExpanded, setIsExpanded] = useState(!isOverLimit); + const [isBaExpanded, setIsBaExpanded] = useState(!isBaOverLimit); const routeToTab = useRouteToTab(); const isCompact = React.useContext(CompactContext); const shouldShowReadMore = !useIsOnTab('Documentation') || isExpandable; + const entityRegistry = useEntityRegistry(); // if we're not in compact mode, route them to the Docs tab for the best documentation viewing experience function readMore() { @@ -39,25 +60,56 @@ export default function DescriptionSection({ description, isExpandable, limit }: } } + function readBAMore() { + if(isCompact || isExpandable) { + setIsBaExpanded(true); + } else { + if (baUrn != null) { + history.push(entityRegistry.getEntityUrl(EntityType.BusinessAttribute, baUrn || '')); + } + } + } + return ( - - {isExpanded && ( - <> - - {isOverLimit && setIsExpanded(false)}>Read Less} - - )} - {!isExpanded && ( - Read More : undefined - } - shouldWrap - > - {description} - - )} - + <> + + {isExpanded && ( + <> + + {isOverLimit && setIsExpanded(false)}>Read Less} + + )} + {!isExpanded && ( + Read More : undefined + } + shouldWrap + > + {description} + + )} + + + {isBaExpanded && ( + <> + + {isBaOverLimit && setIsBaExpanded(false)}>Read Less} + + )} + {!isBaExpanded && ( + Read More : undefined + } + shouldWrap + > + {baDescription} + + )} + + ); } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx index c5b1cd28864e33..35f3bbd7571da2 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx @@ -123,26 +123,12 @@ export default function SchemaTable({ ); const businessAttributeRenderer = useBusinessAttributeRenderer( editableSchemaMetadata, - attributeHoveredIndex, - setAttributeHoveredIndex, filterText, + false, ); const schemaTitleRenderer = useSchemaTitleRenderer(schemaMetadata, setSelectedFkFieldPath, filterText); const schemaBlameRenderer = useSchemaBlameRenderer(schemaFieldBlameList); - const onAttributeCell = (record: SchemaField) => ({ - onMouseEnter: () => { - if (editMode) { - setAttributeHoveredIndex(record.fieldPath); - } - }, - onMouseLeave: () => { - if (editMode) { - setAttributeHoveredIndex(undefined); - } - }, - }); - const fieldColumn = { width: '22%', title: 'Field', @@ -181,11 +167,10 @@ export default function SchemaTable({ const businessAttributeColumn = { width: '18%', - title: 'Business Attributes', + title: 'Business Attribute', dataIndex: 'businessAttribute', key: 'businessAttribute', render: businessAttributeRenderer, - onCell: onAttributeCell, }; const blameColumn = { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx new file mode 100644 index 00000000000000..75a7f586bcf91f --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { EditableSchemaMetadata, SchemaField } from '../../../../../../../../types.generated'; +import useBusinessAttributeRenderer from '../../utils/useBusinessAttributeRenderer'; +import { SectionHeader, StyledDivider } from './components'; +import SchemaEditableContext from '../../../../../../../shared/SchemaEditableContext'; + +interface Props { + expandedField: SchemaField; + editableSchemaMetadata?: EditableSchemaMetadata | null; +} + +export default function FieldAttribute({ expandedField, editableSchemaMetadata }: Props) { + const isSchemaEditable = React.useContext(SchemaEditableContext); + const attributeRenderer = useBusinessAttributeRenderer( + editableSchemaMetadata, + '', + isSchemaEditable, + ); + + return ( + <> + Business Attribute + {/* pass in globalTags since this is a shared component, tags will not be shown or used */} +
+ {attributeRenderer(editableSchemaMetadata, expandedField)} +
+ + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx index 410d2801d51c87..9c631d769e7791 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx @@ -77,13 +77,15 @@ export default function FieldDescription({ expandedField, editableFieldInfo }: P }); const displayedDescription = editableFieldInfo?.description || expandedField.description; + const baDescription = editableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.description; + const baUrn = editableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.urn; return ( <>
Description - +
{isSchemaEditable && ( - + + )} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx index 785a80fba681d9..9ac8f91bb7a677 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx @@ -7,9 +7,8 @@ import BusinessAttributeGroup from '../../../../../../shared/businessAttribute/B export default function useBusinessAttributeRenderer( editableSchemaMetadata: EditableSchemaMetadata | null | undefined, - attributeHoveredIndex: string | undefined, - setAttributeHoveredIndex: (index: string | undefined) => void, filterText: string, + canEdit: boolean, ) { const urn = useMutationUrn(); const refetch = useRefetch(); @@ -26,20 +25,17 @@ export default function useBusinessAttributeRenderer( ); return ( -
- setAttributeHoveredIndex(undefined)} - entityUrn={urn} - entityType={EntityType.Dataset} - entitySubresource={record.fieldPath} - highlightText={filterText} - refetch={refresh} - /> -
+ ); }; } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx index 5f2b5d23771c07..3709449605c9bc 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useDescriptionRenderer.tsx @@ -13,6 +13,7 @@ export default function useDescriptionRenderer(editableSchemaMetadata: EditableS const schemaRefetch = useSchemaRefetch(); const [updateDescription] = useUpdateDescriptionMutation(); const [expandedRows, setExpandedRows] = useState({}); + const [expandedBARows, setExpandedBARows] = useState({}); const refresh: any = () => { refetch?.(); @@ -26,13 +27,20 @@ export default function useDescriptionRenderer(editableSchemaMetadata: EditableS const displayedDescription = relevantEditableFieldInfo?.description || description; const sanitizedDescription = DOMPurify.sanitize(displayedDescription); const original = record.description ? DOMPurify.sanitize(record.description) : undefined; + const businessAttributeDescription = + relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties + ?.description || ''; const handleExpandedRows = (expanded) => setExpandedRows((prev) => ({ ...prev, [index]: expanded })); + const handleBAExpandedRows = (expanded) => setExpandedBARows((prev) => ({ ...prev, [index]: expanded })); return ( { cy.goToBusinessAttributeList(); cy.clickOptionWithText("Create Business Attribute"); - cy.addViaModal(businessAttribute, "Create Business Attribute"); + cy.addViaModal(businessAttribute, "Create Business Attribute", businessAttribute, "create-business-attribute-button"); cy.wait(3000); cy.goToBusinessAttributeList().contains(businessAttribute).should("be.visible"); @@ -80,6 +80,7 @@ describe("businessAttribute", () => { cy.wait(3000); + cy.clickOptionWithText("event_name"); cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => cy .get("span[aria-label=close]") diff --git a/smoke-test/tests/cypress/cypress/e2e/home/home.js b/smoke-test/tests/cypress/cypress/e2e/home/home.js index 0039114ff9c14c..3b6aef97de0405 100644 --- a/smoke-test/tests/cypress/cypress/e2e/home/home.js +++ b/smoke-test/tests/cypress/cypress/e2e/home/home.js @@ -2,7 +2,7 @@ describe('home', () => { it('home page shows ', () => { cy.login(); cy.visit('/'); - cy.get('img[src="/assets/platforms/datahublogo.png"]').should('exist'); + // cy.get('img[src="/assets/platforms/datahublogo.png"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-DATASET"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-DASHBOARD"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-CHART"]').should('exist'); diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js index c6cfda75bdd6f0..fb59783ebfba9c 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js @@ -161,6 +161,7 @@ describe("mutations", () => { cy.viewport(2000, 800); cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); + cy.clickOptionWithText("event_data"); cy.get('[data-testid="schema-field-event_data-businessAttribute"]').trigger( "mouseover", { force: true } @@ -169,15 +170,19 @@ describe("mutations", () => { cy.contains("Add Attribute").click({ force: true }) ); - cy.selectOptionInAttributeModal("test"); + cy.selectOptionInAttributeModal("cypressTestAttribute"); - cy.contains("test"); + cy.contains("cypressTestAttribute"); - cy.get( - 'a[href="/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449"]' - ).within(() => cy.get("span[aria-label=close]").click({ force: true })); + cy.get('[data-testid="schema-field-event_data-businessAttribute"]'). + within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); cy.contains("Yes").click({ force: true }); - cy.contains("test").should("not.exist"); + cy.contains("cypressTestAttribute").should("not.exist"); }); }); diff --git a/smoke-test/tests/cypress/cypress/support/commands.js b/smoke-test/tests/cypress/cypress/support/commands.js index 4c09a698981e97..aeafe95d722ff1 100644 --- a/smoke-test/tests/cypress/cypress/support/commands.js +++ b/smoke-test/tests/cypress/cypress/support/commands.js @@ -285,8 +285,8 @@ Cypress.Commands.add('addTermToBusinessAttribute', (urn, attribute_name, term) = Cypress.Commands.add('addAttributeToDataset', (urn, dataset_name, businessAttribute) => { cy.goToDataset(urn, dataset_name); - cy.contains("Business Attributes"); - cy.mouseover('[data-testid="schema-field-event_name-businessAttribute"]'); + cy.clickOptionWithText("event_name"); + cy.contains("Business Attribute"); cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => cy.contains("Add Attribute").click() ); From 950f40b8f14236afbdd33e34343b2ce711c2cb5c Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Tue, 13 Feb 2024 15:58:40 +0530 Subject: [PATCH 23/50] business-attribute: documentation --- docs-website/sidebars.js | 1 + docs/businessattributes.md | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 docs/businessattributes.md diff --git a/docs-website/sidebars.js b/docs-website/sidebars.js index 1e6d8bec018138..c8691499b71f92 100644 --- a/docs-website/sidebars.js +++ b/docs-website/sidebars.js @@ -57,6 +57,7 @@ module.exports = { "docs/posts", "docs/sync-status", "docs/generated/lineage/lineage-feature-guide", + "docs/businessattributes", { type: "doc", id: "docs/tests/metadata-tests", diff --git a/docs/businessattributes.md b/docs/businessattributes.md new file mode 100644 index 00000000000000..9561b3f9589bac --- /dev/null +++ b/docs/businessattributes.md @@ -0,0 +1,64 @@ +# Business Attributes + + +## What are Business Attributes +A Business attribute is a centrally managed logical field that represents a unique schema field entity. This common construct is global in nature, i.e. it is not bound to a project or application implementation. Instead, its identity exists in representing the same field across various datasets owned by various different projects and applications. Projects or applications use the Business attribute to model a column in a dataset and inherit information about it such as definitions, data type, data quality rules/assertions, tags, glossary terms etc from the global definition. + +## Benefits of Business Attributes +Data architects can use the concept of the business attribute to validate whether applications are conformant with the applicable metadata defined for the business attribute. By abstracting common business metadata into a logical model, different personas with appropriate business knowledge can define pertinent details, like rich definition, business use for the attribute, classification (i.e. PII, sensitive, shareable etc.), specific data rules that govern the attribute, connection to glossary terms. + +With Business Attributes users have the ability to search associated datasets using business description/tags/glossary attached to business attribute +## How can you use Business Attributes +Business attributes can be used to define a common set of metadata for a logical field that is used across multiple datasets. This metadata can be used to drive data quality, data governance, and data discovery. For example, a business attribute can be used to define a common set of data quality rules that are applicable to a logical field across multiple datasets. This can be used to ensure that the same data quality rules are applied consistently across all datasets that use the logical field. + +## Business Attributes Setup, Prerequisites, and Permissions +What you need to create/update and associate business attributes to dataset schema field + +* **Manage Business Attributes** platform privilege to create/update/delete business attributes. +* **Edit Dataset Column Business Attribute** metadata privilege to associate business attributes to dataset schema field. + +## Using Business Attributes +As of now Business Attributes can only be created through UI + +### Creating a Business Attribute (UI) +To create a Business Attribute, first navigate to the Business Attributes tab on the home page. + +

+ + +Then click on '+ Create Business Attribute'. +This will open a new modal where you can configure the settings for your business attribute. Inside the form, you can choose a name for your Business Attribute. Most often, this will align with the logical purpose of the Business Attribute, +for example 'Customer ID'. You can also add documentation for your Business Attribute to help other users easily discover it. This can be changed later. + +We can also add Datatype for Business Attribute. It has String as a default value. + +

+ + +Once you've chosen a name and a description, click 'Create' to create the new Business Attribute. + +Then we can attach tags and glossary terms to it to make it more discoverable. + +### Assigning Business Attribute to a Dataset Schema Field (UI) +You can associate the business attribute to a dataset schema field using the Dataset's schema page as the starting point. As per design, there is one to one mapping between business attribute and dataset schema field. + +On a Dataset's schema page, click the 'Add Attribute' to add business attribute to the dataset schema field. + +

+ + + +After association, dataset schema field gets its description, tags and glossary inherited from Business attribute. +Description inherited from business attribute is greyed out to differentiate between original description of schema field. + +

+ + +### What updates are planned for the Business Attributes feature? + +- Ingestion of Business attributes through recipe file (YAML) +- AutoTagging of Business attributes to child datasets through lineage + +### Related Features +* [Glossary Terms](./glossary/business-glossary.md) +* [Tags](./tags.md) \ No newline at end of file From 6b25744deb8beeaf2f9c06e33813ed37433055f7 Mon Sep 17 00:00:00 2001 From: "Singh, Himanshu" Date: Tue, 13 Feb 2024 19:09:26 +0530 Subject: [PATCH 24/50] Business Attribute : SearchableRef Annotation Elastic Insert and Search --- .../plugins/validation/AspectRetriever.java | 2 + .../linkedin/metadata/models/AspectSpec.java | 11 ++ .../metadata/models/DefaultEntitySpec.java | 15 +- .../linkedin/metadata/models/EntitySpec.java | 7 + .../metadata/models/EntitySpecBuilder.java | 18 ++ .../models/SearchableRefFieldSpec.java | 19 +++ .../SearchableRefFieldSpecExtractor.java | 160 ++++++++++++++++++ .../annotation/SearchableRefAnnotation.java | 121 +++++++++++++ .../StructuredPropertiesValidatorTest.java | 5 + .../PluginEntityRegistryLoaderTest.java | 1 + .../java/com/linkedin/metadata/Constants.java | 4 + .../client/EntityClientAspectRetriever.java | 6 + .../indexbuilder/EntityIndexBuilders.java | 3 + .../indexbuilder/MappingsBuilder.java | 50 +++++- .../elasticsearch/query/ESBrowseDAO.java | 6 +- .../elasticsearch/query/ESSearchDAO.java | 16 +- .../query/request/SearchFieldConfig.java | 78 +++++++++ .../query/request/SearchQueryBuilder.java | 15 ++ .../query/request/SearchRequestHandler.java | 18 +- .../SearchDocumentTransformer.java | 133 +++++++++++++++ .../metadata/search/utils/ESUtils.java | 3 +- .../query/request/SearchQueryBuilderTest.java | 7 + .../request/SearchRequestHandlerTest.java | 17 +- .../schema/EditableSchemaFieldInfo.pdl | 7 +- .../boot/steps/IngestDataTypesStepTest.java | 2 +- .../metadata/entity/EntityService.java | 4 + .../gms/servlet/ConfigSearchExport.java | 3 +- .../src/main/java/mock/MockAspectSpec.java | 3 + .../src/main/java/mock/MockEntitySpec.java | 1 + 29 files changed, 702 insertions(+), 33 deletions(-) create mode 100644 entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpec.java create mode 100644 entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java create mode 100644 entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableRefAnnotation.java diff --git a/entity-registry/src/main/java/com/linkedin/metadata/aspect/plugins/validation/AspectRetriever.java b/entity-registry/src/main/java/com/linkedin/metadata/aspect/plugins/validation/AspectRetriever.java index 11cd2352025efe..169a7ed17c46a9 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/aspect/plugins/validation/AspectRetriever.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/aspect/plugins/validation/AspectRetriever.java @@ -22,6 +22,8 @@ default Aspect getLatestAspectObject(@Nonnull final Urn urn, @Nonnull final Stri .get(aspectName); } + boolean exists(@Nonnull Urn urn) throws RemoteInvocationException, URISyntaxException; + /** * Returns for each URN, the map of aspectName to Aspect * diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/AspectSpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/AspectSpec.java index 9cf8b4174ecfbd..a2ff81da564017 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/AspectSpec.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/AspectSpec.java @@ -23,6 +23,7 @@ public class AspectSpec { private final Map _relationshipFieldSpecs; private final Map _timeseriesFieldSpecs; private final Map _timeseriesFieldCollectionSpecs; + private final Map _searchableRefFieldSpecs; // Classpath & Pegasus-specific: Temporary. private final RecordDataSchema _schema; @@ -37,6 +38,7 @@ public AspectSpec( @Nonnull final List relationshipFieldSpecs, @Nonnull final List timeseriesFieldSpecs, @Nonnull final List timeseriesFieldCollectionSpecs, + @Nonnull final List searchableRefFieldSpecs, final RecordDataSchema schema, final Class aspectClass) { _aspectAnnotation = aspectAnnotation; @@ -45,6 +47,11 @@ public AspectSpec( .collect( Collectors.toMap( spec -> spec.getPath().toString(), spec -> spec, (val1, val2) -> val1)); + _searchableRefFieldSpecs = + searchableRefFieldSpecs.stream() + .collect( + Collectors.toMap( + spec -> spec.getPath().toString(), spec -> spec, (val1, val2) -> val1)); _searchScoreFieldSpecs = searchScoreFieldSpecs.stream() .collect( @@ -113,6 +120,10 @@ public List getSearchableFieldSpecs() { return new ArrayList<>(_searchableFieldSpecs.values()); } + public List getSearchableRefFieldSpecs() { + return new ArrayList<>(_searchableRefFieldSpecs.values()); + } + public List getSearchScoreFieldSpecs() { return new ArrayList<>(_searchScoreFieldSpecs.values()); } diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java index 2546674f9835cb..38a3cbdeb458a8 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/DefaultEntitySpec.java @@ -4,11 +4,7 @@ import com.linkedin.data.schema.TyperefDataSchema; import com.linkedin.metadata.models.annotation.EntityAnnotation; import com.linkedin.metadata.models.annotation.SearchableAnnotation; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -27,6 +23,7 @@ public class DefaultEntitySpec implements EntitySpec { private List _searchableFieldSpecs; private Map> searchableFieldTypeMap; + private List _searchableRefFieldSpecs; public DefaultEntitySpec( @Nonnull final Collection aspectSpecs, @@ -106,6 +103,14 @@ public List getSearchableFieldSpecs() { return _searchableFieldSpecs; } + @Override + public List getSearchableRefFieldSpecs() { + if (_searchableRefFieldSpecs == null) { + _searchableRefFieldSpecs = EntitySpec.super.getSearchableRefFieldSpecs(); + } + return _searchableRefFieldSpecs; + } + @Override public Map> getSearchableFieldTypes() { if (searchableFieldTypeMap == null) { diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java index 9a75cc1f751d3b..02fd1b22b52d6e 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpec.java @@ -89,4 +89,11 @@ default List getRelationshipFieldSpecs() { .flatMap(List::stream) .collect(Collectors.toList()); } + + default List getSearchableRefFieldSpecs() { + return getAspectSpecs().stream() + .map(AspectSpec::getSearchableRefFieldSpecs) + .flatMap(List::stream) + .collect(Collectors.toList()); + } } diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java index 54f2206798da0d..fcad25156884a4 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java @@ -17,6 +17,7 @@ import com.linkedin.metadata.models.annotation.RelationshipAnnotation; import com.linkedin.metadata.models.annotation.SearchScoreAnnotation; import com.linkedin.metadata.models.annotation.SearchableAnnotation; +import com.linkedin.metadata.models.annotation.SearchableRefAnnotation; import com.linkedin.metadata.models.annotation.TimeseriesFieldAnnotation; import com.linkedin.metadata.models.annotation.TimeseriesFieldCollectionAnnotation; import java.util.ArrayList; @@ -39,6 +40,8 @@ public class EntitySpecBuilder { new PegasusSchemaAnnotationHandlerImpl(SearchableAnnotation.ANNOTATION_NAME); public static SchemaAnnotationHandler _searchScoreHandler = new PegasusSchemaAnnotationHandlerImpl(SearchScoreAnnotation.ANNOTATION_NAME); + public static SchemaAnnotationHandler _searchRefScoreHandler = + new PegasusSchemaAnnotationHandlerImpl(SearchableRefAnnotation.ANNOTATION_NAME); public static SchemaAnnotationHandler _relationshipHandler = new PegasusSchemaAnnotationHandlerImpl(RelationshipAnnotation.ANNOTATION_NAME); public static SchemaAnnotationHandler _timeseriesFiledAnnotationHandler = @@ -222,6 +225,7 @@ public AspectSpec buildAspectSpec( Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), aspectRecordSchema, aspectClass); } @@ -245,6 +249,19 @@ public AspectSpec buildAspectSpec( aspectRecordSchema, new SchemaAnnotationProcessor.AnnotationProcessOption()); + // Extract SearchableRef Field Specs + final SchemaAnnotationProcessor.SchemaAnnotationProcessResult processedSearchRefResult = + SchemaAnnotationProcessor.process( + Collections.singletonList(_searchRefScoreHandler), + aspectRecordSchema, + new SchemaAnnotationProcessor.AnnotationProcessOption()); + + final SearchableRefFieldSpecExtractor searchableRefFieldSpecExtractor = + new SearchableRefFieldSpecExtractor(); + final DataSchemaRichContextTraverser searchableRefFieldSpecTraverser = + new DataSchemaRichContextTraverser(searchableRefFieldSpecExtractor); + searchableRefFieldSpecTraverser.traverse(processedSearchRefResult.getResultSchema()); + // Extract SearchScore Field Specs final SearchScoreFieldSpecExtractor searchScoreFieldSpecExtractor = new SearchScoreFieldSpecExtractor(); @@ -289,6 +306,7 @@ public AspectSpec buildAspectSpec( relationshipFieldSpecExtractor.getSpecs(), timeseriesFieldSpecExtractor.getTimeseriesFieldSpecs(), timeseriesFieldSpecExtractor.getTimeseriesFieldCollectionSpecs(), + searchableRefFieldSpecExtractor.getSpecs(), aspectRecordSchema, aspectClass); } diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpec.java b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpec.java new file mode 100644 index 00000000000000..d4093b27cb9391 --- /dev/null +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpec.java @@ -0,0 +1,19 @@ +package com.linkedin.metadata.models; + +import com.linkedin.data.schema.DataSchema; +import com.linkedin.data.schema.PathSpec; +import com.linkedin.metadata.models.annotation.SearchableRefAnnotation; +import lombok.NonNull; +import lombok.Value; + +@Value +public class SearchableRefFieldSpec implements FieldSpec { + + @NonNull PathSpec path; + @NonNull SearchableRefAnnotation searchableRefAnnotation; + @NonNull DataSchema pegasusSchema; + + public boolean isArray() { + return path.getPathComponents().contains("*"); + } +} diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java new file mode 100644 index 00000000000000..1b7de5695b5be8 --- /dev/null +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java @@ -0,0 +1,160 @@ +package com.linkedin.metadata.models; + +import com.linkedin.data.schema.DataSchema; +import com.linkedin.data.schema.DataSchemaTraverse; +import com.linkedin.data.schema.PathSpec; +import com.linkedin.data.schema.annotation.SchemaVisitor; +import com.linkedin.data.schema.annotation.SchemaVisitorTraversalResult; +import com.linkedin.data.schema.annotation.TraverserContext; +import com.linkedin.metadata.models.annotation.SearchableRefAnnotation; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +/** + * Implementation of {@link SchemaVisitor} responsible for extracting {@link SearchableRefFieldSpec} + * from an aspect schema. + */ +@Slf4j +public class SearchableRefFieldSpecExtractor implements SchemaVisitor { + + private final List _specs = new ArrayList<>(); + private final Map _searchRefFieldNamesToPatch = new HashMap<>(); + + public List getSpecs() { + return _specs; + } + + @Override + public void callbackOnContext(TraverserContext context, DataSchemaTraverse.Order order) { + if (context.getEnclosingField() == null) { + return; + } + + if (DataSchemaTraverse.Order.PRE_ORDER.equals(order)) { + + final DataSchema currentSchema = context.getCurrentSchema().getDereferencedDataSchema(); + + final Object annotationObj = getAnnotationObj(context); + + if (annotationObj != null) { + validatePropertiesAnnotation( + currentSchema, annotationObj, context.getTraversePath().toString()); + extractSearchableRefAnnotation(annotationObj, currentSchema, context); + } + } + } + + private Object getAnnotationObj(TraverserContext context) { + final DataSchema currentSchema = context.getCurrentSchema().getDereferencedDataSchema(); + + // First, check properties for primary annotation definition. + final Map properties = context.getEnclosingField().getProperties(); + final Object primaryAnnotationObj = properties.get(SearchableRefAnnotation.ANNOTATION_NAME); + if (primaryAnnotationObj != null) { + validatePropertiesAnnotation( + currentSchema, primaryAnnotationObj, context.getTraversePath().toString()); + } + + // Next, check resolved properties for annotations on primitives. + final Map resolvedProperties = + FieldSpecUtils.getResolvedProperties(currentSchema); + final Object resolvedAnnotationObj = + resolvedProperties.get(SearchableRefAnnotation.ANNOTATION_NAME); + return resolvedAnnotationObj; + } + + private void extractSearchableRefAnnotation( + final Object annotationObj, final DataSchema currentSchema, final TraverserContext context) { + final PathSpec path = new PathSpec(context.getSchemaPathSpec()); + final Optional fullPath = FieldSpecUtils.getPathSpecWithAspectName(context); + SearchableRefAnnotation annotation = + SearchableRefAnnotation.fromPegasusAnnotationObject( + annotationObj, + FieldSpecUtils.getSchemaFieldName(path), + currentSchema.getDereferencedType(), + path.toString()); + String schemaPathSpec = context.getSchemaPathSpec().toString(); + if (_searchRefFieldNamesToPatch.containsKey(annotation.getFieldName()) + && !_searchRefFieldNamesToPatch.get(annotation.getFieldName()).equals(schemaPathSpec)) { + // Try to use path + String pathName = path.toString().replace('/', '_').replace("*", ""); + if (pathName.startsWith("_")) { + pathName = pathName.replaceFirst("_", ""); + } + + if (_searchRefFieldNamesToPatch.containsKey(pathName) + && !_searchRefFieldNamesToPatch.get(pathName).equals(schemaPathSpec)) { + throw new ModelValidationException( + String.format( + "Entity has multiple searchableRef fields with the same field name %s, path: %s", + annotation.getFieldName(), fullPath.orElse(path))); + } else { + annotation = + new SearchableRefAnnotation( + pathName, + annotation.getFieldType(), + annotation.getBoostScore(), + annotation.getDepth(), + annotation.getRefType(), + annotation.getFieldNameAliases()); + } + } + log.debug("SearchableRef annotation for field: {} : {}", schemaPathSpec, annotation); + final SearchableRefFieldSpec fieldSpec = + new SearchableRefFieldSpec(path, annotation, currentSchema); + _specs.add(fieldSpec); + _searchRefFieldNamesToPatch.put( + annotation.getFieldName(), context.getSchemaPathSpec().toString()); + } + + @Override + public VisitorContext getInitialVisitorContext() { + return null; + } + + @Override + public SchemaVisitorTraversalResult getSchemaVisitorTraversalResult() { + return new SchemaVisitorTraversalResult(); + } + + private void validatePropertiesAnnotation( + DataSchema currentSchema, Object annotationObj, String pathStr) { + + // If primitive, assume the annotation is well formed until resolvedProperties reflects it. + if (currentSchema.isPrimitive() + || currentSchema.getDereferencedType().equals(DataSchema.Type.ENUM) + || currentSchema.getDereferencedType().equals(DataSchema.Type.MAP)) { + return; + } + + // Required override case. If the annotation keys are not overrides, they are incorrect. + if (!Map.class.isAssignableFrom(annotationObj.getClass())) { + throw new ModelValidationException( + String.format( + "Failed to validate @%s annotation declared inside %s: Invalid value type provided (Expected Map)", + SearchableRefAnnotation.ANNOTATION_NAME, pathStr)); + } + + Map annotationMap = (Map) annotationObj; + + if (annotationMap.size() == 0) { + throw new ModelValidationException( + String.format( + "Invalid @SearchableRef Annotation at %s. Annotation placed on invalid field of type %s. Must be placed on primitive field.", + pathStr, currentSchema.getType())); + } + + for (String key : annotationMap.keySet()) { + if (!key.startsWith(Character.toString(PathSpec.SEPARATOR))) { + throw new ModelValidationException( + String.format( + "Invalid @SearchableRef Annotation at %s. Annotation placed on invalid field of type %s. Must be placed on primitive field.", + pathStr, currentSchema.getType())); + } + } + } +} diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableRefAnnotation.java b/entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableRefAnnotation.java new file mode 100644 index 00000000000000..db6cf46dfc96f7 --- /dev/null +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/annotation/SearchableRefAnnotation.java @@ -0,0 +1,121 @@ +package com.linkedin.metadata.models.annotation; + +import com.google.common.collect.ImmutableSet; +import com.linkedin.data.schema.DataSchema; +import com.linkedin.metadata.models.ModelValidationException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nonnull; +import lombok.Value; +import org.apache.commons.lang3.EnumUtils; + +/** Simple object representation of the @SearchableRefAnnotation annotation metadata. */ +@Value +public class SearchableRefAnnotation { + + public static final String FIELD_NAME_ALIASES = "fieldNameAliases"; + public static final String ANNOTATION_NAME = "SearchableRef"; + private static final Set DEFAULT_QUERY_FIELD_TYPES = + ImmutableSet.of( + SearchableAnnotation.FieldType.TEXT, + SearchableAnnotation.FieldType.OBJECT, + SearchableAnnotation.FieldType.TEXT_PARTIAL, + SearchableAnnotation.FieldType.WORD_GRAM, + SearchableAnnotation.FieldType.URN, + SearchableAnnotation.FieldType.URN_PARTIAL); + + // Name of the field in the search index. Defaults to the field name in the schema + String fieldName; + // Type of the field. Defines how the field is indexed and matched + SearchableAnnotation.FieldType fieldType; + // Boost multiplier to the match score. Matches on fields with higher boost score ranks higher + double boostScore; + // defines what depth should be explored of reference object + int depth; + // defines entity type of URN + String refType; + // (Optional) Aliases for this given field that can be used for sorting etc. + List fieldNameAliases; + + @Nonnull + public static SearchableRefAnnotation fromPegasusAnnotationObject( + @Nonnull final Object annotationObj, + @Nonnull final String schemaFieldName, + @Nonnull final DataSchema.Type schemaDataType, + @Nonnull final String context) { + if (!Map.class.isAssignableFrom(annotationObj.getClass())) { + throw new ModelValidationException( + String.format( + "Failed to validate @%s annotation declared at %s: Invalid value type provided (Expected Map)", + ANNOTATION_NAME, context)); + } + + Map map = (Map) annotationObj; + final Optional fieldName = AnnotationUtils.getField(map, "fieldName", String.class); + final Optional fieldType = AnnotationUtils.getField(map, "fieldType", String.class); + if (fieldType.isPresent() + && !EnumUtils.isValidEnum(SearchableAnnotation.FieldType.class, fieldType.get())) { + throw new ModelValidationException( + String.format( + "Failed to validate @%s annotation declared at %s: Invalid field 'fieldType'. Invalid fieldType provided. Valid types are %s", + ANNOTATION_NAME, context, Arrays.toString(SearchableAnnotation.FieldType.values()))); + } + final Optional refType = AnnotationUtils.getField(map, "refType", String.class); + if (!refType.isPresent()) { + throw new ModelValidationException( + String.format( + "Failed to validate @%s annotation declared at %s: " + + "Mandatory input field refType defining the Entity Type is not provided", + ANNOTATION_NAME, context)); + } + final Optional depth = AnnotationUtils.getField(map, "depth", Integer.class); + final Optional boostScore = AnnotationUtils.getField(map, "boostScore", Double.class); + final List fieldNameAliases = getFieldNameAliases(map); + final SearchableAnnotation.FieldType resolvedFieldType = + getFieldType(fieldType, schemaDataType); + return new SearchableRefAnnotation( + fieldName.orElse(schemaFieldName), + resolvedFieldType, + boostScore.orElse(1.0), + depth.orElse(2), + refType.get(), + fieldNameAliases); + } + + private static SearchableAnnotation.FieldType getFieldType( + Optional maybeFieldType, DataSchema.Type schemaDataType) { + if (!maybeFieldType.isPresent()) { + return getDefaultFieldType(schemaDataType); + } + return SearchableAnnotation.FieldType.valueOf(maybeFieldType.get()); + } + + private static SearchableAnnotation.FieldType getDefaultFieldType( + DataSchema.Type schemaDataType) { + switch (schemaDataType) { + case INT: + case FLOAT: + return SearchableAnnotation.FieldType.COUNT; + case MAP: + return SearchableAnnotation.FieldType.KEYWORD; + default: + return SearchableAnnotation.FieldType.TEXT; + } + } + + private static List getFieldNameAliases(Map map) { + final List aliases = new ArrayList<>(); + final Optional fieldNameAliases = + AnnotationUtils.getField(map, FIELD_NAME_ALIASES, List.class); + if (fieldNameAliases.isPresent()) { + for (Object alias : fieldNameAliases.get()) { + aliases.add((String) alias); + } + } + return aliases; + } +} diff --git a/entity-registry/src/test/java/com/linkedin/metadata/aspect/validators/StructuredPropertiesValidatorTest.java b/entity-registry/src/test/java/com/linkedin/metadata/aspect/validators/StructuredPropertiesValidatorTest.java index 450b299b48b34f..556fad6fa1f7bf 100644 --- a/entity-registry/src/test/java/com/linkedin/metadata/aspect/validators/StructuredPropertiesValidatorTest.java +++ b/entity-registry/src/test/java/com/linkedin/metadata/aspect/validators/StructuredPropertiesValidatorTest.java @@ -32,6 +32,11 @@ static class MockAspectRetriever implements AspectRetriever { this._propertyDefinition = defToReturn; } + @Override + public boolean exists(@Nonnull Urn urn) throws RemoteInvocationException, URISyntaxException { + return false; + } + @Nonnull @Override public Map> getLatestAspectObjects( diff --git a/entity-registry/src/test/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoaderTest.java b/entity-registry/src/test/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoaderTest.java index 1a64359008dd84..b657bd2c274fd7 100644 --- a/entity-registry/src/test/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoaderTest.java +++ b/entity-registry/src/test/java/com/linkedin/metadata/models/registry/PluginEntityRegistryLoaderTest.java @@ -99,6 +99,7 @@ private EntityRegistry getBaseEntityRegistry() { Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), (RecordDataSchema) DataSchemaFactory.getInstance().getAspectSchema("datasetKey").get(), DataSchemaFactory.getInstance().getAspectClass("datasetKey").get()); diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index 11418caa198576..1badd0d8627b7f 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -1,6 +1,8 @@ package com.linkedin.metadata; import com.linkedin.common.urn.Urn; +import java.util.Arrays; +import java.util.List; /** Static class containing commonly-used constants across DataHub services. */ public class Constants { @@ -364,6 +366,8 @@ public class Constants { public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; + public static final List SKIP_REFERENCE_ASPECT = + Arrays.asList("ownership", "status", "institutionalMemory"); // Posts public static final String POST_INFO_ASPECT_NAME = "postInfo"; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/client/EntityClientAspectRetriever.java b/metadata-io/src/main/java/com/linkedin/metadata/client/EntityClientAspectRetriever.java index 974406c0be0df1..42ae82e0cf43f0 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/client/EntityClientAspectRetriever.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/client/EntityClientAspectRetriever.java @@ -26,6 +26,12 @@ public Aspect getLatestAspectObject(@Nonnull Urn urn, @Nonnull String aspectName return entityClient.getLatestAspectObject(urn, aspectName); } + @Nonnull + @Override + public boolean exists(@Nonnull Urn urn) throws RemoteInvocationException, URISyntaxException { + return entityClient.exists(urn); + } + @Nonnull @Override public Map> getLatestAspectObjects( diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/EntityIndexBuilders.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/EntityIndexBuilders.java index 4322ea90edf1fa..afc831b004ec38 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/EntityIndexBuilders.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/EntityIndexBuilders.java @@ -39,6 +39,7 @@ public void reindexAll() { @Override public List buildReindexConfigs() { Map settings = settingsBuilder.getSettings(); + MappingsBuilder.setEntityRegistry(entityRegistry); return entityRegistry.getEntitySpecs().values().stream() .map( entitySpec -> { @@ -57,6 +58,7 @@ public List buildReindexConfigs() { public List buildReindexConfigsWithAllStructProps( Collection properties) { Map settings = settingsBuilder.getSettings(); + MappingsBuilder.setEntityRegistry(entityRegistry); return entityRegistry.getEntitySpecs().values().stream() .map( entitySpec -> { @@ -81,6 +83,7 @@ public List buildReindexConfigsWithAllStructProps( public List buildReindexConfigsWithNewStructProp( StructuredPropertyDefinition property) { Map settings = settingsBuilder.getSettings(); + MappingsBuilder.setEntityRegistry(entityRegistry); return entityRegistry.getEntitySpecs().values().stream() .map( entitySpec -> { diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java index 79f530f18a3451..15bf6a411af801 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java @@ -7,11 +7,9 @@ import com.google.common.collect.ImmutableMap; import com.linkedin.common.urn.Urn; -import com.linkedin.metadata.models.EntitySpec; -import com.linkedin.metadata.models.LogicalValueType; -import com.linkedin.metadata.models.SearchScoreFieldSpec; -import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.*; import com.linkedin.metadata.models.annotation.SearchableAnnotation.FieldType; +import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.utils.ESUtils; import com.linkedin.structured.StructuredPropertyDefinition; import java.net.URISyntaxException; @@ -53,6 +51,7 @@ public static Map getPartialNgramConfigWithOverrides( public static final String PATH = "path"; public static final String PROPERTIES = "properties"; + private static EntityRegistry entityRegistry; private MappingsBuilder() {} @@ -114,7 +113,14 @@ public static Map getMappings(@Nonnull final EntitySpec entitySp .forEach( searchScoreFieldSpec -> mappings.putAll(getMappingsForSearchScoreField(searchScoreFieldSpec))); - + entitySpec + .getSearchableRefFieldSpecs() + .forEach( + searchableRefFieldSpec -> + mappings.putAll( + getMappingForSearchableRefField( + searchableRefFieldSpec, + searchableRefFieldSpec.getSearchableRefAnnotation().getDepth()))); // Fixed fields mappings.put("urn", getMappingsForUrn()); mappings.put("runId", getMappingsForRunId()); @@ -297,6 +303,36 @@ private static Map getMappingsForSearchScoreField( ImmutableMap.of(TYPE, ESUtils.DOUBLE_FIELD_TYPE)); } + private static Map getMappingForSearchableRefField( + @Nonnull final SearchableRefFieldSpec searchableRefFieldSpec, @Nonnull final int depth) { + Map mappings = new HashMap<>(); + Map mappingForField = new HashMap<>(); + Map mappingForProperty = new HashMap<>(); + if (depth == 0) { + mappings.put( + searchableRefFieldSpec.getSearchableRefAnnotation().getFieldName(), getMappingsForUrn()); + return mappings; + } + String entityType = searchableRefFieldSpec.getSearchableRefAnnotation().getRefType(); + EntitySpec entitySpec = entityRegistry.getEntitySpec(entityType); + entitySpec + .getSearchableFieldSpecs() + .forEach( + searchableFieldSpec -> + mappingForField.putAll(getMappingsForField(searchableFieldSpec))); + entitySpec + .getSearchableRefFieldSpecs() + .forEach( + entitySearchableRefFieldSpec -> + mappingForField.putAll( + getMappingForSearchableRefField(entitySearchableRefFieldSpec, depth - 1))); + mappingForField.put("urn", getMappingsForUrn()); + mappingForProperty.put("properties", mappingForField); + mappings.put( + searchableRefFieldSpec.getSearchableRefAnnotation().getFieldName(), mappingForProperty); + return mappings; + } + private static Map getMappingsForFieldNameAliases( @Nonnull final SearchableFieldSpec searchableFieldSpec) { Map mappings = new HashMap<>(); @@ -311,4 +347,8 @@ private static Map getMappingsForFieldNameAliases( }); return mappings; } + + public static void setEntityRegistry(@Nonnull final EntityRegistry entityRegistryInput) { + entityRegistry = entityRegistryInput; + } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java index 0a9a9fbbad0867..6266eaab0d96b8 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESBrowseDAO.java @@ -544,7 +544,8 @@ private QueryBuilder buildQueryStringV2( EntitySpec entitySpec = entityRegistry.getEntitySpec(entityName); QueryBuilder query = - SearchRequestHandler.getBuilder(entitySpec, searchConfiguration, customSearchConfiguration) + SearchRequestHandler.getBuilder( + entitySpec, entityRegistry, searchConfiguration, customSearchConfiguration) .getQuery(input, false); queryBuilder.must(query); @@ -573,7 +574,8 @@ private QueryBuilder buildQueryStringBrowseAcrossEntities( final BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery(); QueryBuilder query = - SearchRequestHandler.getBuilder(entitySpecs, searchConfiguration, customSearchConfiguration) + SearchRequestHandler.getBuilder( + entitySpecs, entityRegistry, searchConfiguration, customSearchConfiguration) .getQuery(input, false); queryBuilder.must(query); diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java index 76153a8d2adb3f..62cabc8b04e0c9 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/ESSearchDAO.java @@ -105,7 +105,7 @@ private SearchResult executeAndExtract( // extract results, validated against document model as well return transformIndexIntoEntityName( SearchRequestHandler.getBuilder( - entitySpec, searchConfiguration, customSearchConfiguration) + entitySpec, entityRegistry, searchConfiguration, customSearchConfiguration) .extractResult(searchResponse, filter, from, size)); } catch (Exception e) { log.error("Search query failed", e); @@ -189,7 +189,7 @@ private ScrollResult executeAndExtract( // extract results, validated against document model as well return transformIndexIntoEntityName( SearchRequestHandler.getBuilder( - entitySpecs, searchConfiguration, customSearchConfiguration) + entitySpecs, entityRegistry, searchConfiguration, customSearchConfiguration) .extractScrollResult( searchResponse, filter, scrollId, keepAlive, size, supportsPointInTime())); } catch (Exception e) { @@ -230,7 +230,8 @@ public SearchResult search( Filter transformedFilters = transformFilterForEntities(postFilters, indexConvention); // Step 1: construct the query final SearchRequest searchRequest = - SearchRequestHandler.getBuilder(entitySpecs, searchConfiguration, customSearchConfiguration) + SearchRequestHandler.getBuilder( + entitySpecs, entityRegistry, searchConfiguration, customSearchConfiguration) .getSearchRequest( finalInput, transformedFilters, sortCriterion, from, size, searchFlags, facets); searchRequest.indices( @@ -261,7 +262,8 @@ public SearchResult filter( EntitySpec entitySpec = entityRegistry.getEntitySpec(entityName); Filter transformedFilters = transformFilterForEntities(filters, indexConvention); final SearchRequest searchRequest = - SearchRequestHandler.getBuilder(entitySpec, searchConfiguration, customSearchConfiguration) + SearchRequestHandler.getBuilder( + entitySpec, entityRegistry, searchConfiguration, customSearchConfiguration) .getFilterRequest(transformedFilters, sortCriterion, from, size); searchRequest.indices(indexConvention.getIndexName(entitySpec)); @@ -325,7 +327,8 @@ public Map aggregateByValue( entityNames.stream().map(entityRegistry::getEntitySpec).collect(Collectors.toList()); } final SearchRequest searchRequest = - SearchRequestHandler.getBuilder(entitySpecs, searchConfiguration, customSearchConfiguration) + SearchRequestHandler.getBuilder( + entitySpecs, entityRegistry, searchConfiguration, customSearchConfiguration) .getAggregationRequest( field, transformFilterForEntities(requestParams, indexConvention), limit); if (entityNames == null) { @@ -399,7 +402,8 @@ public ScrollResult scroll( Filter transformedFilters = transformFilterForEntities(postFilters, indexConvention); // Step 1: construct the query final SearchRequest searchRequest = - SearchRequestHandler.getBuilder(entitySpecs, searchConfiguration, customSearchConfiguration) + SearchRequestHandler.getBuilder( + entitySpecs, entityRegistry, searchConfiguration, customSearchConfiguration) .getSearchRequest( finalInput, transformedFilters, diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java index 7709ff16f79409..9ffdb8b6002227 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java @@ -1,9 +1,17 @@ package com.linkedin.metadata.search.elasticsearch.query.request; +import static com.linkedin.metadata.Constants.SKIP_REFERENCE_ASPECT; import static com.linkedin.metadata.search.elasticsearch.indexbuilder.SettingsBuilder.*; +import com.linkedin.metadata.models.AspectSpec; +import com.linkedin.metadata.models.EntitySpec; import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.SearchableRefFieldSpec; import com.linkedin.metadata.models.annotation.SearchableAnnotation; +import com.linkedin.metadata.models.annotation.SearchableRefAnnotation; +import com.linkedin.metadata.models.registry.EntityRegistry; +import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.annotation.Nonnull; import lombok.Builder; @@ -20,6 +28,7 @@ public class SearchFieldConfig { public static final Set KEYWORD_FIELDS = Set.of("urn", "runId", "_index"); public static final Set PATH_HIERARCHY_FIELDS = Set.of("browsePathV2"); + public static final float URN_BOOST_SCORE = 10.0f; // These should not be used directly since there is a specific // order in which these rules need to be evaluated for exceptions to @@ -100,6 +109,75 @@ public static SearchFieldConfig detectSubFieldType( .build(); } + public static Set detectSubFieldType( + @Nonnull SearchableRefFieldSpec fieldSpec, int depth, EntityRegistry entityRegistry) { + Set fieldConfigs = new HashSet<>(); + final SearchableRefAnnotation searchableRefAnnotation = fieldSpec.getSearchableRefAnnotation(); + String fieldName = searchableRefAnnotation.getFieldName(); + final float boost = (float) searchableRefAnnotation.getBoostScore(); + fieldConfigs.addAll(detectSubFieldType(fieldSpec, depth, entityRegistry, boost, "")); + return fieldConfigs; + } + + public static Set detectSubFieldType( + @Nonnull SearchableRefFieldSpec refFieldSpec, + int depth, + EntityRegistry entityRegistry, + float boostScore, + String prefixFieldName) { + Set fieldConfigs = new HashSet<>(); + final SearchableRefAnnotation searchableRefAnnotation = + refFieldSpec.getSearchableRefAnnotation(); + EntitySpec refEntitySpec = entityRegistry.getEntitySpec(searchableRefAnnotation.getRefType()); + String fieldName = searchableRefAnnotation.getFieldName(); + final SearchableAnnotation.FieldType fieldType = searchableRefAnnotation.getFieldType(); + if (!prefixFieldName.isEmpty()) { + fieldName = prefixFieldName + "." + fieldName; + } + + if (depth == 0) { + // at depth 0 if URN is present then query by default should be true + fieldConfigs.add(detectSubFieldType(fieldName, boostScore, fieldType, true)); + return fieldConfigs; + } + + String urnFieldName = fieldName + ".urn"; + fieldConfigs.add( + detectSubFieldType(urnFieldName, boostScore, SearchableAnnotation.FieldType.URN, true)); + + List aspectSpecs = refEntitySpec.getAspectSpecs(); + for (AspectSpec aspectSpec : aspectSpecs) { + if (!SKIP_REFERENCE_ASPECT.contains(aspectSpec.getName())) { + for (SearchableFieldSpec searchableFieldSpec : aspectSpec.getSearchableFieldSpecs()) { + String refFieldName = searchableFieldSpec.getSearchableAnnotation().getFieldName(); + refFieldName = fieldName + "." + refFieldName; + + final SearchableAnnotation searchableAnnotation = + searchableFieldSpec.getSearchableAnnotation(); + final float refBoost = (float) searchableAnnotation.getBoostScore() * boostScore; + final SearchableAnnotation.FieldType refFieldType = searchableAnnotation.getFieldType(); + fieldConfigs.add( + detectSubFieldType( + refFieldName, refBoost, refFieldType, searchableAnnotation.isQueryByDefault())); + } + + for (SearchableRefFieldSpec searchableRefFieldSpec : + aspectSpec.getSearchableRefFieldSpecs()) { + String refFieldName = searchableRefFieldSpec.getSearchableRefAnnotation().getFieldName(); + refFieldName = fieldName + "." + refFieldName; + final float refBoost = + (float) searchableRefFieldSpec.getSearchableRefAnnotation().getBoostScore() + * boostScore; + fieldConfigs.addAll( + detectSubFieldType( + searchableRefFieldSpec, depth - 1, entityRegistry, refBoost, refFieldName)); + } + } + } + + return fieldConfigs; + } + public boolean isKeyword() { return KEYWORD_ANALYZER.equals(analyzer()) || isKeyword(fieldName()); } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java index 7ddccb0d56724c..9c10ae9c97c5a8 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java @@ -19,8 +19,10 @@ import com.linkedin.metadata.models.EntitySpec; import com.linkedin.metadata.models.SearchScoreFieldSpec; import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.SearchableRefFieldSpec; import com.linkedin.metadata.models.annotation.SearchScoreAnnotation; import com.linkedin.metadata.models.annotation.SearchableAnnotation; +import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.utils.ESUtils; import java.io.IOException; import java.util.ArrayList; @@ -86,6 +88,7 @@ public class SearchQueryBuilder { private final WordGramConfiguration wordGramConfiguration; private final CustomizedQueryHandler customizedQueryHandler; + private EntityRegistry entityRegistry; public SearchQueryBuilder( @Nonnull SearchConfiguration searchConfiguration, @@ -96,6 +99,10 @@ public SearchQueryBuilder( this.customizedQueryHandler = CustomizedQueryHandler.builder(customSearchConfiguration).build(); } + public void setEntityRegistry(EntityRegistry entityRegistry) { + this.entityRegistry = entityRegistry; + } + public QueryBuilder buildQuery( @Nonnull List entitySpecs, @Nonnull String query, boolean fulltext) { QueryConfiguration customQueryConfig = @@ -257,6 +264,14 @@ public Set getFieldsFromEntitySpec(EntitySpec entitySpec) { } } } + + List searchableRefFieldSpecs = entitySpec.getSearchableRefFieldSpecs(); + for (SearchableRefFieldSpec refFieldSpec : searchableRefFieldSpecs) { + int depth = refFieldSpec.getSearchableRefAnnotation().getDepth(); + Set searchFieldConfig = + SearchFieldConfig.detectSubFieldType(refFieldSpec, depth, entityRegistry); + fields.addAll(searchFieldConfig); + } return fields; } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java index 3ac05ed122cd70..534dca8dc00f5f 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchRequestHandler.java @@ -14,6 +14,7 @@ import com.linkedin.metadata.models.EntitySpec; import com.linkedin.metadata.models.SearchableFieldSpec; import com.linkedin.metadata.models.annotation.SearchableAnnotation; +import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.query.filter.SortCriterion; @@ -73,6 +74,7 @@ public class SearchRequestHandler { private static final Map, SearchRequestHandler> REQUEST_HANDLER_BY_ENTITY_NAME = new ConcurrentHashMap<>(); private final List _entitySpecs; + private final EntityRegistry _entityRegistry; private final Set _defaultQueryFieldNames; private final HighlightBuilder _highlights; @@ -83,16 +85,19 @@ public class SearchRequestHandler { private SearchRequestHandler( @Nonnull EntitySpec entitySpec, + @Nonnull EntityRegistry entityRegistry, @Nonnull SearchConfiguration configs, @Nullable CustomSearchConfiguration customSearchConfiguration) { - this(ImmutableList.of(entitySpec), configs, customSearchConfiguration); + this(ImmutableList.of(entitySpec), entityRegistry, configs, customSearchConfiguration); } private SearchRequestHandler( @Nonnull List entitySpecs, + @Nonnull EntityRegistry entityRegistry, @Nonnull SearchConfiguration configs, @Nullable CustomSearchConfiguration customSearchConfiguration) { _entitySpecs = entitySpecs; + _entityRegistry = entityRegistry; Map> entitySearchAnnotations = getSearchableAnnotations(); List annotations = @@ -102,6 +107,7 @@ private SearchRequestHandler( _defaultQueryFieldNames = getDefaultQueryFieldNames(annotations); _highlights = getHighlights(); _searchQueryBuilder = new SearchQueryBuilder(configs, customSearchConfiguration); + _searchQueryBuilder.setEntityRegistry(entityRegistry); _aggregationQueryBuilder = new AggregationQueryBuilder(configs, entitySearchAnnotations); _configs = configs; searchableFieldTypes = @@ -119,20 +125,26 @@ private SearchRequestHandler( public static SearchRequestHandler getBuilder( @Nonnull EntitySpec entitySpec, + @Nonnull EntityRegistry entityRegistry, @Nonnull SearchConfiguration configs, @Nullable CustomSearchConfiguration customSearchConfiguration) { return REQUEST_HANDLER_BY_ENTITY_NAME.computeIfAbsent( ImmutableList.of(entitySpec), - k -> new SearchRequestHandler(entitySpec, configs, customSearchConfiguration)); + k -> + new SearchRequestHandler( + entitySpec, entityRegistry, configs, customSearchConfiguration)); } public static SearchRequestHandler getBuilder( @Nonnull List entitySpecs, + @Nonnull EntityRegistry entityRegistry, @Nonnull SearchConfiguration configs, @Nullable CustomSearchConfiguration customSearchConfiguration) { return REQUEST_HANDLER_BY_ENTITY_NAME.computeIfAbsent( ImmutableList.copyOf(entitySpecs), - k -> new SearchRequestHandler(entitySpecs, configs, customSearchConfiguration)); + k -> + new SearchRequestHandler( + entitySpecs, entityRegistry, configs, customSearchConfiguration)); } private Map> getSearchableAnnotations() { diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java index d52a80d685fd5b..c47fa13f86b8c1 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java @@ -3,23 +3,29 @@ import static com.linkedin.metadata.Constants.*; import static com.linkedin.metadata.models.StructuredPropertyUtils.sanitizeStructuredPropertyFQN; +import com.datahub.util.RecordUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.linkedin.common.urn.Urn; +import com.linkedin.data.DataMap; import com.linkedin.data.schema.DataSchema; import com.linkedin.data.template.RecordTemplate; import com.linkedin.entity.Aspect; +import com.linkedin.metadata.Constants; import com.linkedin.metadata.aspect.plugins.validation.AspectRetriever; import com.linkedin.metadata.aspect.validation.StructuredPropertiesValidator; +import com.linkedin.metadata.entity.EntityUtils; import com.linkedin.metadata.models.AspectSpec; import com.linkedin.metadata.models.EntitySpec; import com.linkedin.metadata.models.LogicalValueType; import com.linkedin.metadata.models.SearchScoreFieldSpec; import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.SearchableRefFieldSpec; import com.linkedin.metadata.models.annotation.SearchableAnnotation.FieldType; import com.linkedin.metadata.models.extractor.FieldExtractor; +import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.r2.RemoteInvocationException; import com.linkedin.structured.StructuredProperties; import com.linkedin.structured.StructuredPropertyDefinition; @@ -93,6 +99,9 @@ public Optional transformAspect( throws RemoteInvocationException, URISyntaxException { final Map> extractedSearchableFields = FieldExtractor.extractFields(aspect, aspectSpec.getSearchableFieldSpecs(), maxValueLength); + final Map> extractedSearchRefFields = + FieldExtractor.extractFields( + aspect, aspectSpec.getSearchableRefFieldSpecs(), maxValueLength); final Map> extractedSearchScoreFields = FieldExtractor.extractFields(aspect, aspectSpec.getSearchScoreFieldSpecs(), maxValueLength); @@ -103,6 +112,8 @@ public Optional transformAspect( searchDocument.put("urn", urn.toString()); extractedSearchableFields.forEach( (key, values) -> setSearchableValue(key, values, searchDocument, forDelete)); + extractedSearchRefFields.forEach( + (key, values) -> setSearchableRefValue(key, values, searchDocument, forDelete)); extractedSearchScoreFields.forEach( (key, values) -> setSearchScoreValue(key, values, searchDocument, forDelete)); result = Optional.of(searchDocument.toString()); @@ -385,4 +396,126 @@ private void setStructuredPropertiesSearchValue( } }); } + + public void setSearchableRefValue( + final SearchableRefFieldSpec searchableRefFieldSpec, + final List fieldValues, + final ObjectNode searchDocument, + final Boolean forDelete) { + String fieldName = searchableRefFieldSpec.getSearchableRefAnnotation().getFieldName(); + FieldType fieldType = searchableRefFieldSpec.getSearchableRefAnnotation().getFieldType(); + boolean isArray = searchableRefFieldSpec.isArray(); + + if (forDelete) { + searchDocument.set(fieldName, JsonNodeFactory.instance.nullNode()); + return; + } + int depth = searchableRefFieldSpec.getSearchableRefAnnotation().getDepth(); + if (isArray) { + ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode(); + fieldValues + .subList(0, Math.min(fieldValues.size(), maxArrayLength)) + .forEach(value -> getNodeForRef(depth, value, fieldType).ifPresent(arrayNode::add)); + searchDocument.set(fieldName, arrayNode); + } else if (!fieldValues.isEmpty()) { + String finalFieldName = fieldName; + getNodeForRef(depth, fieldValues.get(0), fieldType) + .ifPresent(node -> searchDocument.set(finalFieldName, node)); + } + } + + private Optional getNodeForRef( + final int depth, final Object fieldValue, final FieldType fieldType) { + EntityRegistry entityRegistry = aspectRetriever.getEntityRegistry(); + if (depth == 0) { + if (fieldValue.toString().isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(JsonNodeFactory.instance.textNode(fieldValue.toString())); + } + } + if (fieldType == FieldType.URN) { + ObjectNode resultNode = JsonNodeFactory.instance.objectNode(); + try { + Urn eAUrn = EntityUtils.getUrnFromString(fieldValue.toString()); + if (!aspectRetriever.exists(eAUrn)) { + return Optional.ofNullable(JsonNodeFactory.instance.nullNode()); + } + resultNode.set("urn", JsonNodeFactory.instance.textNode(fieldValue.toString())); + String entityType = eAUrn.getEntityType(); + EntitySpec entitySpec = entityRegistry.getEntitySpec(entityType); + for (Map.Entry mapEntry : entitySpec.getAspectSpecMap().entrySet()) { + String aspectName = mapEntry.getKey(); + AspectSpec aspectSpec = mapEntry.getValue(); + String aspectClass = aspectSpec.getDataTemplateClass().getCanonicalName(); + if (!Constants.SKIP_REFERENCE_ASPECT.contains(aspectName)) { + try { + Aspect aspectDetails = aspectRetriever.getLatestAspectObject(eAUrn, aspectName); + DataMap aspectDataMap = aspectDetails.data(); + RecordTemplate aspectRecord = + RecordUtils.toRecordTemplate(aspectClass, aspectDataMap); + // Extract searchable fields and create node using getNodeForSearchable + final Map> extractedSearchableFields = + FieldExtractor.extractFields( + aspectRecord, aspectSpec.getSearchableFieldSpecs(), maxValueLength); + for (Map.Entry> entry : + extractedSearchableFields.entrySet()) { + SearchableFieldSpec spec = entry.getKey(); + List value = entry.getValue(); + if (!value.isEmpty()) { + setSearchableValue(spec, value, resultNode, false); + } + } + + // Extract searchable ref fields and create node using getNodeForRef + final Map> extractedSearchableRefFields = + FieldExtractor.extractFields( + aspectDetails, aspectSpec.getSearchableRefFieldSpecs(), maxValueLength); + for (Map.Entry> entry : + extractedSearchableRefFields.entrySet()) { + SearchableRefFieldSpec spec = entry.getKey(); + List value = entry.getValue(); + String fieldName = spec.getSearchableRefAnnotation().getFieldName(); + boolean isArray = spec.isArray(); + if (!value.isEmpty()) { + if (isArray) { + ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode(); + value + .subList(0, Math.min(value.size(), maxArrayLength)) + .forEach( + val -> + getNodeForRef( + depth - 1, + val, + spec.getSearchableRefAnnotation().getFieldType()) + .ifPresent(arrayNode::add)); + resultNode.set(fieldName, arrayNode); + } else { + Optional node = + getNodeForRef( + depth - 1, + value.get(0), + spec.getSearchableRefAnnotation().getFieldType()); + if (node.isPresent()) { + resultNode.set(fieldName, node.get()); + } + } + } + } + } catch (RemoteInvocationException e) { + log.error( + "Error while fetching aspect details of {} for urn {} : {}", + aspectName, + eAUrn, + e.getMessage()); + } + } + } + return Optional.of(resultNode); + } catch (Exception e) { + log.error("Error while processing ref field of urn {} : {}", fieldValue, e.getMessage()); + } + } + return Optional.empty(); + } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index 72f0149df23f23..b01239d79ae437 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -108,7 +108,8 @@ public class ESUtils { put("description", ImmutableList.of("description", "editedDescription")); put( "businessAttribute", - ImmutableList.of("editedFieldBusinessAttribute", "businessAttribute")); + ImmutableList.of( + "editedFieldBusinessAttributeRef", "editedFieldBusinessAttributeRef.urn")); } }; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java index 38d630bc302f4e..ba13d412447952 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchQueryBuilderTest.java @@ -35,6 +35,7 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.PostConstruct; import org.mockito.Mockito; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.MatchAllQueryBuilder; @@ -86,6 +87,12 @@ public class SearchQueryBuilderTest extends AbstractTestNGSpringContextTests { public static final SearchQueryBuilder TEST_BUILDER = new SearchQueryBuilder(testQueryConfig, null); + @PostConstruct + public void setup() { + TEST_BUILDER.setEntityRegistry(entityRegistry); + TEST_CUSTOM_BUILDER.setEntityRegistry(entityRegistry); + } + @Test public void testQueryBuilderFulltext() { FunctionScoreQueryBuilder result = diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java index 02c9ea800f0af3..113196c3d38c78 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/query/request/SearchRequestHandlerTest.java @@ -83,7 +83,7 @@ public class SearchRequestHandlerTest extends AbstractTestNGSpringContextTests { public void testDatasetFieldsAndHighlights() { EntitySpec entitySpec = entityRegistry.getEntitySpec("dataset"); SearchRequestHandler datasetHandler = - SearchRequestHandler.getBuilder(entitySpec, testQueryConfig, null); + SearchRequestHandler.getBuilder(entitySpec, entityRegistry, testQueryConfig, null); /* Ensure efficient query performance, we do not expect upstream/downstream/fineGrained lineage @@ -102,7 +102,8 @@ public void testDatasetFieldsAndHighlights() { @Test public void testSearchRequestHandlerHighlightingTurnedOff() { SearchRequestHandler requestHandler = - SearchRequestHandler.getBuilder(TestEntitySpecBuilder.getSpec(), testQueryConfig, null); + SearchRequestHandler.getBuilder( + TestEntitySpecBuilder.getSpec(), entityRegistry, testQueryConfig, null); SearchRequest searchRequest = requestHandler.getSearchRequest( "testQuery", @@ -141,7 +142,8 @@ public void testSearchRequestHandlerHighlightingTurnedOff() { @Test public void testSearchRequestHandler() { SearchRequestHandler requestHandler = - SearchRequestHandler.getBuilder(TestEntitySpecBuilder.getSpec(), testQueryConfig, null); + SearchRequestHandler.getBuilder( + TestEntitySpecBuilder.getSpec(), entityRegistry, testQueryConfig, null); SearchRequest searchRequest = requestHandler.getSearchRequest( "testQuery", null, null, 0, 10, new SearchFlags().setFulltext(false), null); @@ -196,7 +198,8 @@ public void testSearchRequestHandler() { @Test public void testAggregationsInSearch() { SearchRequestHandler requestHandler = - SearchRequestHandler.getBuilder(TestEntitySpecBuilder.getSpec(), testQueryConfig, null); + SearchRequestHandler.getBuilder( + TestEntitySpecBuilder.getSpec(), entityRegistry, testQueryConfig, null); final String nestedAggString = String.format("_entityType%stextFieldOverride", AGGREGATION_SEPARATOR_CHAR); SearchRequest searchRequest = @@ -264,7 +267,8 @@ public void testAggregationsInSearch() { public void testFilteredSearch() { final SearchRequestHandler requestHandler = - SearchRequestHandler.getBuilder(TestEntitySpecBuilder.getSpec(), testQueryConfig, null); + SearchRequestHandler.getBuilder( + TestEntitySpecBuilder.getSpec(), entityRegistry, testQueryConfig, null); final BoolQueryBuilder testQuery = constructFilterQuery(requestHandler, false); @@ -637,7 +641,8 @@ private BoolQueryBuilder getQuery(final Criterion filterCriterion) { .setAnd(new CriterionArray(ImmutableList.of(filterCriterion))))); final SearchRequestHandler requestHandler = - SearchRequestHandler.getBuilder(TestEntitySpecBuilder.getSpec(), testQueryConfig, null); + SearchRequestHandler.getBuilder( + TestEntitySpecBuilder.getSpec(), entityRegistry, testQueryConfig, null); return (BoolQueryBuilder) requestHandler diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl index 5d8916fcaf7b5e..4b0cf938004842 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl @@ -15,11 +15,12 @@ record EditableSchemaFieldInfo includes EditableSchemaFieldBase { "entityTypes": [ "businessAttribute" ] } } - @Searchable = { + @SearchableRef = { "/destinationUrn": { - "fieldName": "editedFieldBusinessAttribute", + "fieldName": "editedFieldBusinessAttributeRef", "fieldType": "URN", - "boostScore": 0.5 + "boostScore": 0.5, + "refType": "businessAttribute" } } businessAttribute: optional BusinessAttributeAssociation diff --git a/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java b/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java index c5539b001e9e35..9656c7d2f60efc 100644 --- a/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java +++ b/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataTypesStepTest.java @@ -60,7 +60,7 @@ public void testExecuteInvalidJson() throws Exception { Assert.assertThrows(RuntimeException.class, step::execute); - verify(entityService, times(1)).exists(any()); + verify(entityService, times(1)).exists(any(Collection.class)); // Verify no additional interactions verifyNoMoreInteractions(entityService); diff --git a/metadata-service/services/src/main/java/com/linkedin/metadata/entity/EntityService.java b/metadata-service/services/src/main/java/com/linkedin/metadata/entity/EntityService.java index d9b0f4b73d5805..57d2082273e80a 100644 --- a/metadata-service/services/src/main/java/com/linkedin/metadata/entity/EntityService.java +++ b/metadata-service/services/src/main/java/com/linkedin/metadata/entity/EntityService.java @@ -303,6 +303,10 @@ default Set exists(@Nonnull final Collection urns) { return exists(urns, true); } + default boolean exists(@Nonnull Urn urn) { + return exists(urn, true); + } + default boolean exists(@Nonnull Urn urn, boolean includeSoftDelete) { return exists(List.of(urn), includeSoftDelete).contains(urn); } diff --git a/metadata-service/servlet/src/main/java/com/datahub/gms/servlet/ConfigSearchExport.java b/metadata-service/servlet/src/main/java/com/datahub/gms/servlet/ConfigSearchExport.java index 27aa9ee04cc756..6e450f2b480b91 100644 --- a/metadata-service/servlet/src/main/java/com/datahub/gms/servlet/ConfigSearchExport.java +++ b/metadata-service/servlet/src/main/java/com/datahub/gms/servlet/ConfigSearchExport.java @@ -79,7 +79,8 @@ private void writeSearchCsv(WebApplicationContext ctx, PrintWriter pw) { entitySpecOpt -> { EntitySpec entitySpec = entitySpecOpt.get(); SearchRequest searchRequest = - SearchRequestHandler.getBuilder(entitySpec, searchConfiguration, null) + SearchRequestHandler.getBuilder( + entitySpec, entityRegistry, searchConfiguration, null) .getSearchRequest( "*", null, diff --git a/mock-entity-registry/src/main/java/mock/MockAspectSpec.java b/mock-entity-registry/src/main/java/mock/MockAspectSpec.java index 92321cce3d9053..8be6f83832abcd 100644 --- a/mock-entity-registry/src/main/java/mock/MockAspectSpec.java +++ b/mock-entity-registry/src/main/java/mock/MockAspectSpec.java @@ -6,6 +6,7 @@ import com.linkedin.metadata.models.RelationshipFieldSpec; import com.linkedin.metadata.models.SearchScoreFieldSpec; import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.SearchableRefFieldSpec; import com.linkedin.metadata.models.TimeseriesFieldCollectionSpec; import com.linkedin.metadata.models.TimeseriesFieldSpec; import com.linkedin.metadata.models.annotation.AspectAnnotation; @@ -20,6 +21,7 @@ public MockAspectSpec( @Nonnull List relationshipFieldSpecs, @Nonnull List timeseriesFieldSpecs, @Nonnull List timeseriesFieldCollectionSpecs, + @Nonnull final List searchableRefFieldSpecs, RecordDataSchema schema, Class aspectClass) { super( @@ -29,6 +31,7 @@ public MockAspectSpec( relationshipFieldSpecs, timeseriesFieldSpecs, timeseriesFieldCollectionSpecs, + searchableRefFieldSpecs, schema, aspectClass); } diff --git a/mock-entity-registry/src/main/java/mock/MockEntitySpec.java b/mock-entity-registry/src/main/java/mock/MockEntitySpec.java index 0013d6615a71d4..f34faea89a870d 100644 --- a/mock-entity-registry/src/main/java/mock/MockEntitySpec.java +++ b/mock-entity-registry/src/main/java/mock/MockEntitySpec.java @@ -89,6 +89,7 @@ public AspectSpec createAspectSpec(T type, String nam Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), type.schema(), (Class) type.getClass().asSubclass(RecordTemplate.class)); } From 07b9494abe5e03a271c6f59ec07baa4db0a00da2 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Thu, 15 Feb 2024 16:55:28 +0530 Subject: [PATCH 25/50] business-attribute: update documentation --- docs/businessattributes.md | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/businessattributes.md b/docs/businessattributes.md index 9561b3f9589bac..e6debe9a88d1ed 100644 --- a/docs/businessattributes.md +++ b/docs/businessattributes.md @@ -2,14 +2,27 @@ ## What are Business Attributes -A Business attribute is a centrally managed logical field that represents a unique schema field entity. This common construct is global in nature, i.e. it is not bound to a project or application implementation. Instead, its identity exists in representing the same field across various datasets owned by various different projects and applications. Projects or applications use the Business attribute to model a column in a dataset and inherit information about it such as definitions, data type, data quality rules/assertions, tags, glossary terms etc from the global definition. +A Business Attribute, as its name implies, is an attribute with a business focus. It embodies the traits or properties of an entity within a business framework. This attribute is a crucial piece of data for a business, utilised to define or control the entity throughout the organisation. If a business process or concept is depicted as a comprehensive logical model, then each Business Attribute can be considered as an individual component within that model. While business names and descriptions are generally managed through glossary terms, Business Attributes encompass additional characteristics such as data quality rules/assertions, data privacy markers, data usage protocols, standard tags, and supplementary documentation, alongside Names and Descriptions. + +For instance, "United States - Social Security Number" comes with a Name and definition. However, it also includes an abbreviation, a Personal Identifiable Information (PII) classification tag, a set of data rules, and possibly some additional references. ## Benefits of Business Attributes -Data architects can use the concept of the business attribute to validate whether applications are conformant with the applicable metadata defined for the business attribute. By abstracting common business metadata into a logical model, different personas with appropriate business knowledge can define pertinent details, like rich definition, business use for the attribute, classification (i.e. PII, sensitive, shareable etc.), specific data rules that govern the attribute, connection to glossary terms. +The principle of "Define Once; Use in Many Places" applies to Business Attributes. Information Stewards can establish these attributes once with all their associated characteristics in an enterprise environment. Subsequently, individual applications or data owners can link their dataset attributes with these Business Attributes. This process allows the complete metadata structure built for a Business Attribute to be inherited. Application owners can also use these attributes to check if their applications align with the organisation-wide standard descriptions and data policies. This approach aids in centralised management for enhanced control and enforcement of metadata standards. + +This standardised metadata can be employed to facilitate data quality, data governance, and data discovery use cases within the organisation. + +A collection of 'related' Business Attributes can create a logical business model. With Business Attributes users have the ability to search associated datasets using business description/tags/glossary attached to business attribute ## How can you use Business Attributes -Business attributes can be used to define a common set of metadata for a logical field that is used across multiple datasets. This metadata can be used to drive data quality, data governance, and data discovery. For example, a business attribute can be used to define a common set of data quality rules that are applicable to a logical field across multiple datasets. This can be used to ensure that the same data quality rules are applied consistently across all datasets that use the logical field. +Business Attributes can be utilised in any of the following scenario: +Attributes that are frequently used across multiple domains, data products, projects, and applications. +Attributes requiring standardisation and inheritance of their characteristics, including name and descriptions, to be propagated. +Attributes that need centralised management for improved control and standard enforcement. + +A Business Attribute could be used to accelerate and standardise business definition management at entity / fields a field across various datasets. This ensures consistent application of the characteristics across all datasets using the Business Attribute. Any change in the them requires a change at only one place (i.e., business attributes) and change can then be inherited across all the application & datasets in the organisation + +Taking the example of "United States- Social Security Number", if an application or data owner has multiple instances of the social security number within their datasets, they can link all these dataset attributes with a Business Attribute to inherit all the aforementioned characteristics. Additionally, users can search for associated datasets using the business description, tags, or glossary linked to the Business Attribute. ## Business Attributes Setup, Prerequisites, and Permissions What you need to create/update and associate business attributes to dataset schema field @@ -24,16 +37,16 @@ As of now Business Attributes can only be created through UI To create a Business Attribute, first navigate to the Business Attributes tab on the home page.

- + Then click on '+ Create Business Attribute'. -This will open a new modal where you can configure the settings for your business attribute. Inside the form, you can choose a name for your Business Attribute. Most often, this will align with the logical purpose of the Business Attribute, -for example 'Customer ID'. You can also add documentation for your Business Attribute to help other users easily discover it. This can be changed later. +This will open a new modal where you can configure the settings for your business attribute. Inside the form, you can choose a name for Business Attribute. Most often, this will align with the logical purpose of the Business Attribute, +for example 'Social Security Number'. You can also add documentation for your Business Attribute to help other users easily discover it. This can be changed later. -We can also add Datatype for Business Attribute. It has String as a default value. +We can also add datatype for Business Attribute. It has String as a default value.

- + Once you've chosen a name and a description, click 'Create' to create the new Business Attribute. @@ -45,14 +58,14 @@ You can associate the business attribute to a dataset schema field using the Dat On a Dataset's schema page, click the 'Add Attribute' to add business attribute to the dataset schema field.

- + After association, dataset schema field gets its description, tags and glossary inherited from Business attribute. -Description inherited from business attribute is greyed out to differentiate between original description of schema field. +Description inherited from business attribute is greyed out to differentiate between original description of schema field. Similarly, tags and glossary terms inherited can't be removed directly.

- + ### What updates are planned for the Business Attributes feature? From 6c05227a971d9538d0f6ea00e18c9eb3e1125729 Mon Sep 17 00:00:00 2001 From: "Bharti, Aakash" Date: Fri, 1 Mar 2024 11:51:38 +0530 Subject: [PATCH 26/50] business-attributes-propagation-tests --- .../hook/BusinessAttributeUpdateHookTest.java | 298 ++++++++++++++++++ .../event/hook/EntityRegistryTestUtil.java | 22 ++ .../test/resources/test-entity-registry.yml | 7 + 3 files changed, 327 insertions(+) create mode 100644 metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java create mode 100644 metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/EntityRegistryTestUtil.java create mode 100644 metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml diff --git a/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java new file mode 100644 index 00000000000000..54b4c3eb3c2adf --- /dev/null +++ b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java @@ -0,0 +1,298 @@ +package com.datahub.event.hook; + +import static com.datahub.event.hook.EntityRegistryTestUtil.ENTITY_REGISTRY; +import static com.linkedin.metadata.search.utils.QueryUtils.EMPTY_FILTER; +import static com.linkedin.metadata.search.utils.QueryUtils.newFilter; +import static com.linkedin.metadata.search.utils.QueryUtils.newRelationshipFilter; +import static org.testng.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.GlobalTags; +import com.linkedin.common.TagAssociation; +import com.linkedin.common.TagAssociationArray; +import com.linkedin.common.urn.TagUrn; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.DataMap; +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.SystemRestliEntityClient; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.graph.GraphService; +import com.linkedin.metadata.graph.RelatedEntitiesResult; +import com.linkedin.metadata.graph.RelatedEntity; +import com.linkedin.metadata.models.AspectSpec; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.metadata.service.BusinessAttributeUpdateService; +import com.linkedin.metadata.timeline.data.ChangeCategory; +import com.linkedin.metadata.timeline.data.ChangeOperation; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.PlatformEvent; +import com.linkedin.mxe.PlatformEventHeader; +import com.linkedin.platform.event.v1.EntityChangeEvent; +import com.linkedin.platform.event.v1.Parameters; +import com.linkedin.schema.EditableSchemaFieldInfo; +import com.linkedin.schema.EditableSchemaFieldInfoArray; +import com.linkedin.schema.EditableSchemaMetadata; +import com.linkedin.util.Pair; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.Future; +import org.mockito.Mockito; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class BusinessAttributeUpdateHookTest { + + private static final String TEST_BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:12668aea-009b-400e-8408-e661c3a230dd"; + private static final String EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE = + "EditableSchemaFieldWithBusinessAttribute"; + private static final Urn datasetUrn = UrnUtils.toDatasetUrn("hive", "test", "DEV"); + private static final String SUB_RESOURCE = "name"; + private static final String TAG_NAME = "test"; + private static final long EVENT_TIME = 123L; + private static final String TEST_ACTOR_URN = "urn:li:corpuser:test"; + private static final String IsPartOfRelationship = "IsPartOf"; + private static Urn actorUrn; + + private static SystemRestliEntityClient _mockClient; + + private GraphService _mockGraphService; + private EntityService _mockEntityService; + private BusinessAttributeUpdateHook _businessAttributeUpdateHook; + private BusinessAttributeUpdateService _businessAttributeServiceHook; + + @BeforeMethod + public void setupTest() throws URISyntaxException { + _mockGraphService = Mockito.mock(GraphService.class); + _mockEntityService = Mockito.mock(EntityService.class); + actorUrn = Urn.createFromString(TEST_ACTOR_URN); + _mockClient = Mockito.mock(SystemRestliEntityClient.class); + _businessAttributeServiceHook = + new BusinessAttributeUpdateService(_mockGraphService, _mockEntityService, ENTITY_REGISTRY); + _businessAttributeUpdateHook = new BusinessAttributeUpdateHook(_businessAttributeServiceHook); + } + + @Test + public void testMCLOnBusinessAttributeUpdate() throws Exception { + PlatformEvent platformEvent = createPlatformEventBusinessAttribute(); + final RelatedEntitiesResult mockRelatedEntities = + new RelatedEntitiesResult( + 0, + 1, + 1, + ImmutableList.of(new RelatedEntity(IsPartOfRelationship, datasetUrn.toString()))); + // mock response + Mockito.when( + _mockGraphService.findRelatedEntities( + null, + newFilter("urn", TEST_BUSINESS_ATTRIBUTE_URN), + null, + EMPTY_FILTER, + Arrays.asList(EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE), + newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING), + 0, + 100000)) + .thenReturn(mockRelatedEntities); + assertEquals(mockRelatedEntities.getTotal(), 1); + + // mock response + Map datasetEntityResponse = datasetEntityResponses(); + Mockito.when( + _mockEntityService.getEntitiesV2( + Constants.DATASET_ENTITY_NAME, + new HashSet<>(Collections.singleton(datasetUrn)), + Collections.singleton(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME))) + .thenReturn(datasetEntityResponse); + assertEquals(datasetEntityResponse.size(), 1); + + // mock response + Mockito.when( + _mockEntityService.alwaysProduceMCLAsync( + Mockito.any(Urn.class), + Mockito.anyString(), + Mockito.anyString(), + Mockito.any(AspectSpec.class), + Mockito.eq(null), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(ChangeType.class))) + .thenReturn(Pair.of(Mockito.mock(Future.class), false)); + + // invoke + _businessAttributeServiceHook.handleChangeEvent(platformEvent); + + // verify + Mockito.verify(_mockGraphService, Mockito.times(1)) + .findRelatedEntities( + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.anyInt(), + Mockito.anyInt()); + } + + @Test + private void testMCLOnNonBusinessAttributeUpdate() { + PlatformEvent platformEvent = createBasePlatformEventDataset(); + + // invoke + _businessAttributeServiceHook.handleChangeEvent(platformEvent); + + // verify + Mockito.verify(_mockGraphService, Mockito.times(0)) + .findRelatedEntities( + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.anyInt(), + Mockito.anyInt()); + } + + @Test + private void testMCLOnInvalidCategory() throws Exception { + PlatformEvent platformEvent = createPlatformEventInvalidCategory(); + + // invoke + _businessAttributeServiceHook.handleChangeEvent(platformEvent); + + // verify + Mockito.verify(_mockGraphService, Mockito.times(0)) + .findRelatedEntities( + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.anyInt(), + Mockito.anyInt()); + } + + public static PlatformEvent createPlatformEventBusinessAttribute() throws Exception { + final GlobalTags newTags = new GlobalTags(); + final TagUrn newTagUrn = new TagUrn(TAG_NAME); + newTags.setTags( + new TagAssociationArray(ImmutableList.of(new TagAssociation().setTag(newTagUrn)))); + PlatformEvent platformEvent = + createChangeEvent( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), + ChangeCategory.TAG, + ChangeOperation.ADD, + newTagUrn.toString(), + ImmutableMap.of("tagUrn", newTagUrn.toString()), + actorUrn); + return platformEvent; + } + + public static PlatformEvent createBasePlatformEventDataset() { + final GlobalTags newTags = new GlobalTags(); + final TagUrn newTagUrn = new TagUrn(TAG_NAME); + newTags.setTags( + new TagAssociationArray(ImmutableList.of(new TagAssociation().setTag(newTagUrn)))); + PlatformEvent platformEvent = + createChangeEvent( + Constants.DATASET_ENTITY_NAME, + datasetUrn, + ChangeCategory.TAG, + ChangeOperation.ADD, + newTagUrn.toString(), + ImmutableMap.of("tagUrn", newTagUrn.toString()), + actorUrn); + return platformEvent; + } + + public static PlatformEvent createPlatformEventInvalidCategory() throws Exception { + final GlobalTags newTags = new GlobalTags(); + final TagUrn newTagUrn = new TagUrn(TAG_NAME); + newTags.setTags( + new TagAssociationArray(ImmutableList.of(new TagAssociation().setTag(newTagUrn)))); + PlatformEvent platformEvent = + createChangeEvent( + Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, + Urn.createFromString(TEST_BUSINESS_ATTRIBUTE_URN), + ChangeCategory.DOMAIN, + ChangeOperation.ADD, + newTagUrn.toString(), + ImmutableMap.of("tagUrn", newTagUrn.toString()), + actorUrn); + return platformEvent; + } + + private static PlatformEvent createChangeEvent( + String entityType, + Urn entityUrn, + ChangeCategory category, + ChangeOperation operation, + String modifier, + Map parameters, + Urn actor) { + final EntityChangeEvent changeEvent = new EntityChangeEvent(); + changeEvent.setEntityType(entityType); + changeEvent.setEntityUrn(entityUrn); + changeEvent.setCategory(category.name()); + changeEvent.setOperation(operation.name()); + if (modifier != null) { + changeEvent.setModifier(modifier); + } + changeEvent.setAuditStamp( + new AuditStamp().setActor(actor).setTime(BusinessAttributeUpdateHookTest.EVENT_TIME)); + changeEvent.setVersion(0); + if (parameters != null) { + changeEvent.setParameters(new Parameters(new DataMap(parameters))); + } + final PlatformEvent platformEvent = new PlatformEvent(); + platformEvent.setName(Constants.CHANGE_EVENT_PLATFORM_EVENT_NAME); + platformEvent.setHeader( + new PlatformEventHeader().setTimestampMillis(BusinessAttributeUpdateHookTest.EVENT_TIME)); + platformEvent.setPayload(GenericRecordUtils.serializePayload(changeEvent)); + return platformEvent; + } + + private Map datasetEntityResponses() { + Map datasetInfoAspects = new HashMap<>(); + datasetInfoAspects.put( + Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, + new EnvelopedAspect().setValue(new Aspect(editableSchemaMetadata().data()))); + Map datasetEntityResponses = new HashMap<>(); + datasetEntityResponses.put( + datasetUrn, + new EntityResponse() + .setUrn(datasetUrn) + .setAspects(new EnvelopedAspectMap(datasetInfoAspects))); + return datasetEntityResponses; + } + + private EditableSchemaMetadata editableSchemaMetadata() { + EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); + EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); + com.linkedin.schema.EditableSchemaFieldInfo editableSchemaFieldInfo = + new EditableSchemaFieldInfo(); + editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); + editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); + editableSchemaFieldInfos.add(editableSchemaFieldInfo); + editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); + return editableSchemaMetadata; + } +} diff --git a/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/EntityRegistryTestUtil.java b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/EntityRegistryTestUtil.java new file mode 100644 index 00000000000000..62f6fd0fceda24 --- /dev/null +++ b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/EntityRegistryTestUtil.java @@ -0,0 +1,22 @@ +package com.datahub.event.hook; + +import com.linkedin.data.schema.annotation.PathSpecBasedSchemaAnnotationVisitor; +import com.linkedin.metadata.models.registry.ConfigEntityRegistry; +import com.linkedin.metadata.models.registry.EntityRegistry; + +public class EntityRegistryTestUtil { + private EntityRegistryTestUtil() {} + + public static final EntityRegistry ENTITY_REGISTRY; + + static { + EntityRegistryTestUtil.class + .getClassLoader() + .setClassAssertionStatus(PathSpecBasedSchemaAnnotationVisitor.class.getName(), false); + ENTITY_REGISTRY = + new ConfigEntityRegistry( + EntityRegistryTestUtil.class + .getClassLoader() + .getResourceAsStream("test-entity-registry.yml")); + } +} diff --git a/metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml b/metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml new file mode 100644 index 00000000000000..081633a32bff88 --- /dev/null +++ b/metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml @@ -0,0 +1,7 @@ +entities: + - name: dataset + keyAspect: datasetKey + aspects: + - editableSchemaMetadata +events: + - name: entityChangeEvent From 3b2c34cde96273bdf96a7fb545414137c89bf88a Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Mon, 4 Mar 2024 16:11:17 +0530 Subject: [PATCH 27/50] business attributes: fixing dependency issue --- metadata-jobs/pe-consumer/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/metadata-jobs/pe-consumer/build.gradle b/metadata-jobs/pe-consumer/build.gradle index 2fd19af92971e2..3c9e916a96dfa6 100644 --- a/metadata-jobs/pe-consumer/build.gradle +++ b/metadata-jobs/pe-consumer/build.gradle @@ -24,6 +24,8 @@ dependencies { runtimeOnly externalDependency.logbackClassic testImplementation externalDependency.mockito testRuntimeOnly externalDependency.logbackClassic + testImplementation externalDependency.springBootTest + testImplementation externalDependency.testng } task avroSchemaSources(type: Copy) { From 366914132974e0bc95c337e98c604a8aa1426eeb Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Wed, 6 Mar 2024 22:49:40 +0530 Subject: [PATCH 28/50] business-attribute: fix issues due to merge conflicts --- .../ListBusinessAttributesResolver.java | 8 ++--- .../mutate/util/BusinessAttributeUtils.java | 10 ++---- .../BusinessAttributeType.java | 8 ++--- .../CreateBusinessAttributeResolverTest.java | 18 ++++------ .../UpdateBusinessAttributeResolverTest.java | 18 ++++------ .../UpdateNameResolverTest.java | 18 ++++------ .../metadata/models/EntitySpecBuilder.java | 2 +- .../SearchableRefFieldSpecExtractor.java | 35 ++++++++++++++++--- .../indexbuilder/MappingsBuilder.java | 1 + 9 files changed, 63 insertions(+), 55 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java index 23b17f999c98de..00ea5975d260e1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/ListBusinessAttributesResolver.java @@ -1,6 +1,6 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; -import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.QueryContext; @@ -10,7 +10,6 @@ import com.linkedin.datahub.graphql.generated.ListBusinessAttributesResult; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; -import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.search.SearchEntity; import com.linkedin.metadata.search.SearchResult; import graphql.schema.DataFetcher; @@ -57,13 +56,12 @@ public CompletableFuture get( final SearchResult gmsResult = _entityClient.search( + context.getOperationContext().withSearchFlags(flags -> flags.setFulltext(true)), Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, query, Collections.emptyMap(), start, - count, - context.getAuthentication(), - new SearchFlags().setFulltext(true)); + count); final ListBusinessAttributesResult result = new ListBusinessAttributesResult(); result.setStart(gmsResult.getFrom()); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java index a01fe020fd8bdc..25dc36f74ef73a 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/BusinessAttributeUtils.java @@ -3,7 +3,6 @@ import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; -import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.query.filter.Condition; import com.linkedin.metadata.query.filter.ConjunctiveCriterion; import com.linkedin.metadata.query.filter.ConjunctiveCriterionArray; @@ -31,7 +30,6 @@ public class BusinessAttributeUtils { private static final Integer DEFAULT_START = 0; private static final Integer DEFAULT_COUNT = 1000; - private static final String DEFAULT_QUERY = ""; private static final String NAME_INDEX_FIELD_NAME = "name"; private BusinessAttributeUtils() {} @@ -41,15 +39,13 @@ public static boolean hasNameConflict( Filter filter = buildNameFilter(name); try { final SearchResult gmsResult = - entityClient.search( + entityClient.filter( + context.getOperationContext(), Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME, - DEFAULT_QUERY, filter, null, DEFAULT_START, - DEFAULT_COUNT, - context.getAuthentication(), - new SearchFlags().setFulltext(true)); + DEFAULT_COUNT); return gmsResult.getNumEntities() > 0; } catch (RemoteInvocationException e) { throw new RuntimeException("Failed to fetch Business Attributes", e); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java index 063e29c70648a9..63575ea08336fc 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java @@ -25,7 +25,6 @@ import com.linkedin.entity.EntityResponse; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.query.AutoCompleteResult; -import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; import graphql.execution.DataFetcherResult; @@ -111,13 +110,12 @@ public SearchResults search( final Map facetFilters = ResolverUtils.buildFacetFilters(filters, FACET_FIELDS); final SearchResult searchResult = _entityClient.search( + context.getOperationContext().withSearchFlags(flags -> flags.setFulltext(true)), "businessAttribute", query, facetFilters, start, - count, - context.getAuthentication(), - new SearchFlags().setFulltext(true)); + count); return UrnSearchResultsMapper.map(searchResult); } @@ -131,7 +129,7 @@ public AutoCompleteResults autoComplete( throws Exception { final AutoCompleteResult result = _entityClient.autoComplete( - "businessAttribute", query, filters, limit, context.getAuthentication()); + context.getOperationContext(), "businessAttribute", query, filters, limit); return AutoCompleteResultsMapper.map(result); } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java index a5fb7fbe54cff0..8dfec0f22b5acf 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java @@ -25,13 +25,13 @@ import com.linkedin.entity.EnvelopedAspectMap; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.schema.BooleanType; import graphql.schema.DataFetchingEnvironment; +import io.datahubproject.metadata.context.OperationContext; import java.util.concurrent.ExecutionException; import org.mockito.Mockito; import org.testng.annotations.Test; @@ -76,15 +76,13 @@ public void testSuccess() throws Exception { Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))) .thenReturn(false); Mockito.when( - mockClient.search( - Mockito.any(String.class), + mockClient.filter( + Mockito.any(OperationContext.class), Mockito.any(String.class), Mockito.any(Filter.class), Mockito.isNull(), Mockito.eq(0), - Mockito.eq(1000), - Mockito.eq(mockAuthentication), - Mockito.any(SearchFlags.class))) + Mockito.eq(1000))) .thenReturn(searchResult); Mockito.when(searchResult.getNumEntities()).thenReturn(0); Mockito.when( @@ -144,15 +142,13 @@ public void testNameAlreadyExists() throws Exception { Mockito.when(mockClient.exists(Mockito.any(Urn.class), Mockito.eq(mockAuthentication))) .thenReturn(false); Mockito.when( - mockClient.search( - Mockito.any(String.class), + mockClient.filter( + Mockito.any(OperationContext.class), Mockito.any(String.class), Mockito.any(Filter.class), Mockito.isNull(), Mockito.eq(0), - Mockito.eq(1000), - Mockito.eq(mockAuthentication), - Mockito.any(SearchFlags.class))) + Mockito.eq(1000))) .thenReturn(searchResult); Mockito.when(searchResult.getNumEntities()).thenReturn(1); diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java index 7535576a0bdce5..44474956eec0b4 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolverTest.java @@ -22,13 +22,13 @@ import com.linkedin.entity.EnvelopedAspectMap; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.entity.AspectUtils; -import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.schema.BooleanType; import graphql.schema.DataFetchingEnvironment; +import io.datahubproject.metadata.context.OperationContext; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -79,15 +79,13 @@ public void testSuccess() throws Exception { TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) .thenReturn(getBusinessAttributeEntityResponse()); Mockito.when( - mockClient.search( - Mockito.any(String.class), + mockClient.filter( + Mockito.any(OperationContext.class), Mockito.any(String.class), Mockito.any(Filter.class), Mockito.isNull(), Mockito.eq(0), - Mockito.eq(1000), - Mockito.eq(mockAuthentication), - Mockito.any(SearchFlags.class))) + Mockito.eq(1000))) .thenReturn(searchResult); Mockito.when(searchResult.getNumEntities()).thenReturn(0); Mockito.when( @@ -153,15 +151,13 @@ public void testNameConflict() throws Exception { TEST_BUSINESS_ATTRIBUTE_URN_OBJ, mockAuthentication)) .thenReturn(getBusinessAttributeEntityResponse()); Mockito.when( - mockClient.search( - Mockito.any(String.class), + mockClient.filter( + Mockito.any(OperationContext.class), Mockito.any(String.class), Mockito.any(Filter.class), Mockito.isNull(), Mockito.eq(0), - Mockito.eq(1000), - Mockito.eq(mockAuthentication), - Mockito.any(SearchFlags.class))) + Mockito.eq(1000))) .thenReturn(searchResult); Mockito.when(searchResult.getNumEntities()).thenReturn(1); diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java index c3267e060801d9..efc84c91409574 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateNameResolverTest.java @@ -18,12 +18,12 @@ import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.entity.EntityUtils; -import com.linkedin.metadata.query.SearchFlags; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.search.SearchResult; import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.schema.BooleanType; import graphql.schema.DataFetchingEnvironment; +import io.datahubproject.metadata.context.OperationContext; import java.util.concurrent.ExecutionException; import org.mockito.Mockito; import org.testng.annotations.Test; @@ -70,15 +70,13 @@ public void testSuccess() throws Exception { .thenReturn(businessAttributeInfo()); Mockito.when( - mockClient.search( - Mockito.any(String.class), + mockClient.filter( + Mockito.any(OperationContext.class), Mockito.any(String.class), Mockito.any(Filter.class), Mockito.isNull(), Mockito.eq(0), - Mockito.eq(1000), - Mockito.eq(mockAuthentication), - Mockito.any(SearchFlags.class))) + Mockito.eq(1000))) .thenReturn(searchResult); Mockito.when(searchResult.getNumEntities()).thenReturn(0); @@ -120,15 +118,13 @@ public void testNameConflict() throws Exception { .thenReturn(businessAttributeInfo()); Mockito.when( - mockClient.search( - Mockito.any(String.class), + mockClient.filter( + Mockito.any(OperationContext.class), Mockito.any(String.class), Mockito.any(Filter.class), Mockito.isNull(), Mockito.eq(0), - Mockito.eq(1000), - Mockito.eq(mockAuthentication), - Mockito.any(SearchFlags.class))) + Mockito.eq(1000))) .thenReturn(searchResult); Mockito.when(searchResult.getNumEntities()).thenReturn(1); diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java index fcad25156884a4..c79ea5de69e277 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/EntitySpecBuilder.java @@ -249,13 +249,13 @@ public AspectSpec buildAspectSpec( aspectRecordSchema, new SchemaAnnotationProcessor.AnnotationProcessOption()); - // Extract SearchableRef Field Specs final SchemaAnnotationProcessor.SchemaAnnotationProcessResult processedSearchRefResult = SchemaAnnotationProcessor.process( Collections.singletonList(_searchRefScoreHandler), aspectRecordSchema, new SchemaAnnotationProcessor.AnnotationProcessOption()); + // Extract SearchableRef Field Specs final SearchableRefFieldSpecExtractor searchableRefFieldSpecExtractor = new SearchableRefFieldSpecExtractor(); final DataSchemaRichContextTraverser searchableRefFieldSpecTraverser = diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java index 1b7de5695b5be8..4f03df973467a9 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/SearchableRefFieldSpecExtractor.java @@ -1,8 +1,10 @@ package com.linkedin.metadata.models; +import com.linkedin.data.schema.ComplexDataSchema; import com.linkedin.data.schema.DataSchema; import com.linkedin.data.schema.DataSchemaTraverse; import com.linkedin.data.schema.PathSpec; +import com.linkedin.data.schema.PrimitiveDataSchema; import com.linkedin.data.schema.annotation.SchemaVisitor; import com.linkedin.data.schema.annotation.SchemaVisitorTraversalResult; import com.linkedin.data.schema.annotation.TraverserContext; @@ -41,9 +43,19 @@ public void callbackOnContext(TraverserContext context, DataSchemaTraverse.Order final Object annotationObj = getAnnotationObj(context); if (annotationObj != null) { - validatePropertiesAnnotation( - currentSchema, annotationObj, context.getTraversePath().toString()); - extractSearchableRefAnnotation(annotationObj, currentSchema, context); + if (currentSchema.getDereferencedDataSchema().isComplex()) { + final ComplexDataSchema complexSchema = (ComplexDataSchema) currentSchema; + if (isValidComplexType(complexSchema)) { + extractSearchableRefAnnotation(annotationObj, currentSchema, context); + } + } else if (isValidPrimitiveType((PrimitiveDataSchema) currentSchema)) { + extractSearchableRefAnnotation(annotationObj, currentSchema, context); + } else { + throw new ModelValidationException( + String.format( + "Invalid @SearchableRef Annotation at %s", + context.getSchemaPathSpec().toString())); + } } } } @@ -57,11 +69,17 @@ private Object getAnnotationObj(TraverserContext context) { if (primaryAnnotationObj != null) { validatePropertiesAnnotation( currentSchema, primaryAnnotationObj, context.getTraversePath().toString()); + + if (currentSchema.getDereferencedType() == DataSchema.Type.MAP + && primaryAnnotationObj instanceof Map + && !((Map) primaryAnnotationObj).isEmpty()) { + return ((Map) primaryAnnotationObj).entrySet().stream().findFirst().get().getValue(); + } } // Next, check resolved properties for annotations on primitives. final Map resolvedProperties = - FieldSpecUtils.getResolvedProperties(currentSchema); + FieldSpecUtils.getResolvedProperties(currentSchema, properties); final Object resolvedAnnotationObj = resolvedProperties.get(SearchableRefAnnotation.ANNOTATION_NAME); return resolvedAnnotationObj; @@ -157,4 +175,13 @@ private void validatePropertiesAnnotation( } } } + + private Boolean isValidComplexType(final ComplexDataSchema schema) { + return DataSchema.Type.ENUM.equals(schema.getDereferencedDataSchema().getDereferencedType()) + || DataSchema.Type.MAP.equals(schema.getDereferencedDataSchema().getDereferencedType()); + } + + private Boolean isValidPrimitiveType(final PrimitiveDataSchema schema) { + return true; + } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java index f31a04367636b3..8b2eb5a7817011 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java @@ -12,6 +12,7 @@ import com.linkedin.metadata.models.LogicalValueType; import com.linkedin.metadata.models.SearchScoreFieldSpec; import com.linkedin.metadata.models.SearchableFieldSpec; +import com.linkedin.metadata.models.SearchableRefFieldSpec; import com.linkedin.metadata.models.annotation.SearchableAnnotation.FieldType; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.utils.ESUtils; From 13e5aee56f0dfd3458eb5f15aad146b64e4a397f Mon Sep 17 00:00:00 2001 From: "Singh, Himanshu" Date: Wed, 20 Dec 2023 21:28:48 +0530 Subject: [PATCH 29/50] Business Attribute : SearchableRef Unit Test --- .../java/com/linkedin/metadata/Constants.java | 3 +- .../query/request/TestSearchFieldConfig.java | 64 +++++++ .../indexbuilder/MappingsBuilderTest.java | 64 +++++++ .../SearchDocumentTransformerTest.java | 163 +++++++++++++++++- .../test/resources/test-entity-registry.yaml | 10 ++ .../com/datahub/test/RefEntityAspect.pdl | 7 + .../com/datahub/test/RefEntityAssociation.pdl | 8 + .../pegasus/com/datahub/test/RefEntityKey.pdl | 17 ++ .../com/datahub/test/RefEntityProperties.pdl | 31 ++++ .../com/datahub/test/RefProperties.pdl | 20 +++ .../com/datahub/test/TestRefEntity.pdl | 20 +++ .../com/datahub/test/TestRefEntityAspect.pdl | 6 + .../com/datahub/test/TestRefEntityInfo.pdl | 49 ++++++ .../com/datahub/test/TestRefEntityKey.pdl | 16 ++ 14 files changed, 475 insertions(+), 3 deletions(-) create mode 100644 metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/TestSearchFieldConfig.java create mode 100644 metadata-io/src/test/resources/test-entity-registry.yaml create mode 100644 test-models/src/main/pegasus/com/datahub/test/RefEntityAspect.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/RefEntityAssociation.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/RefEntityKey.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/RefEntityProperties.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/RefProperties.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/TestRefEntity.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/TestRefEntityAspect.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/TestRefEntityInfo.pdl create mode 100644 test-models/src/main/pegasus/com/datahub/test/TestRefEntityKey.pdl diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index fc1a8199a39f3f..33cc1f028258e2 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -378,8 +378,7 @@ public class Constants { public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; - public static final List SKIP_REFERENCE_ASPECT = - Arrays.asList("ownership", "status", "institutionalMemory"); + public static final List SKIP_REFERENCE_ASPECT = List.of("ownership", "status", "institutionalMemory"); // Posts public static final String POST_INFO_ASPECT_NAME = "postInfo"; diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/TestSearchFieldConfig.java b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/TestSearchFieldConfig.java new file mode 100644 index 00000000000000..062298796dd7c7 --- /dev/null +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/elasticsearch/query/request/TestSearchFieldConfig.java @@ -0,0 +1,64 @@ +package com.linkedin.metadata.search.elasticsearch.query.request; + +import com.linkedin.metadata.models.SearchableRefFieldSpec; +import com.linkedin.metadata.models.registry.ConfigEntityRegistry; +import com.linkedin.metadata.models.registry.EntityRegistry; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.testng.annotations.Test; + +@Test +public class TestSearchFieldConfig { + + void setup() {} + + /** + * + * + *

    + *
  • {@link SearchFieldConfig#detectSubFieldType( SearchableRefFieldSpec, int, EntityRegistry + * ) } + *
+ */ + @Test + public void detectSubFieldType() { + EntityRegistry entityRegistry = getTestEntityRegistry(); + SearchableRefFieldSpec searchableRefFieldSpec = + entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + + Set responseForNonZeroDepth = + SearchFieldConfig.detectSubFieldType(searchableRefFieldSpec, 1, entityRegistry); + Assertions.assertTrue( + responseForNonZeroDepth.stream() + .anyMatch( + searchFieldConfig -> + searchFieldConfig.fieldName().equals("refEntityUrns.displayName"))); + Assertions.assertTrue( + responseForNonZeroDepth.stream() + .anyMatch( + searchFieldConfig -> searchFieldConfig.fieldName().equals("refEntityUrns.urn"))); + Assertions.assertTrue( + responseForNonZeroDepth.stream() + .anyMatch( + searchFieldConfig -> + searchFieldConfig.fieldName().equals("refEntityUrns.editedFieldDescriptions"))); + + Set responseForZeroDepth = + SearchFieldConfig.detectSubFieldType(searchableRefFieldSpec, 0, entityRegistry); + Optional searchFieldConfigToCompare = + responseForZeroDepth.stream() + .filter(searchFieldConfig -> searchFieldConfig.fieldName().equals("refEntityUrns")) + .findFirst(); + + Assertions.assertTrue(searchFieldConfigToCompare.isPresent()); + Assertions.assertEquals("query_urn_component", searchFieldConfigToCompare.get().analyzer()); + } + + private EntityRegistry getTestEntityRegistry() { + return new ConfigEntityRegistry( + TestSearchFieldConfig.class + .getClassLoader() + .getResourceAsStream("test-entity-registry.yaml")); + } +} diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java index 8d504c562c99cc..49a15b43d06aa6 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java @@ -3,12 +3,19 @@ import static com.linkedin.metadata.Constants.*; import static org.testng.Assert.*; +import com.datahub.test.TestRefEntity; import com.google.common.collect.ImmutableMap; import com.linkedin.common.UrnArray; import com.linkedin.common.urn.Urn; import com.linkedin.metadata.TestEntitySpecBuilder; +import com.linkedin.metadata.models.EntitySpec; +import com.linkedin.metadata.models.EntitySpecBuilder; +import com.linkedin.metadata.models.registry.ConfigEntityRegistry; +import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.elasticsearch.indexbuilder.MappingsBuilder; +import com.linkedin.metadata.search.elasticsearch.query.request.TestSearchFieldConfig; import com.linkedin.structured.StructuredPropertyDefinition; +import java.io.Serializable; import java.net.URISyntaxException; import java.util.List; import java.util.Map; @@ -271,4 +278,61 @@ public void testGetMappingsForStructuredProperty() throws URISyntaxException { mappings = structuredPropertyFieldMappingsNumber.get(keyInMap); assertEquals(Map.of("type", "double"), mappings); } + + @Test + public void testRefMappingsBuilder() { + EntityRegistry entityRegistry = getTestEntityRegistry(); + MappingsBuilder.setEntityRegistry(entityRegistry); + EntitySpec entitySpec = new EntitySpecBuilder().buildEntitySpec(new TestRefEntity().schema()); + Map result = MappingsBuilder.getMappings(entitySpec); + assertEquals(result.size(), 1); + Map properties = (Map) result.get("properties"); + assertEquals(properties.size(), 6); + ImmutableMap expectedURNField = + ImmutableMap.of( + "type", + "keyword", + "fields", + ImmutableMap.of( + "delimited", + ImmutableMap.of( + "type", + "text", + "analyzer", + "urn_component", + "search_analyzer", + "query_urn_component", + "search_quote_analyzer", + "quote_analyzer"), + "ngram", + ImmutableMap.of( + "type", + "search_as_you_type", + "max_shingle_size", + "4", + "doc_values", + "false", + "analyzer", + "partial_urn_component"))); + assertEquals(properties.get("urn"), expectedURNField); + assertEquals(properties.get("runId"), ImmutableMap.of("type", "keyword")); + assertTrue(properties.containsKey("editedFieldDescriptions")); + assertTrue(properties.containsKey("displayName")); + assertTrue(properties.containsKey("refEntityUrns")); + // @SearchableRef Field + Map refField = (Map) properties.get("refEntityUrns"); + assertEquals(refField.size(), 1); + Map refFieldProperty = (Map) refField.get("properties"); + + assertEquals(refFieldProperty.get("urn"), expectedURNField); + assertTrue(refFieldProperty.containsKey("displayName")); + assertTrue(refFieldProperty.containsKey("editedFieldDescriptions")); + } + + private EntityRegistry getTestEntityRegistry() { + return new ConfigEntityRegistry( + TestSearchFieldConfig.class + .getClassLoader() + .getResourceAsStream("test-entity-registry.yaml")); + } } diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java index 6e2d90287d5d93..312314d431fb43 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformerTest.java @@ -1,6 +1,7 @@ package com.linkedin.metadata.search.transformer; import static com.linkedin.metadata.Constants.*; +import static org.mockito.Mockito.*; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -13,11 +14,22 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.linkedin.common.urn.Urn; +import com.linkedin.data.DataMapBuilder; +import com.linkedin.entity.Aspect; import com.linkedin.metadata.TestEntitySpecBuilder; import com.linkedin.metadata.TestEntityUtil; +import com.linkedin.metadata.aspect.AspectRetriever; import com.linkedin.metadata.models.EntitySpec; +import com.linkedin.metadata.models.SearchableRefFieldSpec; +import com.linkedin.metadata.models.registry.ConfigEntityRegistry; +import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.search.elasticsearch.query.request.TestSearchFieldConfig; +import com.linkedin.r2.RemoteInvocationException; import java.io.IOException; -import java.util.Optional; +import java.net.URISyntaxException; +import java.util.*; +import org.mockito.Mockito; import org.testng.annotations.Test; public class SearchDocumentTransformerTest { @@ -132,4 +144,153 @@ public void testTransformMaxFieldValue() throws IOException { .add("123") .add("0123456789")); } + + /** + * + * + *
    + *
  • {@link SearchDocumentTransformer#setSearchableRefValue(SearchableRefFieldSpec, List, + * ObjectNode, Boolean ) } + *
+ */ + @Test + public void testSetSearchableRefValue() throws URISyntaxException, RemoteInvocationException { + AspectRetriever aspectRetriever = Mockito.mock(AspectRetriever.class); + SearchDocumentTransformer searchDocumentTransformer = + new SearchDocumentTransformer(1000, 1000, 1000); + searchDocumentTransformer.setAspectRetriever(aspectRetriever); + EntityRegistry entityRegistry = getTestEntityRegistry(); + List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); + + DataMapBuilder dataMapBuilder = new DataMapBuilder(); + dataMapBuilder.addKVPair("fieldPath", "refEntityUrn"); + dataMapBuilder.addKVPair("name", "refEntityUrnName"); + dataMapBuilder.addKVPair("description", "refEntityUrn1 description details"); + Aspect aspect = new Aspect(dataMapBuilder.convertToDataMap()); + + ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); + SearchableRefFieldSpec searchableRefFieldSpec = + entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + + // Mock Behaviour + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when(aspectRetriever.getLatestAspectObject(any(), anyString())).thenReturn(aspect); + + searchDocumentTransformer.setSearchableRefValue( + searchableRefFieldSpec, urnList, searchDocument, false); + assertTrue(searchDocument.has("refEntityUrns")); + assertEquals(searchDocument.get("refEntityUrns").size(), 3); + assertTrue(searchDocument.get("refEntityUrns").has("urn")); + assertTrue(searchDocument.get("refEntityUrns").has("editedFieldDescriptions")); + assertTrue(searchDocument.get("refEntityUrns").has("displayName")); + assertEquals(searchDocument.get("refEntityUrns").get("urn").asText(), "urn:li:refEntity:1"); + assertEquals( + searchDocument.get("refEntityUrns").get("editedFieldDescriptions").asText(), + "refEntityUrn1 description details"); + assertEquals( + searchDocument.get("refEntityUrns").get("displayName").asText(), "refEntityUrnName"); + } + + @Test + public void testSetSearchableRefValue_WithNonURNField() throws URISyntaxException { + AspectRetriever aspectRetriever = Mockito.mock(AspectRetriever.class); + SearchDocumentTransformer searchDocumentTransformer = + new SearchDocumentTransformer(1000, 1000, 1000); + searchDocumentTransformer.setAspectRetriever(aspectRetriever); + EntityRegistry entityRegistry = getTestEntityRegistry(); + List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); + + ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); + SearchableRefFieldSpec searchableRefFieldSpecText = + entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(1); + searchDocumentTransformer.setSearchableRefValue( + searchableRefFieldSpecText, urnList, searchDocument, false); + assertTrue(searchDocument.isEmpty()); + } + + @Test + public void testSetSearchableRefValue_RemoteInvocationException() + throws URISyntaxException, RemoteInvocationException { + AspectRetriever aspectRetriever = Mockito.mock(AspectRetriever.class); + SearchDocumentTransformer searchDocumentTransformer = + new SearchDocumentTransformer(1000, 1000, 1000); + searchDocumentTransformer.setAspectRetriever(aspectRetriever); + EntityRegistry entityRegistry = getTestEntityRegistry(); + List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); + + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when( + aspectRetriever.getLatestAspectObject( + eq(Urn.createFromString("urn:li:refEntity:1")), anyString())) + .thenThrow(new RemoteInvocationException("Error")); + + ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); + SearchableRefFieldSpec searchableRefFieldSpec = + entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + searchDocumentTransformer.setSearchableRefValue( + searchableRefFieldSpec, urnList, searchDocument, false); + assertTrue(searchDocument.isEmpty()); + } + + @Test + public void testSetSearchableRefValue_RemoteInvocationException_URNExist() + throws URISyntaxException, RemoteInvocationException { + AspectRetriever aspectRetriever = Mockito.mock(AspectRetriever.class); + SearchDocumentTransformer searchDocumentTransformer = + new SearchDocumentTransformer(1000, 1000, 1000); + searchDocumentTransformer.setAspectRetriever(aspectRetriever); + EntityRegistry entityRegistry = getTestEntityRegistry(); + List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); + DataMapBuilder dataMapBuilder = new DataMapBuilder(); + dataMapBuilder.addKVPair("fieldPath", "refEntityUrn"); + dataMapBuilder.addKVPair("name", "refEntityUrnName"); + dataMapBuilder.addKVPair("description", "refEntityUrn1 description details"); + + Aspect aspect = new Aspect(dataMapBuilder.convertToDataMap()); + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when( + aspectRetriever.getLatestAspectObject( + eq(Urn.createFromString("urn:li:refEntity:1")), anyString())) + .thenReturn(aspect) + .thenThrow(new RemoteInvocationException("Error")); + + ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); + SearchableRefFieldSpec searchableRefFieldSpec = + entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + searchDocumentTransformer.setSearchableRefValue( + searchableRefFieldSpec, urnList, searchDocument, false); + assertTrue(searchDocument.has("refEntityUrns")); + assertEquals(searchDocument.get("refEntityUrns").size(), 1); + assertTrue(searchDocument.get("refEntityUrns").has("urn")); + assertEquals(searchDocument.get("refEntityUrns").get("urn").asText(), "urn:li:refEntity:1"); + } + + @Test + void testSetSearchableRefValue_WithInvalidURN() + throws URISyntaxException, RemoteInvocationException { + AspectRetriever aspectRetriever = Mockito.mock(AspectRetriever.class); + SearchDocumentTransformer searchDocumentTransformer = + new SearchDocumentTransformer(1000, 1000, 1000); + searchDocumentTransformer.setAspectRetriever(aspectRetriever); + EntityRegistry entityRegistry = getTestEntityRegistry(); + List urnList = List.of(Urn.createFromString("urn:li:refEntity:1")); + + Mockito.when(aspectRetriever.getEntityRegistry()).thenReturn(entityRegistry); + Mockito.when(aspectRetriever.getLatestAspectObject(any(), anyString())).thenReturn(null); + SearchableRefFieldSpec searchableRefFieldSpec = + entityRegistry.getEntitySpec("testRefEntity").getSearchableRefFieldSpecs().get(0); + + ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); + searchDocumentTransformer.setSearchableRefValue( + searchableRefFieldSpec, urnList, searchDocument, false); + assertTrue(searchDocument.has("refEntityUrns")); + assertTrue(searchDocument.get("refEntityUrns").getNodeType().equals(JsonNodeType.NULL)); + } + + private EntityRegistry getTestEntityRegistry() { + return new ConfigEntityRegistry( + TestSearchFieldConfig.class + .getClassLoader() + .getResourceAsStream("test-entity-registry.yaml")); + } } diff --git a/metadata-io/src/test/resources/test-entity-registry.yaml b/metadata-io/src/test/resources/test-entity-registry.yaml new file mode 100644 index 00000000000000..e9bd46a7cf43a2 --- /dev/null +++ b/metadata-io/src/test/resources/test-entity-registry.yaml @@ -0,0 +1,10 @@ +id: test-registry +entities: + - name: testRefEntity + keyAspect: testRefEntityKey + aspects: + - testRefEntityInfo + - name: refEntity + keyAspect: refEntityKey + aspects: + - refEntityProperties \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/RefEntityAspect.pdl b/test-models/src/main/pegasus/com/datahub/test/RefEntityAspect.pdl new file mode 100644 index 00000000000000..2921cc2e389ab1 --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/RefEntityAspect.pdl @@ -0,0 +1,7 @@ +namespace com.datahub.test + + +/** + * A union of all supported metadata aspects for a RefEntity + */ +typeref RefEntityAspect = union[RefEntityKey, RefProperties] \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/RefEntityAssociation.pdl b/test-models/src/main/pegasus/com/datahub/test/RefEntityAssociation.pdl new file mode 100644 index 00000000000000..9384a7d0d9a9c5 --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/RefEntityAssociation.pdl @@ -0,0 +1,8 @@ +namespace com.datahub.test + +import com.linkedin.common.Urn +import com.linkedin.common.Edge + +record RefEntityAssociation includes Edge{ + +} \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/RefEntityKey.pdl b/test-models/src/main/pegasus/com/datahub/test/RefEntityKey.pdl new file mode 100644 index 00000000000000..2197ef81c4031f --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/RefEntityKey.pdl @@ -0,0 +1,17 @@ +namespace com.datahub.test + +import com.linkedin.common.Urn + +/** + * Key for Test Entity entity + */ +@Aspect = { + "name": "refEntityKey" +} +record RefEntityKey { + + /** + * A unique id + */ + id: string +} diff --git a/test-models/src/main/pegasus/com/datahub/test/RefEntityProperties.pdl b/test-models/src/main/pegasus/com/datahub/test/RefEntityProperties.pdl new file mode 100644 index 00000000000000..eab805e4fc7b52 --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/RefEntityProperties.pdl @@ -0,0 +1,31 @@ +namespace com.datahub.test + + +/** + * Additional properties associated with a RefEntity + */ +@Aspect = { + "name": "refEntityProperties" +} +record RefEntityProperties { + /** + * Display name of the RefEntity + */ + @Searchable = { + "fieldType": "WORD_GRAM", + "enableAutocomplete": true, + "boostScore": 10.0, + "fieldName": "displayName" + } + name: string + + /** + * Description of the RefEntity + */ + @Searchable = { + "fieldName": "editedFieldDescriptions", + "fieldType": "TEXT", + "boostScore": 0.1 + } + description: optional string +} \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/RefProperties.pdl b/test-models/src/main/pegasus/com/datahub/test/RefProperties.pdl new file mode 100644 index 00000000000000..e04faeab3b0e7d --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/RefProperties.pdl @@ -0,0 +1,20 @@ +namespace com.datahub.test + +/** + * Properties associated with a Tag + */ +@Aspect = { + "name": "RefProperties" +} +record RefProperties { + /** + * Display name of the ref + */ + @Searchable = { + "fieldType": "WORD_GRAM", + "enableAutocomplete": true, + "boostScore": 10.0, + "fieldNameAliases": [ "_entityName" ] + } + name: string +} \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/TestRefEntity.pdl b/test-models/src/main/pegasus/com/datahub/test/TestRefEntity.pdl new file mode 100644 index 00000000000000..b128f6780e4fbc --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/TestRefEntity.pdl @@ -0,0 +1,20 @@ +namespace com.datahub.test + +import com.linkedin.common.Urn + +@Entity = { + "name": "testRefEntity", + "keyAspect": "testRefEntityKey" +} +record TestRefEntity { + + /** + * Urn for the service + */ + urn: Urn + + /** + * The list of service aspects + */ + aspects: array[TestRefEntityAspect] +} \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/TestRefEntityAspect.pdl b/test-models/src/main/pegasus/com/datahub/test/TestRefEntityAspect.pdl new file mode 100644 index 00000000000000..9c732c9678c6f2 --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/TestRefEntityAspect.pdl @@ -0,0 +1,6 @@ +namespace com.datahub.test + +/** + * A union of all supported metadata aspects for a RefEntity + */ +typeref TestRefEntityAspect = union[TestRefEntityKey, TestRefEntityInfo] \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/TestRefEntityInfo.pdl b/test-models/src/main/pegasus/com/datahub/test/TestRefEntityInfo.pdl new file mode 100644 index 00000000000000..8116753a4b7274 --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/TestRefEntityInfo.pdl @@ -0,0 +1,49 @@ +namespace com.datahub.test + + +/** + * Additional properties associated with a RefEntity + */ +@Aspect = { + "name": "testRefEntityInfo" +} +record TestRefEntityInfo { + /** + * Display name of the testRefEntityInfo + */ + @Searchable = { + "fieldType": "WORD_GRAM", + "enableAutocomplete": true, + "boostScore": 10.0, + "fieldName": "displayName" + } + name: string + + /** + * Description of the RefEntity + */ + @Searchable = { + "fieldName": "editedFieldDescriptions", + "fieldType": "TEXT", + "boostScore": 0.1 + } + description: optional string + + +@SearchableRef = { + "/destinationUrn": { + "fieldName": "refEntityUrns", + "fieldType": "URN", + "refType" : "RefEntity" + } + } + refEntityAssociation: optional RefEntityAssociation + + @SearchableRef = { + "fieldName": "editedFieldDescriptionsRef", + "fieldType": "TEXT", + "boostScore": 0.5, + "refType" : "RefEntity" + } + refEntityAssociationText: optional string +} \ No newline at end of file diff --git a/test-models/src/main/pegasus/com/datahub/test/TestRefEntityKey.pdl b/test-models/src/main/pegasus/com/datahub/test/TestRefEntityKey.pdl new file mode 100644 index 00000000000000..0aab3d091d0ff9 --- /dev/null +++ b/test-models/src/main/pegasus/com/datahub/test/TestRefEntityKey.pdl @@ -0,0 +1,16 @@ +namespace com.datahub.test + + +/** + * Key for Test Ref Entity Defining parent entity with reference field + */ +@Aspect = { + "name": "testRefEntityKey" +} +record TestRefEntityKey { + + /** + * A unique id + */ + id: string +} From 6b242a0a38b224b2675876aee15a2025f3873e35 Mon Sep 17 00:00:00 2001 From: "Shukla, Amit" Date: Wed, 6 Mar 2024 17:25:43 +0530 Subject: [PATCH 30/50] fix(ui): business-attribute: Dulicate Gloassary term rendering --- .../Schema/utils/useTagsAndTermsRenderer.tsx | 38 ++----------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx index d14bf635208e31..bd452dfb492d0c 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx @@ -25,44 +25,14 @@ export default function useTagsAndTermsRenderer( (candidateEditableFieldInfo) => pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, record.fieldPath), ); - const newRecord = { ...record }; + const businessAttributeTags = relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.tags?.tags || []; + const businessAttributeTerms = relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.glossaryTerms?.terms || []; - if (!newRecord.glossaryTerms) { - newRecord.glossaryTerms = { terms: [] }; - } - - if (!newRecord.glossaryTerms.terms) { - newRecord.glossaryTerms.terms = []; - } - - if ( - relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties - ?.glossaryTerms?.terms - ) { - newRecord.glossaryTerms.terms = [ - ...newRecord.glossaryTerms.terms, - ...relevantEditableFieldInfo.businessAttributes.businessAttribute.businessAttribute.properties - .glossaryTerms.terms, - ]; - } - let newTags = {}; - if ( - relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.tags?.tags - ) { - newTags = { - ...tags, - tags: [ - ...(tags?.tags || []), - ...relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties - ?.tags?.tags, - ], - }; - } return ( Date: Mon, 4 Mar 2024 18:27:54 +0530 Subject: [PATCH 31/50] Bug Fix : SearchableRef Search Reference Field --- .../java/com/linkedin/metadata/Constants.java | 4 +- .../query/request/SearchFieldConfig.java | 81 ++++++++--- .../query/request/SearchQueryBuilder.java | 134 ++++++++++++++---- 3 files changed, 164 insertions(+), 55 deletions(-) diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index 33cc1f028258e2..3fb95996f53544 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -2,7 +2,6 @@ import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; -import java.util.Arrays; import java.util.List; /** Static class containing commonly-used constants across DataHub services. */ @@ -378,7 +377,8 @@ public class Constants { public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; - public static final List SKIP_REFERENCE_ASPECT = List.of("ownership", "status", "institutionalMemory"); + public static final List SKIP_REFERENCE_ASPECT = + List.of("ownership", "status", "institutionalMemory"); // Posts public static final String POST_INFO_ASPECT_NAME = "postInfo"; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java index 248872908a39ce..a7ba78230e9def 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java @@ -89,27 +89,6 @@ public static SearchFieldConfig detectSubFieldType(@Nonnull SearchableFieldSpec return detectSubFieldType(fieldName, boost, fieldType, searchableAnnotation.isQueryByDefault()); } - public static SearchFieldConfig detectSubFieldType( - String fieldName, SearchableAnnotation.FieldType fieldType, boolean isQueryByDefault) { - return detectSubFieldType(fieldName, DEFAULT_BOOST, fieldType, isQueryByDefault); - } - - public static SearchFieldConfig detectSubFieldType( - String fieldName, - float boost, - SearchableAnnotation.FieldType fieldType, - boolean isQueryByDefault) { - return SearchFieldConfig.builder() - .fieldName(fieldName) - .boost(boost) - .analyzer(getAnalyzer(fieldName, fieldType)) - .hasKeywordSubfield(hasKeywordSubfield(fieldName, fieldType)) - .hasDelimitedSubfield(hasDelimitedSubfield(fieldName, fieldType)) - .hasWordGramSubfields(hasWordGramSubfields(fieldName, fieldType)) - .isQueryByDefault(isQueryByDefault) - .build(); - } - public static Set detectSubFieldType( @Nonnull SearchableRefFieldSpec fieldSpec, int depth, EntityRegistry entityRegistry) { Set fieldConfigs = new HashSet<>(); @@ -145,8 +124,8 @@ public static Set detectSubFieldType( String urnFieldName = fieldName + ".urn"; fieldConfigs.add( detectSubFieldType(urnFieldName, boostScore, SearchableAnnotation.FieldType.URN, true)); - List aspectSpecs = refEntitySpec.getAspectSpecs(); + for (AspectSpec aspectSpec : aspectSpecs) { if (!SKIP_REFERENCE_ASPECT.contains(aspectSpec.getName())) { for (SearchableFieldSpec searchableFieldSpec : aspectSpec.getSearchableFieldSpecs()) { @@ -158,7 +137,7 @@ public static Set detectSubFieldType( final float refBoost = (float) searchableAnnotation.getBoostScore() * boostScore; final SearchableAnnotation.FieldType refFieldType = searchableAnnotation.getFieldType(); fieldConfigs.add( - detectSubFieldType( + detectSubFieldTypeForRef( refFieldName, refBoost, refFieldType, searchableAnnotation.isQueryByDefault())); } @@ -179,6 +158,43 @@ public static Set detectSubFieldType( return fieldConfigs; } + public static SearchFieldConfig detectSubFieldType( + String fieldName, SearchableAnnotation.FieldType fieldType, boolean isQueryByDefault) { + return detectSubFieldType(fieldName, DEFAULT_BOOST, fieldType, isQueryByDefault); + } + + public static SearchFieldConfig detectSubFieldType( + String fieldName, + float boost, + SearchableAnnotation.FieldType fieldType, + boolean isQueryByDefault) { + return SearchFieldConfig.builder() + .fieldName(fieldName) + .boost(boost) + .analyzer(getAnalyzer(fieldName, fieldType)) + .hasKeywordSubfield(hasKeywordSubfield(fieldName, fieldType)) + .hasDelimitedSubfield(hasDelimitedSubfield(fieldName, fieldType)) + .hasWordGramSubfields(hasWordGramSubfields(fieldName, fieldType)) + .isQueryByDefault(isQueryByDefault) + .build(); + } + + public static SearchFieldConfig detectSubFieldTypeForRef( + String fieldName, + float boost, + SearchableAnnotation.FieldType fieldType, + boolean isQueryByDefault) { + return SearchFieldConfig.builder() + .fieldName(fieldName) + .boost(boost) + .analyzer(getAnalyzer(fieldName, fieldType)) + .hasKeywordSubfield(hasKeywordSubfieldForRefField(fieldName, fieldType)) + .hasDelimitedSubfield(hasDelimitedSubfieldForRefField(fieldName, fieldType)) + .hasWordGramSubfields(hasWordGramSubfieldsForRefField(fieldType)) + .isQueryByDefault(isQueryByDefault) + .build(); + } + public boolean isKeyword() { return KEYWORD_ANALYZER.equals(analyzer()) || isKeyword(fieldName()); } @@ -206,6 +222,25 @@ private static boolean isKeyword(String fieldName) { return fieldName.endsWith(".keyword") || KEYWORD_FIELDS.contains(fieldName); } + private static boolean hasKeywordSubfieldForRefField( + String fieldName, SearchableAnnotation.FieldType fieldType) { + return !"urn".equals(fieldName) + && !fieldName.endsWith(".urn") + && (TYPES_WITH_DELIMITED_SUBFIELD.contains(fieldType) // if delimited then also has keyword + || TYPES_WITH_KEYWORD_SUBFIELD.contains(fieldType)); + } + + private static boolean hasWordGramSubfieldsForRefField(SearchableAnnotation.FieldType fieldType) { + return TYPES_WITH_WORD_GRAM.contains(fieldType); + } + + private static boolean hasDelimitedSubfieldForRefField( + String fieldName, SearchableAnnotation.FieldType fieldType) { + return (fieldName.endsWith(".urn") + || "urn".equals(fieldName) + || TYPES_WITH_DELIMITED_SUBFIELD.contains(fieldType)); + } + private static String getAnalyzer(String fieldName, SearchableAnnotation.FieldType fieldType) { // order is important if (TYPES_WITH_BROWSE_PATH.contains(fieldType)) { diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java index 5683b571888e0d..52067003eef149 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java @@ -1,5 +1,6 @@ package com.linkedin.metadata.search.elasticsearch.query.request; +import static com.linkedin.metadata.Constants.SKIP_REFERENCE_ASPECT; import static com.linkedin.metadata.models.SearchableFieldSpecExtractor.PRIMARY_URN_SEARCH_PROPERTIES; import static com.linkedin.metadata.search.elasticsearch.indexbuilder.SettingsBuilder.*; import static com.linkedin.metadata.search.elasticsearch.query.request.SearchFieldConfig.*; @@ -16,12 +17,14 @@ import com.linkedin.metadata.config.search.custom.BoolQueryConfiguration; import com.linkedin.metadata.config.search.custom.CustomSearchConfiguration; import com.linkedin.metadata.config.search.custom.QueryConfiguration; +import com.linkedin.metadata.models.AspectSpec; import com.linkedin.metadata.models.EntitySpec; import com.linkedin.metadata.models.SearchScoreFieldSpec; import com.linkedin.metadata.models.SearchableFieldSpec; import com.linkedin.metadata.models.SearchableRefFieldSpec; import com.linkedin.metadata.models.annotation.SearchScoreAnnotation; import com.linkedin.metadata.models.annotation.SearchableAnnotation; +import com.linkedin.metadata.models.annotation.SearchableRefAnnotation; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.utils.ESUtils; import java.io.IOException; @@ -227,36 +230,7 @@ public Set getFieldsFromEntitySpec(EntitySpec entitySpec) { searchableAnnotation.isQueryByDefault())); if (SearchFieldConfig.detectSubFieldType(fieldSpec).hasWordGramSubfields()) { - fields.add( - SearchFieldConfig.builder() - .fieldName(searchFieldConfig.fieldName() + ".wordGrams2") - .boost(searchFieldConfig.boost() * wordGramConfiguration.getTwoGramFactor()) - .analyzer(WORD_GRAM_2_ANALYZER) - .hasKeywordSubfield(true) - .hasDelimitedSubfield(true) - .hasWordGramSubfields(true) - .isQueryByDefault(true) - .build()); - fields.add( - SearchFieldConfig.builder() - .fieldName(searchFieldConfig.fieldName() + ".wordGrams3") - .boost(searchFieldConfig.boost() * wordGramConfiguration.getThreeGramFactor()) - .analyzer(WORD_GRAM_3_ANALYZER) - .hasKeywordSubfield(true) - .hasDelimitedSubfield(true) - .hasWordGramSubfields(true) - .isQueryByDefault(true) - .build()); - fields.add( - SearchFieldConfig.builder() - .fieldName(searchFieldConfig.fieldName() + ".wordGrams4") - .boost(searchFieldConfig.boost() * wordGramConfiguration.getFourGramFactor()) - .analyzer(WORD_GRAM_4_ANALYZER) - .hasKeywordSubfield(true) - .hasDelimitedSubfield(true) - .hasWordGramSubfields(true) - .isQueryByDefault(true) - .build()); + addWordGramSearchConfig(fields, searchFieldConfig); } } } @@ -267,10 +241,61 @@ public Set getFieldsFromEntitySpec(EntitySpec entitySpec) { Set searchFieldConfig = SearchFieldConfig.detectSubFieldType(refFieldSpec, depth, entityRegistry); fields.addAll(searchFieldConfig); + + Map fieldTypeMap = + getAllFieldTypeFromSearchableRef(refFieldSpec, depth, entityRegistry, ""); + for (SearchFieldConfig fieldConfig : searchFieldConfig) { + if (fieldConfig.hasDelimitedSubfield()) { + fields.add( + SearchFieldConfig.detectSubFieldType( + fieldConfig.fieldName() + ".delimited", + fieldConfig.boost() * partialConfiguration.getFactor(), + fieldTypeMap.get(fieldConfig.fieldName()), + fieldConfig.isQueryByDefault())); + } + + if (fieldConfig.hasWordGramSubfields()) { + addWordGramSearchConfig(fields, fieldConfig); + } + } } return fields; } + private void addWordGramSearchConfig( + Set fields, SearchFieldConfig searchFieldConfig) { + fields.add( + SearchFieldConfig.builder() + .fieldName(searchFieldConfig.fieldName() + ".wordGrams2") + .boost(searchFieldConfig.boost() * wordGramConfiguration.getTwoGramFactor()) + .analyzer(WORD_GRAM_2_ANALYZER) + .hasKeywordSubfield(true) + .hasDelimitedSubfield(true) + .hasWordGramSubfields(true) + .isQueryByDefault(true) + .build()); + fields.add( + SearchFieldConfig.builder() + .fieldName(searchFieldConfig.fieldName() + ".wordGrams3") + .boost(searchFieldConfig.boost() * wordGramConfiguration.getThreeGramFactor()) + .analyzer(WORD_GRAM_3_ANALYZER) + .hasKeywordSubfield(true) + .hasDelimitedSubfield(true) + .hasWordGramSubfields(true) + .isQueryByDefault(true) + .build()); + fields.add( + SearchFieldConfig.builder() + .fieldName(searchFieldConfig.fieldName() + ".wordGrams4") + .boost(searchFieldConfig.boost() * wordGramConfiguration.getFourGramFactor()) + .analyzer(WORD_GRAM_4_ANALYZER) + .hasKeywordSubfield(true) + .hasDelimitedSubfield(true) + .hasWordGramSubfields(true) + .isQueryByDefault(true) + .build()); + } + private Set getStandardFields(@Nonnull EntitySpec entitySpec) { Set fields = new HashSet<>(); @@ -616,4 +641,53 @@ public float getWordGramFactor(String fieldName) { } throw new IllegalArgumentException(fieldName + " does not end with Grams[2-4]"); } + + // visible for unit test + public Map getAllFieldTypeFromSearchableRef( + SearchableRefFieldSpec refFieldSpec, + int depth, + EntityRegistry entityRegistry, + String prefixField) { + final SearchableRefAnnotation searchableRefAnnotation = + refFieldSpec.getSearchableRefAnnotation(); + // contains fieldName as key and SearchableAnnotation as value + Map fieldNameMap = new HashMap<>(); + EntitySpec refEntitySpec = entityRegistry.getEntitySpec(searchableRefAnnotation.getRefType()); + String fieldName = searchableRefAnnotation.getFieldName(); + final SearchableAnnotation.FieldType fieldType = searchableRefAnnotation.getFieldType(); + if (!prefixField.isEmpty()) { + fieldName = prefixField + "." + fieldName; + } + + if (depth == 0) { + // at depth 0 only URN is present then add and return + fieldNameMap.put(fieldName, fieldType); + return fieldNameMap; + } + String urnFieldName = fieldName + ".urn"; + fieldNameMap.put(urnFieldName, SearchableAnnotation.FieldType.URN); + List aspectSpecs = refEntitySpec.getAspectSpecs(); + for (AspectSpec aspectSpec : aspectSpecs) { + if (!SKIP_REFERENCE_ASPECT.contains(aspectSpec.getName())) { + for (SearchableFieldSpec searchableFieldSpec : aspectSpec.getSearchableFieldSpecs()) { + String refFieldName = searchableFieldSpec.getSearchableAnnotation().getFieldName(); + refFieldName = fieldName + "." + refFieldName; + final SearchableAnnotation searchableAnnotation = + searchableFieldSpec.getSearchableAnnotation(); + final SearchableAnnotation.FieldType refFieldType = searchableAnnotation.getFieldType(); + fieldNameMap.put(refFieldName, refFieldType); + } + + for (SearchableRefFieldSpec searchableRefFieldSpec : + aspectSpec.getSearchableRefFieldSpecs()) { + String refFieldName = searchableRefFieldSpec.getSearchableRefAnnotation().getFieldName(); + refFieldName = fieldName + "." + refFieldName; + fieldNameMap.putAll( + getAllFieldTypeFromSearchableRef( + searchableRefFieldSpec, depth - 1, entityRegistry, refFieldName)); + } + } + } + return fieldNameMap; + } } From f3946572fa8a3b4f62d0d9fae61d6f63bafe0052 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Thu, 7 Mar 2024 15:49:52 +0530 Subject: [PATCH 32/50] fix(ui): business-attribute: institutional memory support --- .../mappers/BusinessAttributeMapper.java | 9 +++++++++ datahub-web-react/src/graphql/businessAttribute.graphql | 3 +++ .../src/main/java/com/linkedin/metadata/Constants.java | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java index 59815900e1dffd..1c5c2e7eb14d65 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -1,9 +1,11 @@ package com.linkedin.datahub.graphql.types.businessattribute.mappers; import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME; +import static com.linkedin.metadata.Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME; import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME; import com.linkedin.businessattribute.BusinessAttributeInfo; +import com.linkedin.common.InstitutionalMemory; import com.linkedin.common.Ownership; import com.linkedin.common.urn.Urn; import com.linkedin.data.DataMap; @@ -12,6 +14,7 @@ import com.linkedin.datahub.graphql.generated.SchemaFieldDataType; import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper; import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper; +import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper; @@ -46,6 +49,12 @@ public BusinessAttribute apply(@Nonnull final EntityResponse entityResponse) { (businessAttribute, dataMap) -> businessAttribute.setOwnership( OwnershipMapper.map(new Ownership(dataMap), entityResponse.getUrn()))); + mappingHelper.mapToResult( + INSTITUTIONAL_MEMORY_ASPECT_NAME, + (dataset, dataMap) -> + dataset.setInstitutionalMemory( + InstitutionalMemoryMapper.map( + new InstitutionalMemory(dataMap), entityResponse.getUrn()))); return mappingHelper.getResult(); } diff --git a/datahub-web-react/src/graphql/businessAttribute.graphql b/datahub-web-react/src/graphql/businessAttribute.graphql index c58b2cd8451a5a..544a5083d1f2bc 100644 --- a/datahub-web-react/src/graphql/businessAttribute.graphql +++ b/datahub-web-react/src/graphql/businessAttribute.graphql @@ -62,6 +62,9 @@ fragment businessAttributeFields on BusinessAttribute { } } } + institutionalMemory { + ...institutionalMemoryFields + } } mutation createBusinessAttribute($input: CreateBusinessAttributeInput!) { diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index 33cc1f028258e2..3fb95996f53544 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -2,7 +2,6 @@ import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; -import java.util.Arrays; import java.util.List; /** Static class containing commonly-used constants across DataHub services. */ @@ -378,7 +377,8 @@ public class Constants { public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; - public static final List SKIP_REFERENCE_ASPECT = List.of("ownership", "status", "institutionalMemory"); + public static final List SKIP_REFERENCE_ASPECT = + List.of("ownership", "status", "institutionalMemory"); // Posts public static final String POST_INFO_ASPECT_NAME = "postInfo"; From 45629788746c184317e5c30cae73cd2aa72c112f Mon Sep 17 00:00:00 2001 From: "Singh, Himanshu" Date: Tue, 19 Mar 2024 19:30:29 +0530 Subject: [PATCH 33/50] Fix : Business Attribute SearchableRef Search Depth --- .../elasticsearch/indexbuilder/MappingsBuilder.java | 8 +++++++- .../elasticsearch/query/request/SearchFieldConfig.java | 4 +++- .../elasticsearch/query/request/SearchQueryBuilder.java | 4 +++- .../search/transformer/SearchDocumentTransformer.java | 5 +++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java index 8b2eb5a7817011..8696d5a1d557c8 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/indexbuilder/MappingsBuilder.java @@ -332,7 +332,13 @@ private static Map getMappingForSearchableRefField( .forEach( entitySearchableRefFieldSpec -> mappingForField.putAll( - getMappingForSearchableRefField(entitySearchableRefFieldSpec, depth - 1))); + getMappingForSearchableRefField( + entitySearchableRefFieldSpec, + Math.min( + depth - 1, + entitySearchableRefFieldSpec + .getSearchableRefAnnotation() + .getDepth())))); mappingForField.put("urn", getMappingsForUrn()); mappingForProperty.put("properties", mappingForField); mappings.put( diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java index a7ba78230e9def..7415c6e5ce5aa6 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchFieldConfig.java @@ -145,12 +145,14 @@ public static Set detectSubFieldType( aspectSpec.getSearchableRefFieldSpecs()) { String refFieldName = searchableRefFieldSpec.getSearchableRefAnnotation().getFieldName(); refFieldName = fieldName + "." + refFieldName; + int newDepth = + Math.min(depth - 1, searchableRefFieldSpec.getSearchableRefAnnotation().getDepth()); final float refBoost = (float) searchableRefFieldSpec.getSearchableRefAnnotation().getBoostScore() * boostScore; fieldConfigs.addAll( detectSubFieldType( - searchableRefFieldSpec, depth - 1, entityRegistry, refBoost, refFieldName)); + searchableRefFieldSpec, newDepth, entityRegistry, refBoost, refFieldName)); } } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java index 52067003eef149..1aa92b71b7a529 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/elasticsearch/query/request/SearchQueryBuilder.java @@ -682,9 +682,11 @@ public Map getAllFieldTypeFromSearchable aspectSpec.getSearchableRefFieldSpecs()) { String refFieldName = searchableRefFieldSpec.getSearchableRefAnnotation().getFieldName(); refFieldName = fieldName + "." + refFieldName; + int newDepth = + Math.min(depth - 1, searchableRefFieldSpec.getSearchableRefAnnotation().getDepth()); fieldNameMap.putAll( getAllFieldTypeFromSearchableRef( - searchableRefFieldSpec, depth - 1, entityRegistry, refFieldName)); + searchableRefFieldSpec, newDepth, entityRegistry, refFieldName)); } } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java index 1f9487be28e919..9651198bb79848 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java @@ -502,6 +502,7 @@ private Optional getNodeForRef( String fieldName = spec.getSearchableRefAnnotation().getFieldName(); boolean isArray = spec.isArray(); if (!value.isEmpty()) { + int newDepth = Math.min(depth - 1, spec.getSearchableRefAnnotation().getDepth()); if (isArray) { ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode(); value @@ -509,7 +510,7 @@ private Optional getNodeForRef( .forEach( val -> getNodeForRef( - depth - 1, + newDepth, val, spec.getSearchableRefAnnotation().getFieldType()) .ifPresent(arrayNode::add)); @@ -517,7 +518,7 @@ private Optional getNodeForRef( } else { Optional node = getNodeForRef( - depth - 1, + newDepth, value.get(0), spec.getSearchableRefAnnotation().getFieldType()); if (node.isPresent()) { From a1294bf9fabebc664f68f2c59e83f06ba10a30b5 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Wed, 20 Mar 2024 00:06:23 +0530 Subject: [PATCH 34/50] business-attributes: review comments - business attributes lives in schemafield entity --- .../datahub/graphql/GmsGraphQLEngine.java | 5 +- .../AddBusinessAttributeResolver.java | 138 ++++----- .../BusinessAttributeAuthorizationUtils.java | 20 -- .../RemoveBusinessAttributeResolver.java | 99 ++---- .../mappers/BusinessAttributesMapper.java | 33 +- .../graphql/types/dataset/DatasetType.java | 2 - .../EditableSchemaFieldInfoMapper.java | 9 - .../types/schemafield/SchemaFieldMapper.java | 9 +- .../types/schemafield/SchemaFieldType.java | 3 +- .../src/main/resources/entity.graphql | 15 +- .../AddBusinessAttributeResolverTest.java | 154 +++------- .../RemoveBusinessAttributeResolverTest.java | 131 ++------ .../test/resources/test-entity-registry.yaml | 5 + datahub-web-react/src/graphql/dataset.graphql | 5 - .../src/graphql/fragments.graphql | 5 + .../java/com/linkedin/metadata/Constants.java | 1 + .../common/urn/BusinessAttributeUrn.java | 70 +++++ .../linkedin/common/BusinessAttributeUrn.pdl | 4 + .../SearchDocumentTransformer.java | 6 +- .../metadata/search/utils/ESUtils.java | 3 +- ...ributeAssociationChangeEventGenerator.java | 7 +- ...bleSchemaMetadataChangeEventGenerator.java | 28 -- .../BusinessAttributeAssociation.pdl | 9 +- .../BusinessAttributeInfo.pdl | 5 +- .../BusinessAttributeKey.pdl | 2 +- .../businessattribute/BusinessAttributes.pdl | 29 ++ .../schema/EditableSchemaFieldBase.pdl | 61 ---- .../schema/EditableSchemaFieldInfo.pdl | 73 +++-- .../src/main/resources/entity-registry.yml | 1 + .../com.linkedin.entity.aspects.snapshot.json | 261 ++++++++-------- ...com.linkedin.entity.entities.snapshot.json | 284 +++++++++--------- .../com.linkedin.entity.runs.snapshot.json | 261 ++++++++-------- ...nkedin.operations.operations.snapshot.json | 261 ++++++++-------- ...m.linkedin.platform.platform.snapshot.json | 284 +++++++++--------- .../authorization/PoliciesConfig.java | 9 +- 35 files changed, 1013 insertions(+), 1279 deletions(-) create mode 100644 li-utils/src/main/javaPegasus/com/linkedin/common/urn/BusinessAttributeUrn.java create mode 100644 li-utils/src/main/pegasus/com/linkedin/common/BusinessAttributeUrn.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl delete mode 100644 metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index b7391795df4f2b..96c6262a4bd1da 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -1263,11 +1263,10 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { "deleteBusinessAttribute", new DeleteBusinessAttributeResolver(this.entityClient)) .dataFetcher( - "addBusinessAttribute", - new AddBusinessAttributeResolver(this.entityClient, this.entityService)) + "addBusinessAttribute", new AddBusinessAttributeResolver(this.entityService)) .dataFetcher( "removeBusinessAttribute", - new RemoveBusinessAttributeResolver(this.entityClient, this.entityService))); + new RemoveBusinessAttributeResolver(this.entityService))); } private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java index a213dd224648fe..eb477dff088abe 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolver.java @@ -1,29 +1,25 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; -import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ASPECT; import com.linkedin.businessattribute.BusinessAttributeAssociation; -import com.linkedin.common.AuditStamp; +import com.linkedin.businessattribute.BusinessAttributes; +import com.linkedin.common.urn.BusinessAttributeUrn; import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; -import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.ResourceRefInput; -import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils; -import com.linkedin.entity.client.EntityClient; -import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.entity.EntityUtils; import com.linkedin.mxe.MetadataChangeProposal; -import com.linkedin.r2.RemoteInvocationException; -import com.linkedin.schema.EditableSchemaFieldInfo; -import com.linkedin.schema.EditableSchemaMetadata; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -31,99 +27,83 @@ @Slf4j @RequiredArgsConstructor public class AddBusinessAttributeResolver implements DataFetcher> { - private final EntityClient _entityClient; - private final EntityService _entityService; + private final EntityService entityService; @Override public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { final QueryContext context = environment.getContext(); - AddBusinessAttributeInput input = + final AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); - Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); - ResourceRefInput resourceRefInput = input.getResourceUrn(); - if (!isAuthorizeToUpdateDataset( - context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { - throw new AuthorizationException( - "Unauthorized to perform this action. Please contact your DataHub administrator."); - } - if (!_entityClient.exists(businessAttributeUrn, context.getAuthentication())) { - throw new RuntimeException( - String.format("This urn does not exist: %s", businessAttributeUrn)); - } + final Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); + final List resourceRefInputs = input.getResourceUrn(); + validateBusinessAttribute(businessAttributeUrn); return CompletableFuture.supplyAsync( () -> { try { - validateInputResource(resourceRefInput); - addBusinessAttribute(businessAttributeUrn, resourceRefInput, context); + addBusinessAttributeToResource( + businessAttributeUrn, + resourceRefInputs, + UrnUtils.getUrn(context.getActorUrn()), + entityService); return true; } catch (Exception e) { + log.error( + String.format( + "Failed to add Business Attribute %s to resources %s", + businessAttributeUrn, resourceRefInputs)); throw new RuntimeException( String.format( - "Failed to add Business Attribute with urn %s to dataset with urn %s", - businessAttributeUrn, resourceRefInput.getResourceUrn()), + "Failed to add Business Attribute %s to resources %s", + businessAttributeUrn, resourceRefInputs), e); } }); } - private void validateInputResource(ResourceRefInput resource) { - final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); - LabelUtils.validateResource( - resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); + private void validateBusinessAttribute(Urn businessAttributeUrn) { + if (!entityService.exists(businessAttributeUrn, true)) { + throw new IllegalArgumentException( + String.format("This urn does not exist: %s", businessAttributeUrn)); + } } - private void addBusinessAttribute( - Urn businessAttributeUrn, ResourceRefInput resourceRefInput, QueryContext context) - throws RemoteInvocationException { - _entityClient.ingestProposal( - buildAddBusinessAttributeToSubresourceProposal( - businessAttributeUrn, resourceRefInput, context), - context.getAuthentication()); + private void addBusinessAttributeToResource( + Urn businessAttributeUrn, + List resourceRefInputs, + Urn actorUrn, + EntityService entityService) + throws URISyntaxException { + List proposals = new ArrayList<>(); + for (ResourceRefInput resourceRefInput : resourceRefInputs) { + proposals.add( + buildAddBusinessAttributeToEntityProposal( + businessAttributeUrn, resourceRefInput, entityService, actorUrn)); + } + EntityUtils.ingestChangeProposals(proposals, entityService, actorUrn, false); } - private MetadataChangeProposal buildAddBusinessAttributeToSubresourceProposal( - Urn businessAttributeUrn, ResourceRefInput resource, QueryContext context) { - com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = - (com.linkedin.schema.EditableSchemaMetadata) + private MetadataChangeProposal buildAddBusinessAttributeToEntityProposal( + Urn businessAttributeUrn, + ResourceRefInput resource, + EntityService entityService, + Urn actorUrn) + throws URISyntaxException { + BusinessAttributes businessAttributes = + (BusinessAttributes) EntityUtils.getAspectFromEntity( resource.getResourceUrn(), - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - _entityService, - new EditableSchemaMetadata()); - - EditableSchemaFieldInfo editableFieldInfo = - getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); - - if (editableFieldInfo == null) { - throw new IllegalArgumentException( - String.format( - "Subresource %s does not exist in dataset %s", - resource.getSubResource(), resource.getResourceUrn())); + BUSINESS_ATTRIBUTE_ASPECT, + entityService, + new BusinessAttributes()); + if (!businessAttributes.hasBusinessAttribute()) { + businessAttributes.setBusinessAttribute(new BusinessAttributeAssociation()); } - - if (editableFieldInfo.hasBusinessAttribute()) { - throw new RuntimeException( - String.format("Schema field has already attached with business attribute")); - } - editableFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); - addBusinessAttribute( - editableFieldInfo.getBusinessAttribute(), - businessAttributeUrn, - UrnUtils.getUrn(context.getActorUrn())); + BusinessAttributeAssociation businessAttributeAssociation = + businessAttributes.getBusinessAttribute(); + businessAttributeAssociation.setBusinessAttributeUrn( + BusinessAttributeUrn.createFromUrn(businessAttributeUrn)); + businessAttributes.setBusinessAttribute(businessAttributeAssociation); return buildMetadataChangeProposalWithUrn( - UrnUtils.getUrn(resource.getResourceUrn()), - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - editableSchemaMetadata); - } - - private void addBusinessAttribute( - BusinessAttributeAssociation businessAttributeAssociation, - Urn businessAttributeUrn, - Urn actorUrn) { - businessAttributeAssociation.setDestinationUrn(businessAttributeUrn); - AuditStamp nowAuditStamp = - new AuditStamp().setTime(System.currentTimeMillis()).setActor(actorUrn); - businessAttributeAssociation.setCreated(nowAuditStamp); - businessAttributeAssociation.setLastModified(nowAuditStamp); + UrnUtils.getUrn(resource.getResourceUrn()), BUSINESS_ATTRIBUTE_ASPECT, businessAttributes); } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java index b545c08a622e3e..c5ac56a13040b6 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java @@ -1,11 +1,8 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; -import static com.linkedin.datahub.graphql.resolvers.AuthUtils.ALL_PRIVILEGES_GROUP; - import com.datahub.authorization.ConjunctivePrivilegeGroup; import com.datahub.authorization.DisjunctivePrivilegeGroup; 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.metadata.authorization.PoliciesConfig; @@ -37,21 +34,4 @@ public static boolean canManageBusinessAttribute(@Nonnull QueryContext context) return AuthorizationUtils.isAuthorized( context.getAuthorizer(), context.getActorUrn(), orPrivilegeGroups); } - - public static boolean isAuthorizeToUpdateDataset(QueryContext context, Urn targetUrn) { - final DisjunctivePrivilegeGroup orPrivilegeGroups = - new DisjunctivePrivilegeGroup( - ImmutableList.of( - ALL_PRIVILEGES_GROUP, - new ConjunctivePrivilegeGroup( - ImmutableList.of( - PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())))); - - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), - context.getActorUrn(), - targetUrn.getEntityType(), - targetUrn.toString(), - orPrivilegeGroups); - } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java index a434bb11afd4f0..63e9ac562a6586 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolver.java @@ -1,27 +1,22 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; -import static com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils.isAuthorizeToUpdateDataset; import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.buildMetadataChangeProposalWithUrn; -import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getFieldInfoFromSchema; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ASPECT; +import com.linkedin.businessattribute.BusinessAttributes; import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; -import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.ResourceRefInput; -import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils; -import com.linkedin.entity.client.EntityClient; -import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.entity.EntityUtils; import com.linkedin.mxe.MetadataChangeProposal; -import com.linkedin.r2.RemoteInvocationException; -import com.linkedin.schema.EditableSchemaFieldInfo; -import com.linkedin.schema.EditableSchemaMetadata; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,87 +24,59 @@ @Slf4j @RequiredArgsConstructor public class RemoveBusinessAttributeResolver implements DataFetcher> { - private final EntityClient _entityClient; - private final EntityService _entityService; + private final EntityService entityService; @Override public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { final QueryContext context = environment.getContext(); - AddBusinessAttributeInput input = + final AddBusinessAttributeInput input = bindArgument(environment.getArgument("input"), AddBusinessAttributeInput.class); - Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); - ResourceRefInput resourceRefInput = input.getResourceUrn(); - if (!isAuthorizeToUpdateDataset( - context, Urn.createFromString(resourceRefInput.getResourceUrn()))) { - throw new AuthorizationException( - "Unauthorized to perform this action. Please contact your DataHub administrator."); - } + final Urn businessAttributeUrn = UrnUtils.getUrn(input.getBusinessAttributeUrn()); + final List resourceRefInputs = input.getResourceUrn(); + return CompletableFuture.supplyAsync( () -> { try { - if (!businessAttributeUrn.getEntityType().equals("businessAttribute")) { - log.error( - "Failed to remove {}. It is not a business attribute urn.", - businessAttributeUrn.toString()); - return false; - } - - validateInputResource(resourceRefInput, context); - - removeBusinessAttribute(resourceRefInput, context); - + removeBusinessAttribute(resourceRefInputs, UrnUtils.getUrn(context.getActorUrn())); return true; } catch (Exception e) { + log.error( + String.format( + "Failed to remove Business Attribute with urn %s from resources %s", + businessAttributeUrn, resourceRefInputs)); throw new RuntimeException( String.format( - "Failed to remove Business Attribute with urn %s to dataset with urn %s", - businessAttributeUrn, resourceRefInput.getResourceUrn()), + "Failed to remove Business Attribute with urn %s from resources %s", + businessAttributeUrn, resourceRefInputs), e); } }); } - private void validateInputResource(ResourceRefInput resource, QueryContext context) { - final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn()); - LabelUtils.validateResource( - resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService); - } - - private void removeBusinessAttribute(ResourceRefInput resourceRefInput, QueryContext context) - throws RemoteInvocationException { - _entityClient.ingestProposal( - buildRemoveBusinessAttributeToSubresourceProposal(resourceRefInput), - context.getAuthentication()); + private void removeBusinessAttribute(List resourceRefInputs, Urn actorUrn) { + List proposals = new ArrayList<>(); + for (ResourceRefInput resourceRefInput : resourceRefInputs) { + proposals.add( + buildRemoveBusinessAttributeFromResourceProposal(resourceRefInput, entityService)); + } + EntityUtils.ingestChangeProposals(proposals, entityService, actorUrn, false); } - private MetadataChangeProposal buildRemoveBusinessAttributeToSubresourceProposal( - ResourceRefInput resource) { - com.linkedin.schema.EditableSchemaMetadata editableSchemaMetadata = - (com.linkedin.schema.EditableSchemaMetadata) + private MetadataChangeProposal buildRemoveBusinessAttributeFromResourceProposal( + ResourceRefInput resource, EntityService entityService) { + BusinessAttributes businessAttributes = + (BusinessAttributes) EntityUtils.getAspectFromEntity( resource.getResourceUrn(), - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - _entityService, - new EditableSchemaMetadata()); - - EditableSchemaFieldInfo editableFieldInfo = - getFieldInfoFromSchema(editableSchemaMetadata, resource.getSubResource()); - - if (editableFieldInfo == null) { - throw new IllegalArgumentException( - String.format( - "Subresource %s does not exist in dataset %s", - resource.getSubResource(), resource.getResourceUrn())); - } - - if (!editableFieldInfo.hasBusinessAttribute()) { + BUSINESS_ATTRIBUTE_ASPECT, + entityService, + new BusinessAttributes()); + if (!businessAttributes.hasBusinessAttribute()) { throw new RuntimeException( String.format("Schema field has not attached with business attribute")); } - editableFieldInfo.removeBusinessAttribute(); + businessAttributes.removeBusinessAttribute(); return buildMetadataChangeProposalWithUrn( - UrnUtils.getUrn(resource.getResourceUrn()), - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - editableSchemaMetadata); + UrnUtils.getUrn(resource.getResourceUrn()), BUSINESS_ATTRIBUTE_ASPECT, businessAttributes); } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java index c374d6a99aedb1..104bc6ecd9222b 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributesMapper.java @@ -5,6 +5,7 @@ import com.linkedin.datahub.graphql.generated.BusinessAttributeAssociation; import com.linkedin.datahub.graphql.generated.BusinessAttributes; import com.linkedin.datahub.graphql.generated.EntityType; +import java.util.Objects; import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,25 +17,33 @@ public class BusinessAttributesMapper { public static final BusinessAttributesMapper INSTANCE = new BusinessAttributesMapper(); public static BusinessAttributes map( - @Nonnull final com.linkedin.businessattribute.BusinessAttributeAssociation businessAttribute, + @Nonnull final com.linkedin.businessattribute.BusinessAttributes businessAttributes, @Nonnull final Urn entityUrn) { - return INSTANCE.apply(businessAttribute, entityUrn); + return INSTANCE.apply(businessAttributes, entityUrn); } private BusinessAttributes apply( - @Nonnull com.linkedin.businessattribute.BusinessAttributeAssociation businessAttributes, + @Nonnull com.linkedin.businessattribute.BusinessAttributes businessAttributes, @Nonnull Urn entityUrn) { - final BusinessAttributeAssociation businessAttributeAssociation = - new BusinessAttributeAssociation(); final BusinessAttributes result = new BusinessAttributes(); + result.setBusinessAttribute( + mapBusinessAttributeAssociation(businessAttributes.getBusinessAttribute(), entityUrn)); + return result; + } + + private BusinessAttributeAssociation mapBusinessAttributeAssociation( + com.linkedin.businessattribute.BusinessAttributeAssociation businessAttributeAssociation, + Urn entityUrn) { + if (Objects.isNull(businessAttributeAssociation)) { + return null; + } + final BusinessAttributeAssociation businessAttributeAssociationResult = + new BusinessAttributeAssociation(); final BusinessAttribute businessAttribute = new BusinessAttribute(); - businessAttribute.setUrn(businessAttributes.getDestinationUrn().toString()); + businessAttribute.setUrn(businessAttributeAssociation.getBusinessAttributeUrn().toString()); businessAttribute.setType(EntityType.BUSINESS_ATTRIBUTE); - - businessAttributeAssociation.setBusinessAttribute(businessAttribute); - - businessAttributeAssociation.setAssociatedUrn(entityUrn.toString()); - result.setBusinessAttribute(businessAttributeAssociation); - return result; + businessAttributeAssociationResult.setBusinessAttribute(businessAttribute); + businessAttributeAssociationResult.setAssociatedUrn(entityUrn.toString()); + return businessAttributeAssociationResult; } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java index 3c6d5cd9c07159..0ae41eef6b1b1e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java @@ -316,8 +316,6 @@ private DisjunctivePrivilegeGroup getAuthorizedPrivileges(final DatasetUpdateInp if (updateInput.getEditableSchemaMetadata() != null) { specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_TAGS_PRIVILEGE.getType()); specificPrivileges.add(PoliciesConfig.EDIT_DATASET_COL_DESCRIPTION_PRIVILEGE.getType()); - specificPrivileges.add( - PoliciesConfig.EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE.getType()); } final ConjunctivePrivilegeGroup specificPrivilegeGroup = diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java index c452316894f2ba..f54adbe8ba26c6 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/EditableSchemaFieldInfoMapper.java @@ -1,17 +1,12 @@ package com.linkedin.datahub.graphql.types.dataset.mappers; import com.linkedin.common.urn.Urn; -import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributesMapper; import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper; import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper; import com.linkedin.schema.EditableSchemaFieldInfo; import javax.annotation.Nonnull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class EditableSchemaFieldInfoMapper { - private static final Logger _logger = - LoggerFactory.getLogger(EditableSchemaFieldInfoMapper.class.getName()); public static final EditableSchemaFieldInfoMapper INSTANCE = new EditableSchemaFieldInfoMapper(); @@ -37,10 +32,6 @@ public com.linkedin.datahub.graphql.generated.EditableSchemaFieldInfo apply( if (input.hasGlossaryTerms()) { result.setGlossaryTerms(GlossaryTermsMapper.map(input.getGlossaryTerms(), entityUrn)); } - if (input.hasBusinessAttribute()) { - result.setBusinessAttributes( - BusinessAttributesMapper.map(input.getBusinessAttribute(), entityUrn)); - } return result; } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java index 254a1ed1767f17..7e145e4ba650eb 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java @@ -1,10 +1,13 @@ package com.linkedin.datahub.graphql.types.schemafield; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ASPECT; import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTIES_ASPECT_NAME; +import com.linkedin.businessattribute.BusinessAttributes; import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.SchemaFieldEntity; +import com.linkedin.datahub.graphql.types.businessattribute.mappers.BusinessAttributesMapper; import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper; import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; import com.linkedin.datahub.graphql.types.mappers.ModelMapper; @@ -34,7 +37,11 @@ public SchemaFieldEntity apply(@Nonnull final EntityResponse entityResponse) { ((schemaField, dataMap) -> schemaField.setStructuredProperties( StructuredPropertiesMapper.map(new StructuredProperties(dataMap))))); - + mappingHelper.mapToResult( + BUSINESS_ATTRIBUTE_ASPECT, + (((schemaField, dataMap) -> + schemaField.setBusinessAttributes( + BusinessAttributesMapper.map(new BusinessAttributes(dataMap), entityUrn))))); return result; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldType.java index 9f14bf52733ea9..04b3567df41b60 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldType.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.types.schemafield; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ASPECT; import static com.linkedin.metadata.Constants.SCHEMA_FIELD_ENTITY_NAME; import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTIES_ASPECT_NAME; @@ -31,7 +32,7 @@ public class SchemaFieldType implements com.linkedin.datahub.graphql.types.EntityType { public static final Set ASPECTS_TO_FETCH = - ImmutableSet.of(STRUCTURED_PROPERTIES_ASPECT_NAME); + ImmutableSet.of(STRUCTURED_PROPERTIES_ASPECT_NAME, BUSINESS_ATTRIBUTE_ASPECT); private final EntityClient _entityClient; private final FeatureFlags _featureFlags; diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 064a52f1e18a64..f3366f74c9fdb1 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -3016,6 +3016,11 @@ type SchemaFieldEntity implements Entity { Granular API for querying edges extending from this entity """ relationships(input: RelationshipsInput!): EntityRelationshipsResult + + """ + Business Attribute associated with the field + """ + businessAttributes: BusinessAttributes } """ @@ -3149,10 +3154,6 @@ type EditableSchemaFieldInfo { """ glossaryTerms: GlossaryTerms - """ - Business Attribute associated with the field - """ - businessAttributes: BusinessAttributes } """ @@ -12078,12 +12079,12 @@ input AddBusinessAttributeInput { """ The urn of the business attribute to add """ - businessAttributeUrn: String + businessAttributeUrn: String! """ resource urns to add the business attribute to """ - resourceUrn: ResourceRefInput! + resourceUrn: [ResourceRefInput!]! } """ @@ -12093,7 +12094,7 @@ type BusinessAttributes { """ Business Attribute attached to the Metadata Entity """ - businessAttribute: BusinessAttributeAssociation! + businessAttribute: BusinessAttributeAssociation } """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java index 9fcda136d2d057..f787879b7a0a60 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/AddBusinessAttributeResolverTest.java @@ -2,29 +2,23 @@ import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.mockito.ArgumentMatchers.eq; import static org.testng.Assert.assertTrue; import static org.testng.Assert.expectThrows; -import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableList; import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.businessattribute.BusinessAttributes; +import com.linkedin.common.urn.BusinessAttributeUrn; import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.ResourceRefInput; -import com.linkedin.datahub.graphql.generated.SubResourceType; -import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.entity.EntityUtils; -import com.linkedin.mxe.MetadataChangeProposal; -import com.linkedin.schema.EditableSchemaFieldInfo; -import com.linkedin.schema.EditableSchemaFieldInfoArray; -import com.linkedin.schema.EditableSchemaMetadata; -import com.linkedin.schema.SchemaField; -import com.linkedin.schema.SchemaFieldArray; -import com.linkedin.schema.SchemaMetadata; +import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl; import graphql.schema.DataFetchingEnvironment; -import java.util.concurrent.ExecutionException; +import java.net.URISyntaxException; import org.mockito.Mockito; import org.testng.annotations.Test; @@ -32,24 +26,18 @@ public class AddBusinessAttributeResolverTest { private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; private static final String RESOURCE_URN = - "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; - private static final String SUB_RESOURCE = "name"; - private EntityClient mockClient; + "urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD),field_bar)"; private EntityService mockService; private QueryContext mockContext; private DataFetchingEnvironment mockEnv; - private Authentication mockAuthentication; private void init() { - mockClient = Mockito.mock(EntityClient.class); mockService = getMockEntityService(); mockEnv = Mockito.mock(DataFetchingEnvironment.class); - mockAuthentication = Mockito.mock(Authentication.class); } private void setupAllowContext() { mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); Mockito.when(mockEnv.getContext()).thenReturn(mockContext); } @@ -57,121 +45,67 @@ private void setupAllowContext() { public void testSuccess() throws Exception { init(); setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when( - mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + Mockito.when(mockService.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), true)) .thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + Mockito.when( mockService.getAspect( - Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); + Urn.createFromString(RESOURCE_URN), Constants.BUSINESS_ATTRIBUTE_ASPECT, 0)) + .thenReturn(new BusinessAttributes()); AddBusinessAttributeResolver addBusinessAttributeResolver = - new AddBusinessAttributeResolver(mockClient, mockService); + new AddBusinessAttributeResolver(mockService); addBusinessAttributeResolver.get(mockEnv).get(); - Mockito.verify(mockClient, Mockito.times(1)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + Mockito.verify(mockService, Mockito.times(1)) + .ingestProposal(Mockito.any(AspectsBatchImpl.class), eq(false)); } @Test public void testBusinessAttributeAlreadyAdded() throws Exception { init(); setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when( - mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + Mockito.when(mockService.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), true)) .thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); Mockito.when( mockService.getAspect( - Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - Mockito.when( - EntityUtils.getAspectFromEntity( - RESOURCE_URN, Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, mockService, null)) - .thenReturn(editableSchemaMetadata()); + Urn.createFromString(RESOURCE_URN), Constants.BUSINESS_ATTRIBUTE_ASPECT, 0)) + .thenReturn(businessAttributes()); AddBusinessAttributeResolver addBusinessAttributeResolver = - new AddBusinessAttributeResolver(mockClient, mockService); - ExecutionException exception = - expectThrows( - ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); - assertTrue( - exception - .getCause() - .getMessage() - .equals( - String.format( - "Failed to add Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); + new AddBusinessAttributeResolver(mockService); + addBusinessAttributeResolver.get(mockEnv).get(); - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + Mockito.verify(mockService, Mockito.times(1)) + .ingestProposal(Mockito.any(AspectsBatchImpl.class), eq(false)); } @Test public void testBusinessAttributeNotExists() throws Exception { init(); setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when( - mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) + Mockito.when(mockService.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), true)) .thenReturn(false); Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); - Mockito.when( - mockService.getAspect( - Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); AddBusinessAttributeResolver addBusinessAttributeResolver = - new AddBusinessAttributeResolver(mockClient, mockService); + new AddBusinessAttributeResolver(mockService); RuntimeException exception = expectThrows(RuntimeException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); assertTrue( exception .getMessage() .equals(String.format("This urn does not exist: %s", BUSINESS_ATTRIBUTE_URN))); - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); - } - - @Test - public void testResourceNotExists() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when( - mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) - .thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(false); - Mockito.when( - mockService.getAspect( - Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - - AddBusinessAttributeResolver addBusinessAttributeResolver = - new AddBusinessAttributeResolver(mockClient, mockService); - ExecutionException exception = - expectThrows( - ExecutionException.class, () -> addBusinessAttributeResolver.get(mockEnv).get()); - assertTrue( - exception - .getCause() - .getMessage() - .equals( - String.format( - "Failed to add Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); - - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + Mockito.verify(mockService, Mockito.times(0)) + .ingestProposal(Mockito.any(AspectsBatchImpl.class), eq(false)); } - @Test - public void testNotAuthorized() throws Exception {} - public AddBusinessAttributeInput addBusinessAttributeInput() { AddBusinessAttributeInput addBusinessAttributeInput = new AddBusinessAttributeInput(); addBusinessAttributeInput.setBusinessAttributeUrn(BUSINESS_ATTRIBUTE_URN); @@ -179,32 +113,18 @@ public AddBusinessAttributeInput addBusinessAttributeInput() { return addBusinessAttributeInput; } - private ResourceRefInput resourceRefInput() { + private ImmutableList resourceRefInput() { ResourceRefInput resourceRefInput = new ResourceRefInput(); resourceRefInput.setResourceUrn(RESOURCE_URN); - resourceRefInput.setSubResource(SUB_RESOURCE); - resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); - return resourceRefInput; - } - - private SchemaMetadata schemaMetadata() { - SchemaMetadata schemaMetadata = new SchemaMetadata(); - SchemaFieldArray schemaFields = new SchemaFieldArray(); - SchemaField schemaField = new SchemaField(); - schemaField.setFieldPath(SUB_RESOURCE); - schemaFields.add(schemaField); - schemaMetadata.setFields(schemaFields); - return schemaMetadata; + return ImmutableList.of(resourceRefInput); } - private EditableSchemaMetadata editableSchemaMetadata() { - EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); - EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); - EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); - editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); - editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); - editableSchemaFieldInfos.add(editableSchemaFieldInfo); - editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); - return editableSchemaMetadata; + private BusinessAttributes businessAttributes() throws URISyntaxException { + BusinessAttributes businessAttributes = new BusinessAttributes(); + BusinessAttributeAssociation businessAttributeAssociation = new BusinessAttributeAssociation(); + businessAttributeAssociation.setBusinessAttributeUrn( + BusinessAttributeUrn.createFromString(BUSINESS_ATTRIBUTE_URN)); + businessAttributes.setBusinessAttribute(businessAttributeAssociation); + return businessAttributes; } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java index b0b53c2d772136..78909a6910c13b 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/RemoveBusinessAttributeResolverTest.java @@ -2,28 +2,23 @@ import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; import static com.linkedin.datahub.graphql.TestUtils.getMockEntityService; +import static org.mockito.ArgumentMatchers.eq; import static org.testng.Assert.assertTrue; import static org.testng.Assert.expectThrows; -import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableList; import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.businessattribute.BusinessAttributes; +import com.linkedin.common.urn.BusinessAttributeUrn; import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.AddBusinessAttributeInput; import com.linkedin.datahub.graphql.generated.ResourceRefInput; -import com.linkedin.datahub.graphql.generated.SubResourceType; -import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.entity.EntityUtils; -import com.linkedin.mxe.MetadataChangeProposal; -import com.linkedin.schema.EditableSchemaFieldInfo; -import com.linkedin.schema.EditableSchemaFieldInfoArray; -import com.linkedin.schema.EditableSchemaMetadata; -import com.linkedin.schema.SchemaField; -import com.linkedin.schema.SchemaFieldArray; -import com.linkedin.schema.SchemaMetadata; +import com.linkedin.metadata.entity.ebean.batch.AspectsBatchImpl; import graphql.schema.DataFetchingEnvironment; +import java.net.URISyntaxException; import java.util.concurrent.ExecutionException; import org.mockito.Mockito; import org.testng.annotations.Test; @@ -32,24 +27,18 @@ public class RemoveBusinessAttributeResolverTest { private static final String BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; private static final String RESOURCE_URN = - "urn:li:dataset:(urn:li:dataPlatform:postgres,postgres.auth,PROD)"; - private static final String SUB_RESOURCE = "name"; - private EntityClient mockClient; + "urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD),field_bar)"; private EntityService mockService; private QueryContext mockContext; private DataFetchingEnvironment mockEnv; - private Authentication mockAuthentication; private void init() { - mockClient = Mockito.mock(EntityClient.class); mockService = getMockEntityService(); mockEnv = Mockito.mock(DataFetchingEnvironment.class); - mockAuthentication = Mockito.mock(Authentication.class); } private void setupAllowContext() { mockContext = getMockAllowContext(); - Mockito.when(mockContext.getAuthentication()).thenReturn(mockAuthentication); Mockito.when(mockEnv.getContext()).thenReturn(mockContext); } @@ -57,44 +46,33 @@ private void setupAllowContext() { public void testSuccess() throws Exception { init(); setupAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when( - mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) - .thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + Mockito.when( mockService.getAspect( - Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - Mockito.when( - EntityUtils.getAspectFromEntity( - RESOURCE_URN, Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, mockService, null)) - .thenReturn(editableSchemaMetadata()); + Urn.createFromString(RESOURCE_URN), Constants.BUSINESS_ATTRIBUTE_ASPECT, 0)) + .thenReturn(businessAttributes()); - RemoveBusinessAttributeResolver resolver = - new RemoveBusinessAttributeResolver(mockClient, mockService); + RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockService); resolver.get(mockEnv).get(); - Mockito.verify(mockClient, Mockito.times(1)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + Mockito.verify(mockService, Mockito.times(1)) + .ingestProposal(Mockito.any(AspectsBatchImpl.class), eq(false)); } @Test public void testBusinessAttributeNotAdded() throws Exception { init(); setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when( - mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) - .thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(true); + AddBusinessAttributeInput input = addBusinessAttributeInput(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input); Mockito.when( mockService.getAspect( - Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); + Urn.createFromString(RESOURCE_URN), Constants.BUSINESS_ATTRIBUTE_ASPECT, 0)) + .thenReturn(new BusinessAttributes()); - RemoveBusinessAttributeResolver resolver = - new RemoveBusinessAttributeResolver(mockClient, mockService); + RemoveBusinessAttributeResolver resolver = new RemoveBusinessAttributeResolver(mockService); ExecutionException actualException = expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); assertTrue( @@ -103,42 +81,11 @@ public void testBusinessAttributeNotAdded() throws Exception { .getMessage() .equals( String.format( - "Failed to remove Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); + "Failed to remove Business Attribute with urn %s from resources %s", + input.getBusinessAttributeUrn(), input.getResourceUrn()))); - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); - } - - @Test - public void testResourceNotExists() throws Exception { - init(); - setupAllowContext(); - Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(addBusinessAttributeInput()); - Mockito.when( - mockClient.exists(Urn.createFromString((BUSINESS_ATTRIBUTE_URN)), mockAuthentication)) - .thenReturn(true); - Mockito.when(mockService.exists(Urn.createFromString(RESOURCE_URN), true)).thenReturn(false); - Mockito.when( - mockService.getAspect( - Urn.createFromString(RESOURCE_URN), Constants.SCHEMA_METADATA_ASPECT_NAME, 0)) - .thenReturn(schemaMetadata()); - - RemoveBusinessAttributeResolver resolver = - new RemoveBusinessAttributeResolver(mockClient, mockService); - ExecutionException exception = - expectThrows(ExecutionException.class, () -> resolver.get(mockEnv).get()); - assertTrue( - exception - .getCause() - .getMessage() - .equals( - String.format( - "Failed to remove Business Attribute with urn %s to dataset with urn %s", - BUSINESS_ATTRIBUTE_URN, RESOURCE_URN))); - - Mockito.verify(mockClient, Mockito.times(0)) - .ingestProposal(Mockito.any(MetadataChangeProposal.class), Mockito.eq(mockAuthentication)); + Mockito.verify(mockService, Mockito.times(0)) + .ingestProposal(Mockito.any(AspectsBatchImpl.class), eq(false)); } public AddBusinessAttributeInput addBusinessAttributeInput() { @@ -148,32 +95,18 @@ public AddBusinessAttributeInput addBusinessAttributeInput() { return addBusinessAttributeInput; } - private ResourceRefInput resourceRefInput() { + private ImmutableList resourceRefInput() { ResourceRefInput resourceRefInput = new ResourceRefInput(); resourceRefInput.setResourceUrn(RESOURCE_URN); - resourceRefInput.setSubResource(SUB_RESOURCE); - resourceRefInput.setSubResourceType(SubResourceType.DATASET_FIELD); - return resourceRefInput; - } - - private SchemaMetadata schemaMetadata() { - SchemaMetadata schemaMetadata = new SchemaMetadata(); - SchemaFieldArray schemaFields = new SchemaFieldArray(); - SchemaField schemaField = new SchemaField(); - schemaField.setFieldPath(SUB_RESOURCE); - schemaFields.add(schemaField); - schemaMetadata.setFields(schemaFields); - return schemaMetadata; + return ImmutableList.of(resourceRefInput); } - private EditableSchemaMetadata editableSchemaMetadata() { - EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); - EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); - EditableSchemaFieldInfo editableSchemaFieldInfo = new EditableSchemaFieldInfo(); - editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); - editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); - editableSchemaFieldInfos.add(editableSchemaFieldInfo); - editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); - return editableSchemaMetadata; + private BusinessAttributes businessAttributes() throws URISyntaxException { + BusinessAttributes businessAttributes = new BusinessAttributes(); + BusinessAttributeAssociation businessAttributeAssociation = new BusinessAttributeAssociation(); + businessAttributeAssociation.setBusinessAttributeUrn( + BusinessAttributeUrn.createFromString(BUSINESS_ATTRIBUTE_URN)); + businessAttributes.setBusinessAttribute(businessAttributeAssociation); + return businessAttributes; } } diff --git a/datahub-graphql-core/src/test/resources/test-entity-registry.yaml b/datahub-graphql-core/src/test/resources/test-entity-registry.yaml index 20142cfbb799c7..4df822377ddf2b 100644 --- a/datahub-graphql-core/src/test/resources/test-entity-registry.yaml +++ b/datahub-graphql-core/src/test/resources/test-entity-registry.yaml @@ -308,4 +308,9 @@ entities: - dataContractProperties - dataContractStatus - status +- name: schemaField + category: core + keyAspect: schemaFieldKey + aspects: + - businessAttributes events: diff --git a/datahub-web-react/src/graphql/dataset.graphql b/datahub-web-react/src/graphql/dataset.graphql index 4dec644cf3711f..989ec31961b57f 100644 --- a/datahub-web-react/src/graphql/dataset.graphql +++ b/datahub-web-react/src/graphql/dataset.graphql @@ -289,11 +289,6 @@ fragment datasetSchema on Dataset { glossaryTerms { ...glossaryTerms } - businessAttributes { - businessAttribute { - ...businessAttribute - } - } } } } diff --git a/datahub-web-react/src/graphql/fragments.graphql b/datahub-web-react/src/graphql/fragments.graphql index 3deffe4f6c50b4..9d18342d9b27de 100644 --- a/datahub-web-react/src/graphql/fragments.graphql +++ b/datahub-web-react/src/graphql/fragments.graphql @@ -731,6 +731,11 @@ fragment schemaFieldFields on SchemaField { ...structuredPropertiesFields } } + businessAttributes { + businessAttribute { + ...businessAttribute + } + } } } diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index 3fb95996f53544..ed9410da2d9c5a 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -377,6 +377,7 @@ public class Constants { public static final String BUSINESS_ATTRIBUTE_KEY_ASPECT_NAME = "businessAttributeKey"; public static final String BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME = "businessAttributeInfo"; public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; + public static final String BUSINESS_ATTRIBUTE_ASPECT = "businessAttributes"; public static final List SKIP_REFERENCE_ASPECT = List.of("ownership", "status", "institutionalMemory"); diff --git a/li-utils/src/main/javaPegasus/com/linkedin/common/urn/BusinessAttributeUrn.java b/li-utils/src/main/javaPegasus/com/linkedin/common/urn/BusinessAttributeUrn.java new file mode 100644 index 00000000000000..31893e95e1a979 --- /dev/null +++ b/li-utils/src/main/javaPegasus/com/linkedin/common/urn/BusinessAttributeUrn.java @@ -0,0 +1,70 @@ +package com.linkedin.common.urn; + +import com.linkedin.data.template.Custom; +import com.linkedin.data.template.DirectCoercer; +import com.linkedin.data.template.TemplateOutputCastException; +import java.net.URISyntaxException; + +public final class BusinessAttributeUrn extends Urn { + + public static final String ENTITY_TYPE = "businessAttribute"; + + private final String _name; + + public BusinessAttributeUrn(String name) { + super(ENTITY_TYPE, TupleKey.create(name)); + this._name = name; + } + + public String getName() { + return _name; + } + + public static BusinessAttributeUrn createFromString(String rawUrn) throws URISyntaxException { + return createFromUrn(Urn.createFromString(rawUrn)); + } + + public static BusinessAttributeUrn createFromUrn(Urn urn) throws URISyntaxException { + if (!"li".equals(urn.getNamespace())) { + throw new URISyntaxException(urn.toString(), "Urn namespace type should be 'li'."); + } else if (!ENTITY_TYPE.equals(urn.getEntityType())) { + throw new URISyntaxException( + urn.toString(), "Urn entity type should be '" + urn.getEntityType() + "'."); + } else { + TupleKey key = urn.getEntityKey(); + if (key.size() != 1) { + throw new URISyntaxException( + urn.toString(), "Invalid number of keys: found " + key.size() + " expected 1."); + } else { + try { + return new BusinessAttributeUrn((String) key.getAs(0, String.class)); + } catch (Exception e) { + throw new URISyntaxException(urn.toString(), "Invalid URN Parameter: '" + e.getMessage()); + } + } + } + } + + public static BusinessAttributeUrn deserialize(String rawUrn) throws URISyntaxException { + return createFromString(rawUrn); + } + + static { + Custom.registerCoercer( + new DirectCoercer() { + public Object coerceInput(BusinessAttributeUrn object) throws ClassCastException { + return object.toString(); + } + + public BusinessAttributeUrn coerceOutput(Object object) + throws TemplateOutputCastException { + try { + return BusinessAttributeUrn.createFromString((String) object); + } catch (URISyntaxException e) { + throw new TemplateOutputCastException("Invalid URN syntax: " + e.getMessage(), e); + } + } + }, + BusinessAttributeUrn.class); + } +} diff --git a/li-utils/src/main/pegasus/com/linkedin/common/BusinessAttributeUrn.pdl b/li-utils/src/main/pegasus/com/linkedin/common/BusinessAttributeUrn.pdl new file mode 100644 index 00000000000000..105fb1fefec21c --- /dev/null +++ b/li-utils/src/main/pegasus/com/linkedin/common/BusinessAttributeUrn.pdl @@ -0,0 +1,4 @@ +namespace com.linkedin.common + +@java.class = "com.linkedin.common.urn.BusinessAttributeUrn" +typeref BusinessAttributeUrn = string \ No newline at end of file diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java index 1f9487be28e919..9c5cc5fc9b2032 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java @@ -109,7 +109,9 @@ public Optional transformAspect( Optional result = Optional.empty(); - if (!extractedSearchableFields.isEmpty() || !extractedSearchScoreFields.isEmpty()) { + if (!extractedSearchableFields.isEmpty() + || !extractedSearchScoreFields.isEmpty() + || !extractedSearchRefFields.isEmpty()) { final ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); searchDocument.put("urn", urn.toString()); extractedSearchableFields.forEach( @@ -442,6 +444,8 @@ public void setSearchableRefValue( String finalFieldName = fieldName; getNodeForRef(depth, fieldValues.get(0), fieldType) .ifPresent(node -> searchDocument.set(finalFieldName, node)); + } else { + searchDocument.set(fieldName, JsonNodeFactory.instance.nullNode()); } } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java index 3d7a87a04e2c88..c45df945937ba5 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESUtils.java @@ -112,8 +112,7 @@ public class ESUtils { put("description", ImmutableList.of("description", "editedDescription")); put( "businessAttribute", - ImmutableList.of( - "editedFieldBusinessAttributeRef", "editedFieldBusinessAttributeRef.urn")); + ImmutableList.of("businessAttributeRef", "businessAttributeRef.urn")); } }; diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java index 03a30c95477ab1..f0369bc4ace136 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/BusinessAttributeAssociationChangeEventGenerator.java @@ -57,13 +57,14 @@ private static ChangeEvent createChangeEvent( AuditStamp auditStamp) { return BusinessAttributeAssociationChangeEvent .entityBusinessAttributeAssociationChangeEventBuilder() - .modifier(association.getDestinationUrn().toString()) + .modifier(association.getBusinessAttributeUrn().toString()) .entityUrn(entityUrn) .category(ChangeCategory.BUSINESS_ATTRIBUTE) .operation(operation) .semVerChange(SemanticChangeType.MINOR) - .description(String.format(format, association.getDestinationUrn().getId(), entityUrn)) - .businessAttributeUrn(association.getDestinationUrn()) + .description( + String.format(format, association.getBusinessAttributeUrn().getId(), entityUrn)) + .businessAttributeUrn(association.getBusinessAttributeUrn()) .auditStamp(auditStamp) .build(); } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java index a7c4cf2e863d60..1f094bb6ca9890 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/timeline/eventgenerator/EditableSchemaMetadataChangeEventGenerator.java @@ -5,7 +5,6 @@ import com.datahub.util.RecordUtils; import com.github.fge.jsonpatch.JsonPatch; -import com.linkedin.businessattribute.BusinessAttributeAssociation; import com.linkedin.common.AuditStamp; import com.linkedin.common.GlobalTags; import com.linkedin.common.GlossaryTerms; @@ -77,11 +76,6 @@ private static List getAllChangeEvents( changeEvents.addAll( getGlossaryTermChangeEvents(baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); } - if (changeCategory == ChangeCategory.BUSINESS_ATTRIBUTE) { - changeEvents.addAll( - getBusinessAttributeAssociationChangeEvents( - baseFieldInfo, targetFieldInfo, datasetFieldUrn, auditStamp)); - } return changeEvents; } @@ -266,28 +260,6 @@ private static List getTagChangeEvents( return Collections.emptyList(); } - private static List getBusinessAttributeAssociationChangeEvents( - EditableSchemaFieldInfo baseFieldInfo, - EditableSchemaFieldInfo targetFieldInfo, - Urn datasetFieldUrn, - AuditStamp auditStamp) { - BusinessAttributeAssociation baseBusinessAttributeAssociation = - (baseFieldInfo != null) ? baseFieldInfo.getBusinessAttribute() : null; - BusinessAttributeAssociation targetBusinessAttributeAssociation = - (targetFieldInfo != null) ? targetFieldInfo.getBusinessAttribute() : null; - - // 1. Get EntityBusinessAttributeAssociationChangeEvent, then rebind into a - // SchemaFieldBusinessAttributeAssociationChangeEvent. - List entityBusinessAttributeAssociationChangeEvents = - BusinessAttributeAssociationChangeEventGenerator.computeDiffs( - baseBusinessAttributeAssociation, - targetBusinessAttributeAssociation, - datasetFieldUrn.toString(), - auditStamp); - - return entityBusinessAttributeAssociationChangeEvents; - } - @Override public ChangeTransaction getSemanticDiff( EntityAspect previousValue, diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl index 139a77d463df52..5422864185f141 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeAssociation.pdl @@ -1,6 +1,9 @@ namespace com.linkedin.businessattribute -import com.linkedin.common.Edge - -record BusinessAttributeAssociation includes Edge { +import com.linkedin.common.BusinessAttributeUrn +record BusinessAttributeAssociation { + /** + * Urn of the applied businessAttribute + */ + businessAttributeUrn: BusinessAttributeUrn } \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl index 9a7d8922940300..6236c9e77f455e 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl @@ -1,7 +1,7 @@ namespace com.linkedin.businessattribute import com.linkedin.schema.SchemaFieldDataType -import com.linkedin.schema.EditableSchemaFieldBase +import com.linkedin.schema.EditableSchemaFieldInfo import com.linkedin.common.CustomProperties import com.linkedin.common.ChangeAuditStamps @@ -11,7 +11,7 @@ import com.linkedin.common.ChangeAuditStamps @Aspect = { "name": "businessAttributeInfo" } -record BusinessAttributeInfo includes EditableSchemaFieldBase, CustomProperties, ChangeAuditStamps { +record BusinessAttributeInfo includes EditableSchemaFieldInfo, CustomProperties, ChangeAuditStamps { /** * Display name of the BusinessAttribute */ @@ -19,7 +19,6 @@ record BusinessAttributeInfo includes EditableSchemaFieldBase, CustomProperties, "fieldType": "WORD_GRAM", "enableAutocomplete": true, "boostScore": 10.0, - "fieldNameAliases": [ "_entityName" ] } name: string type: optional SchemaFieldDataType diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl index 648a35d79534a7..5c134804af19dd 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeKey.pdl @@ -8,7 +8,7 @@ namespace com.linkedin.businessattribute } record BusinessAttributeKey { /** - * A unique id for the Data Product. + * A unique id for the Business Attribute. */ id: string } \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl new file mode 100644 index 00000000000000..5b6403dcc2c0af --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl @@ -0,0 +1,29 @@ +namespace com.linkedin.businessattribute + +/** + * BusinessAttribute aspect used for applying it to an entity + */ +@Aspect = { + "name": "businessAttributes" +} +record BusinessAttributes { + + /** + * Business Attribute for this field. + */ + @Relationship = { + "/destinationUrn": { + "name": "BusinessAttributeOf", + "entityTypes": [ "businessAttribute" ] + } + } + @SearchableRef = { + "/businessAttributeUrn": { + "fieldName": "businessAttributeRef", + "fieldType": "URN", + "boostScore": 0.5 + "refType" : "businessAttribute" + } + } + businessAttribute: optional BusinessAttributeAssociation +} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl deleted file mode 100644 index c68ca97c939be3..00000000000000 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldBase.pdl +++ /dev/null @@ -1,61 +0,0 @@ -namespace com.linkedin.schema - -import com.linkedin.common.GlobalTags -import com.linkedin.common.GlossaryTerms - -/** -* Base class to describe metadata related to dataset schema. -*/ - -record EditableSchemaFieldBase { - /** - * FieldPath uniquely identifying the SchemaField this metadata is associated with - */ - fieldPath: string - - /** - * Description - */ - @Searchable = { - "fieldName": "editedFieldDescriptions", - "fieldType": "TEXT", - "boostScore": 0.1 - } - description: optional string - - /** - * Tags associated with the field - */ - @Relationship = { - "/tags/*/tag": { - "name": "EditableSchemaFieldTaggedWith", - "entityTypes": [ "tag" ] - } - } - @Searchable = { - "/tags/*/tag": { - "fieldName": "editedFieldTags", - "fieldType": "URN", - "boostScore": 0.5 - } - } - globalTags: optional GlobalTags - - /** - * Glossary terms associated with the field - */ - @Relationship = { - "/terms/*/urn": { - "name": "EditableSchemaFieldWithGlossaryTerm", - "entityTypes": [ "glossaryTerm" ] - } - } - @Searchable = { - "/terms/*/urn": { - "fieldName": "editedFieldGlossaryTerms", - "fieldType": "URN", - "boostScore": 0.5 - } - } - glossaryTerms: optional GlossaryTerms -} \ No newline at end of file diff --git a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl index 4b0cf938004842..4e6e135ae05da5 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/schema/EditableSchemaFieldInfo.pdl @@ -1,27 +1,60 @@ namespace com.linkedin.schema -import com.linkedin.businessattribute.BusinessAttributeAssociation + +import com.linkedin.common.GlobalTags +import com.linkedin.common.GlossaryTerms /** * SchemaField to describe metadata related to dataset schema. */ -record EditableSchemaFieldInfo includes EditableSchemaFieldBase { +record EditableSchemaFieldInfo { + /** + * FieldPath uniquely identifying the SchemaField this metadata is associated with + */ + fieldPath: string + + /** + * Description + */ + @Searchable = { + "fieldName": "editedFieldDescriptions", + "fieldType": "TEXT", + "boostScore": 0.1 + } + description: optional string + + /** + * Tags associated with the field + */ + @Relationship = { + "/tags/*/tag": { + "name": "EditableSchemaFieldTaggedWith", + "entityTypes": [ "tag" ] + } + } + @Searchable = { + "/tags/*/tag": { + "fieldName": "editedFieldTags", + "fieldType": "URN", + "boostScore": 0.5 + } + } + globalTags: optional GlobalTags - /** - * Business Attribute for this field. - */ - @Relationship = { - "/destinationUrn": { - "name": "EditableSchemaFieldWithBusinessAttribute", - "entityTypes": [ "businessAttribute" ] - } - } - @SearchableRef = { - "/destinationUrn": { - "fieldName": "editedFieldBusinessAttributeRef", - "fieldType": "URN", - "boostScore": 0.5, - "refType": "businessAttribute" - } - } - businessAttribute: optional BusinessAttributeAssociation + /** + * Glossary terms associated with the field + */ + @Relationship = { + "/terms/*/urn": { + "name": "EditableSchemaFieldWithGlossaryTerm", + "entityTypes": [ "glossaryTerm" ] + } + } + @Searchable = { + "/terms/*/urn": { + "fieldName": "editedFieldGlossaryTerms", + "fieldType": "URN", + "boostScore": 0.5 + } + } + glossaryTerms: optional GlossaryTerms } diff --git a/metadata-models/src/main/resources/entity-registry.yml b/metadata-models/src/main/resources/entity-registry.yml index 0e91018969ce3a..87c18ca17b33fd 100644 --- a/metadata-models/src/main/resources/entity-registry.yml +++ b/metadata-models/src/main/resources/entity-registry.yml @@ -450,6 +450,7 @@ entities: aspects: - structuredProperties - forms + - businessAttributes - name: globalSettings doc: Global settings for an the platform category: internal diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json index 6d3cab816aa355..468613181aef90 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json @@ -258,80 +258,6 @@ "compliance" : "NONE" } ] }, "com.linkedin.avro2pegasus.events.UUID", { - "type" : "record", - "name" : "BusinessAttributeAssociation", - "namespace" : "com.linkedin.businessattribute", - "include" : [ { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : "Urn", - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } ], - "fields" : [ ] - }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -444,7 +370,42 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : "AuditStamp", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -494,7 +455,40 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : "com.linkedin.common.Edge" + "items" : { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : "Urn", + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } }, "doc" : "Data sources for the chart", "optional" : true, @@ -1237,12 +1231,12 @@ "items" : "Urn" } }, - "doc" : "Owners to ownership type map, populated with mutation hook.", + "doc" : "Ownership type to Owners map, populated via mutation hook.", "default" : { }, "optional" : true, "Searchable" : { "/*" : { - "fieldType" : "OBJECT", + "fieldType" : "MAP_ARRAY", "queryByDefault" : false } } @@ -3168,75 +3162,54 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "include" : [ { - "type" : "record", - "name" : "EditableSchemaFieldBase", - "doc" : "Base class to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" - } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" - } + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" + } + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", - "optional" : true, - "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" - } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" } - } ] - } ], - "fields" : [ { - "name" : "businessAttribute", - "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", - "doc" : "Business Attribute for this field.", + } + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", "optional" : true, "Relationship" : { - "/destinationUrn" : { - "createdActor" : "businessAttribute/created/actor", - "createdOn" : "businessAttribute/created/time", - "entityTypes" : [ "businessAttribute" ], - "name" : "EditableSchemaFieldWithBusinessAttribute", - "updatedActor" : "businessAttribute/lastModified/actor", - "updatedOn" : "businessAttribute/lastModified/time" + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" } } } ] @@ -4049,7 +4022,7 @@ "doc" : "A string->string map of custom properties that one might want to attach to an event\n", "optional" : true } ] - }, "com.linkedin.mxe.SystemMetadata", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.mxe.SystemMetadata", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "aspects", "namespace" : "com.linkedin.entity", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json index e953d8bc8ebf8a..fff84d00494e70 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json @@ -1,79 +1,5 @@ { "models" : [ { - "type" : "record", - "name" : "BusinessAttributeAssociation", - "namespace" : "com.linkedin.businessattribute", - "include" : [ { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : "Urn", - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } ], - "fields" : [ ] - }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -186,7 +112,42 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : "AuditStamp", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -236,7 +197,40 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : "com.linkedin.common.Edge" + "items" : { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : "Urn", + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } }, "doc" : "Data sources for the chart", "optional" : true, @@ -1273,12 +1267,12 @@ "items" : "Urn" } }, - "doc" : "Owners to ownership type map, populated with mutation hook.", + "doc" : "Ownership type to Owners map, populated via mutation hook.", "default" : { }, "optional" : true, "Searchable" : { "/*" : { - "fieldType" : "OBJECT", + "fieldType" : "MAP_ARRAY", "queryByDefault" : false } } @@ -3552,75 +3546,54 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "include" : [ { - "type" : "record", - "name" : "EditableSchemaFieldBase", - "doc" : "Base class to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" - } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" - } + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" + } + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", - "optional" : true, - "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" - } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" } - } ] - } ], - "fields" : [ { - "name" : "businessAttribute", - "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", - "doc" : "Business Attribute for this field.", + } + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", "optional" : true, "Relationship" : { - "/destinationUrn" : { - "createdActor" : "businessAttribute/created/actor", - "createdOn" : "businessAttribute/created/time", - "entityTypes" : [ "businessAttribute" ], - "name" : "EditableSchemaFieldWithBusinessAttribute", - "updatedActor" : "businessAttribute/lastModified/actor", - "updatedOn" : "businessAttribute/lastModified/time" + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" } } } ] @@ -5251,11 +5224,17 @@ }, { "name" : "type", "type" : "string", - "doc" : "The type of policy" + "doc" : "The type of policy", + "Searchable" : { + "fieldType" : "KEYWORD" + } }, { "name" : "state", "type" : "string", - "doc" : "The state of policy, ACTIVE or INACTIVE" + "doc" : "The state of policy, ACTIVE or INACTIVE", + "Searchable" : { + "fieldType" : "KEYWORD" + } }, { "name" : "resources", "type" : { @@ -5339,7 +5318,13 @@ "type" : "array", "items" : "string" }, - "doc" : "The privileges that the policy grants." + "doc" : "The privileges that the policy grants.", + "Searchable" : { + "/*" : { + "addToFilters" : true, + "fieldType" : "KEYWORD" + } + } }, { "name" : "actors", "type" : { @@ -5406,7 +5391,10 @@ "name" : "editable", "type" : "boolean", "doc" : "Whether the policy should be editable via the UI", - "default" : true + "default" : true, + "Searchable" : { + "fieldType" : "BOOLEAN" + } }, { "name" : "lastUpdatedTimestamp", "type" : "long", @@ -6408,7 +6396,7 @@ "doc" : "Additional properties", "optional" : true } ] - }, "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "entities", "namespace" : "com.linkedin.entity", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json index aa81b072da904a..48fcf631102291 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json @@ -1,79 +1,5 @@ { "models" : [ { - "type" : "record", - "name" : "BusinessAttributeAssociation", - "namespace" : "com.linkedin.businessattribute", - "include" : [ { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : "Urn", - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } ], - "fields" : [ ] - }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -186,7 +112,42 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : "AuditStamp", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -236,7 +197,40 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : "com.linkedin.common.Edge" + "items" : { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : "Urn", + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } }, "doc" : "Data sources for the chart", "optional" : true, @@ -979,12 +973,12 @@ "items" : "Urn" } }, - "doc" : "Owners to ownership type map, populated with mutation hook.", + "doc" : "Ownership type to Owners map, populated via mutation hook.", "default" : { }, "optional" : true, "Searchable" : { "/*" : { - "fieldType" : "OBJECT", + "fieldType" : "MAP_ARRAY", "queryByDefault" : false } } @@ -2902,75 +2896,54 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "include" : [ { - "type" : "record", - "name" : "EditableSchemaFieldBase", - "doc" : "Base class to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" - } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" - } + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" + } + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", - "optional" : true, - "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" - } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" } - } ] - } ], - "fields" : [ { - "name" : "businessAttribute", - "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", - "doc" : "Business Attribute for this field.", + } + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", "optional" : true, "Relationship" : { - "/destinationUrn" : { - "createdActor" : "businessAttribute/created/actor", - "createdOn" : "businessAttribute/created/time", - "entityTypes" : [ "businessAttribute" ], - "name" : "EditableSchemaFieldWithBusinessAttribute", - "updatedActor" : "businessAttribute/lastModified/actor", - "updatedOn" : "businessAttribute/lastModified/time" + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" } } } ] @@ -3804,7 +3777,7 @@ } } } ] - }, "com.linkedin.metadata.run.UnsafeEntityInfo", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.metadata.run.UnsafeEntityInfo", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "runs", "namespace" : "com.linkedin.entity", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json index 1a09456fa6740a..d7199bed56d2ce 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.operations.operations.snapshot.json @@ -1,79 +1,5 @@ { "models" : [ { - "type" : "record", - "name" : "BusinessAttributeAssociation", - "namespace" : "com.linkedin.businessattribute", - "include" : [ { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : "Urn", - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } ], - "fields" : [ ] - }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -186,7 +112,42 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : "AuditStamp", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -236,7 +197,40 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : "com.linkedin.common.Edge" + "items" : { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : "Urn", + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } }, "doc" : "Data sources for the chart", "optional" : true, @@ -979,12 +973,12 @@ "items" : "Urn" } }, - "doc" : "Owners to ownership type map, populated with mutation hook.", + "doc" : "Ownership type to Owners map, populated via mutation hook.", "default" : { }, "optional" : true, "Searchable" : { "/*" : { - "fieldType" : "OBJECT", + "fieldType" : "MAP_ARRAY", "queryByDefault" : false } } @@ -2896,75 +2890,54 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "include" : [ { - "type" : "record", - "name" : "EditableSchemaFieldBase", - "doc" : "Base class to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" - } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" - } + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" + } + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", - "optional" : true, - "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" - } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" } - } ] - } ], - "fields" : [ { - "name" : "businessAttribute", - "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", - "doc" : "Business Attribute for this field.", + } + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", "optional" : true, "Relationship" : { - "/destinationUrn" : { - "createdActor" : "businessAttribute/created/actor", - "createdOn" : "businessAttribute/created/time", - "entityTypes" : [ "businessAttribute" ], - "name" : "EditableSchemaFieldWithBusinessAttribute", - "updatedActor" : "businessAttribute/lastModified/actor", - "updatedOn" : "businessAttribute/lastModified/time" + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" } } } ] @@ -3710,7 +3683,7 @@ "name" : "version", "type" : "long" } ] - }, "com.linkedin.metadata.key.ChartKey", "com.linkedin.metadata.key.CorpGroupKey", "com.linkedin.metadata.key.CorpUserKey", "com.linkedin.metadata.key.DashboardKey", "com.linkedin.metadata.key.DataFlowKey", "com.linkedin.metadata.key.DataJobKey", "com.linkedin.metadata.key.GlossaryNodeKey", "com.linkedin.metadata.key.GlossaryTermKey", "com.linkedin.metadata.key.MLFeatureKey", "com.linkedin.metadata.key.MLModelKey", "com.linkedin.metadata.key.TagKey", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties", { + }, "com.linkedin.metadata.key.ChartKey", "com.linkedin.metadata.key.CorpGroupKey", "com.linkedin.metadata.key.CorpUserKey", "com.linkedin.metadata.key.DashboardKey", "com.linkedin.metadata.key.DataFlowKey", "com.linkedin.metadata.key.DataJobKey", "com.linkedin.metadata.key.GlossaryNodeKey", "com.linkedin.metadata.key.GlossaryTermKey", "com.linkedin.metadata.key.MLFeatureKey", "com.linkedin.metadata.key.MLModelKey", "com.linkedin.metadata.key.TagKey", "com.linkedin.ml.metadata.BaseData", "com.linkedin.ml.metadata.CaveatDetails", "com.linkedin.ml.metadata.CaveatsAndRecommendations", "com.linkedin.ml.metadata.EthicalConsiderations", "com.linkedin.ml.metadata.EvaluationData", "com.linkedin.ml.metadata.HyperParameterValueType", "com.linkedin.ml.metadata.IntendedUse", "com.linkedin.ml.metadata.IntendedUserType", "com.linkedin.ml.metadata.MLFeatureProperties", "com.linkedin.ml.metadata.MLHyperParam", "com.linkedin.ml.metadata.MLMetric", "com.linkedin.ml.metadata.MLModelFactorPrompts", "com.linkedin.ml.metadata.MLModelFactors", "com.linkedin.ml.metadata.MLModelProperties", "com.linkedin.ml.metadata.Metrics", "com.linkedin.ml.metadata.QuantitativeAnalyses", "com.linkedin.ml.metadata.ResultsType", "com.linkedin.ml.metadata.SourceCode", "com.linkedin.ml.metadata.SourceCodeUrl", "com.linkedin.ml.metadata.SourceCodeUrlType", "com.linkedin.ml.metadata.TrainingData", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties", { "type" : "record", "name" : "TimeseriesIndexSizeResult", "namespace" : "com.linkedin.timeseries", diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json index 94d3e18df7c20a..c9733639c8909b 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.platform.platform.snapshot.json @@ -1,79 +1,5 @@ { "models" : [ { - "type" : "record", - "name" : "BusinessAttributeAssociation", - "namespace" : "com.linkedin.businessattribute", - "include" : [ { - "type" : "record", - "name" : "Edge", - "namespace" : "com.linkedin.common", - "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", - "fields" : [ { - "name" : "sourceUrn", - "type" : { - "type" : "typeref", - "name" : "Urn", - "ref" : "string", - "java" : { - "class" : "com.linkedin.common.urn.Urn" - } - }, - "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", - "optional" : true - }, { - "name" : "destinationUrn", - "type" : "Urn", - "doc" : "Urn of the destination of this relationship edge." - }, { - "name" : "created", - "type" : { - "type" : "record", - "name" : "AuditStamp", - "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", - "fields" : [ { - "name" : "time", - "type" : { - "type" : "typeref", - "name" : "Time", - "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", - "ref" : "long" - }, - "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." - }, { - "name" : "actor", - "type" : "Urn", - "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." - }, { - "name" : "impersonator", - "type" : "Urn", - "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", - "optional" : true - }, { - "name" : "message", - "type" : "string", - "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", - "optional" : true - } ] - }, - "doc" : "Audit stamp containing who created this relationship edge and when", - "optional" : true - }, { - "name" : "lastModified", - "type" : "AuditStamp", - "doc" : "Audit stamp containing who last modified this relationship edge and when", - "optional" : true - }, { - "name" : "properties", - "type" : { - "type" : "map", - "values" : "string" - }, - "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", - "optional" : true - } ] - } ], - "fields" : [ ] - }, { "type" : "typeref", "name" : "ChartDataSourceType", "namespace" : "com.linkedin.chart", @@ -186,7 +112,42 @@ "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into various lifecycle stages, and who acted to move it into those lifecycle stages. The recommended best practice is to include this record in your record schema, and annotate its fields as @readOnly in your resource. See https://github.com/linkedin/rest.li/wiki/Validation-in-Rest.li#restli-validation-annotations", "fields" : [ { "name" : "created", - "type" : "AuditStamp", + "type" : { + "type" : "record", + "name" : "AuditStamp", + "doc" : "Data captured on a resource/association/sub-resource level giving insight into when that resource/association/sub-resource moved into a particular lifecycle stage, and who acted to move it into that specific lifecycle stage.", + "fields" : [ { + "name" : "time", + "type" : { + "type" : "typeref", + "name" : "Time", + "doc" : "Number of milliseconds since midnight, January 1, 1970 UTC. It must be a positive number", + "ref" : "long" + }, + "doc" : "When did the resource/association/sub-resource move into the specific lifecycle stage represented by this AuditEvent." + }, { + "name" : "actor", + "type" : { + "type" : "typeref", + "name" : "Urn", + "ref" : "string", + "java" : { + "class" : "com.linkedin.common.urn.Urn" + } + }, + "doc" : "The entity (e.g. a member URN) which will be credited for moving the resource/association/sub-resource into the specific lifecycle stage. It is also the one used to authorize the change." + }, { + "name" : "impersonator", + "type" : "Urn", + "doc" : "The entity (e.g. a service URN) which performs the change on behalf of the Actor and must be authorized to act as the Actor.", + "optional" : true + }, { + "name" : "message", + "type" : "string", + "doc" : "Additional context around how DataHub was informed of the particular change. For example: was the change created by an automated process, or manually.", + "optional" : true + } ] + }, "doc" : "An AuditStamp corresponding to the creation of this resource/association/sub-resource. A value of 0 for time indicates missing data.", "default" : { "actor" : "urn:li:corpuser:unknown", @@ -236,7 +197,40 @@ "name" : "inputEdges", "type" : { "type" : "array", - "items" : "com.linkedin.common.Edge" + "items" : { + "type" : "record", + "name" : "Edge", + "namespace" : "com.linkedin.common", + "doc" : "A common structure to represent all edges to entities when used inside aspects as collections\nThis ensures that all edges have common structure around audit-stamps and will support PATCH, time-travel automatically.\n", + "fields" : [ { + "name" : "sourceUrn", + "type" : "Urn", + "doc" : "Urn of the source of this relationship edge.\nIf not specified, assumed to be the entity that this aspect belongs to.", + "optional" : true + }, { + "name" : "destinationUrn", + "type" : "Urn", + "doc" : "Urn of the destination of this relationship edge." + }, { + "name" : "created", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who created this relationship edge and when", + "optional" : true + }, { + "name" : "lastModified", + "type" : "AuditStamp", + "doc" : "Audit stamp containing who last modified this relationship edge and when", + "optional" : true + }, { + "name" : "properties", + "type" : { + "type" : "map", + "values" : "string" + }, + "doc" : "A generic properties bag that allows us to store specific information on this graph edge.", + "optional" : true + } ] + } }, "doc" : "Data sources for the chart", "optional" : true, @@ -1273,12 +1267,12 @@ "items" : "Urn" } }, - "doc" : "Owners to ownership type map, populated with mutation hook.", + "doc" : "Ownership type to Owners map, populated via mutation hook.", "default" : { }, "optional" : true, "Searchable" : { "/*" : { - "fieldType" : "OBJECT", + "fieldType" : "MAP_ARRAY", "queryByDefault" : false } } @@ -3546,75 +3540,54 @@ "type" : "record", "name" : "EditableSchemaFieldInfo", "doc" : "SchemaField to describe metadata related to dataset schema.", - "include" : [ { - "type" : "record", - "name" : "EditableSchemaFieldBase", - "doc" : "Base class to describe metadata related to dataset schema.", - "fields" : [ { - "name" : "fieldPath", - "type" : "string", - "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" - }, { - "name" : "description", - "type" : "string", - "doc" : "Description", - "optional" : true, - "Searchable" : { - "boostScore" : 0.1, - "fieldName" : "editedFieldDescriptions", - "fieldType" : "TEXT" - } - }, { - "name" : "globalTags", - "type" : "com.linkedin.common.GlobalTags", - "doc" : "Tags associated with the field", - "optional" : true, - "Relationship" : { - "/tags/*/tag" : { - "entityTypes" : [ "tag" ], - "name" : "EditableSchemaFieldTaggedWith" - } - }, - "Searchable" : { - "/tags/*/tag" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldTags", - "fieldType" : "URN" - } + "fields" : [ { + "name" : "fieldPath", + "type" : "string", + "doc" : "FieldPath uniquely identifying the SchemaField this metadata is associated with" + }, { + "name" : "description", + "type" : "string", + "doc" : "Description", + "optional" : true, + "Searchable" : { + "boostScore" : 0.1, + "fieldName" : "editedFieldDescriptions", + "fieldType" : "TEXT" + } + }, { + "name" : "globalTags", + "type" : "com.linkedin.common.GlobalTags", + "doc" : "Tags associated with the field", + "optional" : true, + "Relationship" : { + "/tags/*/tag" : { + "entityTypes" : [ "tag" ], + "name" : "EditableSchemaFieldTaggedWith" } - }, { - "name" : "glossaryTerms", - "type" : "com.linkedin.common.GlossaryTerms", - "doc" : "Glossary terms associated with the field", - "optional" : true, - "Relationship" : { - "/terms/*/urn" : { - "entityTypes" : [ "glossaryTerm" ], - "name" : "EditableSchemaFieldWithGlossaryTerm" - } - }, - "Searchable" : { - "/terms/*/urn" : { - "boostScore" : 0.5, - "fieldName" : "editedFieldGlossaryTerms", - "fieldType" : "URN" - } + }, + "Searchable" : { + "/tags/*/tag" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldTags", + "fieldType" : "URN" } - } ] - } ], - "fields" : [ { - "name" : "businessAttribute", - "type" : "com.linkedin.businessattribute.BusinessAttributeAssociation", - "doc" : "Business Attribute for this field.", + } + }, { + "name" : "glossaryTerms", + "type" : "com.linkedin.common.GlossaryTerms", + "doc" : "Glossary terms associated with the field", "optional" : true, "Relationship" : { - "/destinationUrn" : { - "createdActor" : "businessAttribute/created/actor", - "createdOn" : "businessAttribute/created/time", - "entityTypes" : [ "businessAttribute" ], - "name" : "EditableSchemaFieldWithBusinessAttribute", - "updatedActor" : "businessAttribute/lastModified/actor", - "updatedOn" : "businessAttribute/lastModified/time" + "/terms/*/urn" : { + "entityTypes" : [ "glossaryTerm" ], + "name" : "EditableSchemaFieldWithGlossaryTerm" + } + }, + "Searchable" : { + "/terms/*/urn" : { + "boostScore" : 0.5, + "fieldName" : "editedFieldGlossaryTerms", + "fieldType" : "URN" } } } ] @@ -5245,11 +5218,17 @@ }, { "name" : "type", "type" : "string", - "doc" : "The type of policy" + "doc" : "The type of policy", + "Searchable" : { + "fieldType" : "KEYWORD" + } }, { "name" : "state", "type" : "string", - "doc" : "The state of policy, ACTIVE or INACTIVE" + "doc" : "The state of policy, ACTIVE or INACTIVE", + "Searchable" : { + "fieldType" : "KEYWORD" + } }, { "name" : "resources", "type" : { @@ -5333,7 +5312,13 @@ "type" : "array", "items" : "string" }, - "doc" : "The privileges that the policy grants." + "doc" : "The privileges that the policy grants.", + "Searchable" : { + "/*" : { + "addToFilters" : true, + "fieldType" : "KEYWORD" + } + } }, { "name" : "actors", "type" : { @@ -5400,7 +5385,10 @@ "name" : "editable", "type" : "boolean", "doc" : "Whether the policy should be editable via the UI", - "default" : true + "default" : true, + "Searchable" : { + "fieldType" : "BOOLEAN" + } }, { "name" : "lastUpdatedTimestamp", "type" : "long", @@ -5598,7 +5586,7 @@ "type" : "GenericPayload", "doc" : "The event payload." } ] - }, "com.linkedin.mxe.PlatformEventHeader", "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldBase", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], + }, "com.linkedin.mxe.PlatformEventHeader", "com.linkedin.policy.DataHubActorFilter", "com.linkedin.policy.DataHubPolicyInfo", "com.linkedin.policy.DataHubResourceFilter", "com.linkedin.policy.PolicyMatchCondition", "com.linkedin.policy.PolicyMatchCriterion", "com.linkedin.policy.PolicyMatchFilter", "com.linkedin.retention.DataHubRetentionConfig", "com.linkedin.retention.Retention", "com.linkedin.retention.TimeBasedRetention", "com.linkedin.retention.VersionBasedRetention", "com.linkedin.schema.ArrayType", "com.linkedin.schema.BinaryJsonSchema", "com.linkedin.schema.BooleanType", "com.linkedin.schema.BytesType", "com.linkedin.schema.DatasetFieldForeignKey", "com.linkedin.schema.DateType", "com.linkedin.schema.EditableSchemaFieldInfo", "com.linkedin.schema.EditableSchemaMetadata", "com.linkedin.schema.EnumType", "com.linkedin.schema.EspressoSchema", "com.linkedin.schema.FixedType", "com.linkedin.schema.ForeignKeyConstraint", "com.linkedin.schema.ForeignKeySpec", "com.linkedin.schema.KafkaSchema", "com.linkedin.schema.KeyValueSchema", "com.linkedin.schema.MapType", "com.linkedin.schema.MySqlDDL", "com.linkedin.schema.NullType", "com.linkedin.schema.NumberType", "com.linkedin.schema.OracleDDL", "com.linkedin.schema.OrcSchema", "com.linkedin.schema.OtherSchema", "com.linkedin.schema.PrestoDDL", "com.linkedin.schema.RecordType", "com.linkedin.schema.SchemaField", "com.linkedin.schema.SchemaFieldDataType", "com.linkedin.schema.SchemaMetadata", "com.linkedin.schema.SchemaMetadataKey", "com.linkedin.schema.Schemaless", "com.linkedin.schema.StringType", "com.linkedin.schema.TimeType", "com.linkedin.schema.UnionType", "com.linkedin.schema.UrnForeignKey", "com.linkedin.tag.TagProperties" ], "schema" : { "name" : "platform", "namespace" : "com.linkedin.platform", diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index 9eeadd3e22a1ac..b2e0b604b7c32c 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -424,12 +424,6 @@ public class PoliciesConfig { "Produce Platform Event API", "The ability to produce Platform Events using the API."); - public static final Privilege EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE = - Privilege.of( - "EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE", - "Edit Dataset Column Business Attribute", - "The ability to edit the column (field) business attribute associated with a dataset schema."); - public static final ResourcePrivileges DATASET_PRIVILEGES = ResourcePrivileges.of( "dataset", @@ -446,8 +440,7 @@ public class PoliciesConfig { EDIT_ENTITY_ASSERTIONS_PRIVILEGE, EDIT_LINEAGE_PRIVILEGE, EDIT_ENTITY_EMBED_PRIVILEGE, - EDIT_QUERIES_PRIVILEGE, - EDIT_DATASET_COL_BUSINESS_ATTRIBUTE_PRIVILEGE)) + EDIT_QUERIES_PRIVILEGE)) .flatMap(Collection::stream) .collect(Collectors.toList())); From 0bb7aa4c234cb097cd7caa5b317745dbf5ccb484 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Wed, 20 Mar 2024 23:23:52 +0530 Subject: [PATCH 35/50] business-attributes: review comments - refactor businessAttribute propagation --- .../BusinessAttributeUpdateHookService.java | 127 ++++++++++++++ .../BusinessAttributeUpdateService.java | 138 --------------- .../hook/BusinessAttributeUpdateHook.java | 10 +- .../hook/BusinessAttributeUpdateHookTest.java | 162 +++++++----------- .../test/resources/test-entity-registry.yml | 5 + .../businessattribute/BusinessAttributes.pdl | 2 +- .../src/main/resources/application.yml | 3 + 7 files changed, 203 insertions(+), 244 deletions(-) create mode 100644 metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateHookService.java delete mode 100644 metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java diff --git a/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateHookService.java b/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateHookService.java new file mode 100644 index 00000000000000..c12a1be0d96ac1 --- /dev/null +++ b/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateHookService.java @@ -0,0 +1,127 @@ +package com.linkedin.metadata.service; + +import static com.linkedin.metadata.search.utils.QueryUtils.EMPTY_FILTER; +import static com.linkedin.metadata.search.utils.QueryUtils.newFilter; +import static com.linkedin.metadata.search.utils.QueryUtils.newRelationshipFilter; + +import com.google.common.collect.ImmutableSet; +import com.linkedin.businessattribute.BusinessAttributes; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.Urn; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.graph.GraphService; +import com.linkedin.metadata.graph.RelatedEntitiesResult; +import com.linkedin.metadata.graph.RelatedEntity; +import com.linkedin.metadata.models.AspectSpec; +import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.PlatformEvent; +import com.linkedin.platform.event.v1.EntityChangeEvent; +import java.util.Arrays; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class BusinessAttributeUpdateHookService { + private static final String BUSINESS_ATTRIBUTE_OF = "BusinessAttributeOf"; + + private final GraphService graphService; + private final EntityService entityService; + private final EntityRegistry entityRegistry; + + private final int relatedEntitiesCount; + + public static final String TAG = "TAG"; + public static final String GLOSSARY_TERM = "GLOSSARY_TERM"; + public static final String DOCUMENTATION = "DOCUMENTATION"; + + public BusinessAttributeUpdateHookService( + GraphService graphService, + EntityService entityService, + EntityRegistry entityRegistry, + @NonNull @Value("${businessAttribute.fetchRelatedEntitiesCount}") int relatedEntitiesCount) { + this.graphService = graphService; + this.entityService = entityService; + this.entityRegistry = entityRegistry; + this.relatedEntitiesCount = relatedEntitiesCount; + } + + public void handleChangeEvent(@NonNull final PlatformEvent event) { + final EntityChangeEvent entityChangeEvent = + GenericRecordUtils.deserializePayload( + event.getPayload().getValue(), EntityChangeEvent.class); + + if (!entityChangeEvent.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { + log.info("Skipping MCL event for entity:" + entityChangeEvent.getEntityType()); + return; + } + + final Set businessAttributeCategories = + ImmutableSet.of(TAG, GLOSSARY_TERM, DOCUMENTATION); + if (!businessAttributeCategories.contains(entityChangeEvent.getCategory())) { + log.info("Skipping MCL event for category: " + entityChangeEvent.getCategory()); + return; + } + + Urn urn = entityChangeEvent.getEntityUrn(); + log.info("Business Attribute update hook invoked for urn :" + urn); + + RelatedEntitiesResult entityAssociatedWithBusinessAttribute = + graphService.findRelatedEntities( + null, + newFilter("urn", urn.toString()), + null, + EMPTY_FILTER, + Arrays.asList(BUSINESS_ATTRIBUTE_OF), + newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING), + 0, + relatedEntitiesCount); + + for (RelatedEntity relatedEntity : entityAssociatedWithBusinessAttribute.getEntities()) { + String entityUrnStr = relatedEntity.getUrn(); + try { + Urn entityUrn = new Urn(entityUrnStr); + final AspectSpec aspectSpec = + entityRegistry + .getEntitySpec(Constants.SCHEMA_FIELD_ENTITY_NAME) + .getAspectSpec(Constants.BUSINESS_ATTRIBUTE_ASPECT); + + EnvelopedAspect envelopedAspect = + entityService.getLatestEnvelopedAspect( + Constants.SCHEMA_FIELD_ENTITY_NAME, entityUrn, Constants.BUSINESS_ATTRIBUTE_ASPECT); + BusinessAttributes businessAttributes = + new BusinessAttributes(envelopedAspect.getValue().data()); + + final AuditStamp auditStamp = + new AuditStamp() + .setActor(Urn.createFromString(Constants.SYSTEM_ACTOR)) + .setTime(System.currentTimeMillis()); + + entityService + .alwaysProduceMCLAsync( + entityUrn, + Constants.SCHEMA_FIELD_ENTITY_NAME, + Constants.BUSINESS_ATTRIBUTE_ASPECT, + aspectSpec, + null, + businessAttributes, + null, + null, + auditStamp, + ChangeType.RESTATE) + .getFirst(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java b/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java deleted file mode 100644 index a638644d7aa11e..00000000000000 --- a/metadata-io/src/main/java/com/linkedin/metadata/service/BusinessAttributeUpdateService.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.linkedin.metadata.service; - -import static com.linkedin.metadata.search.utils.QueryUtils.EMPTY_FILTER; -import static com.linkedin.metadata.search.utils.QueryUtils.newFilter; -import static com.linkedin.metadata.search.utils.QueryUtils.newRelationshipFilter; - -import com.google.common.collect.ImmutableSet; -import com.linkedin.common.AuditStamp; -import com.linkedin.common.urn.Urn; -import com.linkedin.dataset.EditableDatasetProperties; -import com.linkedin.entity.EntityResponse; -import com.linkedin.entity.EnvelopedAspectMap; -import com.linkedin.events.metadata.ChangeType; -import com.linkedin.metadata.Constants; -import com.linkedin.metadata.entity.EntityService; -import com.linkedin.metadata.graph.GraphService; -import com.linkedin.metadata.graph.RelatedEntitiesResult; -import com.linkedin.metadata.graph.RelatedEntity; -import com.linkedin.metadata.models.AspectSpec; -import com.linkedin.metadata.models.registry.EntityRegistry; -import com.linkedin.metadata.query.filter.RelationshipDirection; -import com.linkedin.metadata.utils.GenericRecordUtils; -import com.linkedin.mxe.PlatformEvent; -import com.linkedin.platform.event.v1.EntityChangeEvent; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nonnull; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -public class BusinessAttributeUpdateService { - private static final String EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE = - "EditableSchemaFieldWithBusinessAttribute"; - - private final GraphService _graphService; - private final EntityService _entityService; - private final EntityRegistry _entityRegistry; - - public static final String TAG = "TAG"; - public static final String GLOSSARY_TERM = "GLOSSARY_TERM"; - public static final String DOCUMENTATION = "DOCUMENTATION"; - - public BusinessAttributeUpdateService( - GraphService graphService, EntityService entityService, EntityRegistry entityRegistry) { - this._graphService = graphService; - this._entityService = entityService; - this._entityRegistry = entityRegistry; - } - - public void handleChangeEvent(@Nonnull final PlatformEvent event) { - final EntityChangeEvent entityChangeEvent = - GenericRecordUtils.deserializePayload( - event.getPayload().getValue(), EntityChangeEvent.class); - - if (!entityChangeEvent.getEntityType().equals(Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)) { - log.info( - "Skipping MCL event for invalid event entity type: " + entityChangeEvent.getEntityType()); - return; - } - - final Set businessAttributeCategories = - ImmutableSet.of(TAG, GLOSSARY_TERM, DOCUMENTATION); - if (!businessAttributeCategories.contains(entityChangeEvent.getCategory())) { - log.info("Skipping MCL event for invalid event category: " + entityChangeEvent.getCategory()); - return; - } - - Urn urn = entityChangeEvent.getEntityUrn(); - log.info("Business Attribute update hook invoked for :" + urn.toString()); - - RelatedEntitiesResult relatedEntitiesResult = - _graphService.findRelatedEntities( - null, - newFilter("urn", urn.toString()), - null, - EMPTY_FILTER, - Arrays.asList(EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE), - newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING), - 0, - 100000); - - for (RelatedEntity relatedEntity : relatedEntitiesResult.getEntities()) { - String datasetUrnStr = relatedEntity.getUrn(); - Map datasetEntityResponses; - try { - Urn datasetUrn = new Urn(datasetUrnStr); - final AspectSpec datasetAspectSpec = - _entityRegistry - .getEntitySpec(Constants.DATASET_ENTITY_NAME) - .getAspectSpec(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME); - datasetEntityResponses = - _entityService.getEntitiesV2( - Constants.DATASET_ENTITY_NAME, - new HashSet<>(Arrays.asList(datasetUrn)), - Collections.singleton(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME)); - - EntityResponse datasetEntityResponse = datasetEntityResponses.get(datasetUrn); - EditableDatasetProperties datasetProperties = mapTermInfo(datasetEntityResponse); - final AuditStamp auditStamp = - new AuditStamp() - .setActor(Urn.createFromString(Constants.SYSTEM_ACTOR)) - .setTime(System.currentTimeMillis()); - - _entityService - .alwaysProduceMCLAsync( - datasetUrn, - Constants.DATASET_ENTITY_NAME, - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - datasetAspectSpec, - null, - datasetProperties, - null, - null, - auditStamp, - ChangeType.RESTATE) - .getFirst(); - - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - } - - private EditableDatasetProperties mapTermInfo(EntityResponse entityResponse) { - EnvelopedAspectMap aspectMap = entityResponse.getAspects(); - if (!aspectMap.containsKey(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME)) { - return null; - } - return new EditableDatasetProperties( - aspectMap.get(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME).getValue().data()); - } -} diff --git a/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java index 50cb49d0bd81cc..b5317dd0ac78c2 100644 --- a/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java +++ b/metadata-jobs/pe-consumer/src/main/java/com/datahub/event/hook/BusinessAttributeUpdateHook.java @@ -3,7 +3,7 @@ import com.linkedin.gms.factory.common.GraphServiceFactory; import com.linkedin.gms.factory.entity.EntityServiceFactory; import com.linkedin.gms.factory.entityregistry.EntityRegistryFactory; -import com.linkedin.metadata.service.BusinessAttributeUpdateService; +import com.linkedin.metadata.service.BusinessAttributeUpdateHookService; import com.linkedin.mxe.PlatformEvent; import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; @@ -15,11 +15,11 @@ @Import({EntityServiceFactory.class, EntityRegistryFactory.class, GraphServiceFactory.class}) public class BusinessAttributeUpdateHook implements PlatformEventHook { - protected final BusinessAttributeUpdateService _businessAttributeUpdateService; + protected final BusinessAttributeUpdateHookService businessAttributeUpdateHookService; public BusinessAttributeUpdateHook( - BusinessAttributeUpdateService businessAttributeUpdateService) { - this._businessAttributeUpdateService = businessAttributeUpdateService; + BusinessAttributeUpdateHookService businessAttributeUpdateHookService) { + this.businessAttributeUpdateHookService = businessAttributeUpdateHookService; } /** @@ -29,6 +29,6 @@ public BusinessAttributeUpdateHook( */ @Override public void invoke(@Nonnull PlatformEvent event) { - _businessAttributeUpdateService.handleChangeEvent(event); + businessAttributeUpdateHookService.handleChangeEvent(event); } } diff --git a/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java index 54b4c3eb3c2adf..f7daf453d4676e 100644 --- a/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java +++ b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java @@ -1,14 +1,17 @@ package com.datahub.event.hook; import static com.datahub.event.hook.EntityRegistryTestUtil.ENTITY_REGISTRY; +import static com.linkedin.metadata.Constants.BUSINESS_ATTRIBUTE_ASPECT; +import static com.linkedin.metadata.Constants.SCHEMA_FIELD_ENTITY_NAME; import static com.linkedin.metadata.search.utils.QueryUtils.EMPTY_FILTER; import static com.linkedin.metadata.search.utils.QueryUtils.newFilter; import static com.linkedin.metadata.search.utils.QueryUtils.newRelationshipFilter; +import static org.mockito.ArgumentMatchers.eq; import static org.testng.Assert.assertEquals; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.linkedin.businessattribute.BusinessAttributeAssociation; +import com.linkedin.businessattribute.BusinessAttributes; import com.linkedin.common.AuditStamp; import com.linkedin.common.GlobalTags; import com.linkedin.common.TagAssociation; @@ -18,9 +21,7 @@ import com.linkedin.common.urn.UrnUtils; import com.linkedin.data.DataMap; 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.SystemRestliEntityClient; import com.linkedin.events.metadata.ChangeType; import com.linkedin.metadata.Constants; @@ -30,23 +31,18 @@ import com.linkedin.metadata.graph.RelatedEntity; import com.linkedin.metadata.models.AspectSpec; import com.linkedin.metadata.query.filter.RelationshipDirection; -import com.linkedin.metadata.service.BusinessAttributeUpdateService; +import com.linkedin.metadata.service.BusinessAttributeUpdateHookService; import com.linkedin.metadata.timeline.data.ChangeCategory; import com.linkedin.metadata.timeline.data.ChangeOperation; import com.linkedin.metadata.utils.GenericRecordUtils; import com.linkedin.mxe.PlatformEvent; import com.linkedin.mxe.PlatformEventHeader; +import com.linkedin.mxe.SystemMetadata; import com.linkedin.platform.event.v1.EntityChangeEvent; import com.linkedin.platform.event.v1.Parameters; -import com.linkedin.schema.EditableSchemaFieldInfo; -import com.linkedin.schema.EditableSchemaFieldInfoArray; -import com.linkedin.schema.EditableSchemaMetadata; import com.linkedin.util.Pair; import java.net.URISyntaxException; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.concurrent.Future; import org.mockito.Mockito; @@ -57,32 +53,32 @@ public class BusinessAttributeUpdateHookTest { private static final String TEST_BUSINESS_ATTRIBUTE_URN = "urn:li:businessAttribute:12668aea-009b-400e-8408-e661c3a230dd"; - private static final String EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE = - "EditableSchemaFieldWithBusinessAttribute"; - private static final Urn datasetUrn = UrnUtils.toDatasetUrn("hive", "test", "DEV"); - private static final String SUB_RESOURCE = "name"; + private static final String BUSINESS_ATTRIBUTE_OF = "BusinessAttributeOf"; + private static final Urn SCHEMA_FIELD_URN = + UrnUtils.getUrn( + "urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,SampleCypressHiveDataset,PROD),field_bar)"); private static final String TAG_NAME = "test"; private static final long EVENT_TIME = 123L; private static final String TEST_ACTOR_URN = "urn:li:corpuser:test"; - private static final String IsPartOfRelationship = "IsPartOf"; private static Urn actorUrn; - private static SystemRestliEntityClient _mockClient; + private static SystemRestliEntityClient mockClient; - private GraphService _mockGraphService; - private EntityService _mockEntityService; - private BusinessAttributeUpdateHook _businessAttributeUpdateHook; - private BusinessAttributeUpdateService _businessAttributeServiceHook; + private GraphService mockGraphService; + private EntityService mockEntityService; + private BusinessAttributeUpdateHook businessAttributeUpdateHook; + private BusinessAttributeUpdateHookService businessAttributeServiceHook; @BeforeMethod public void setupTest() throws URISyntaxException { - _mockGraphService = Mockito.mock(GraphService.class); - _mockEntityService = Mockito.mock(EntityService.class); + mockGraphService = Mockito.mock(GraphService.class); + mockEntityService = Mockito.mock(EntityService.class); actorUrn = Urn.createFromString(TEST_ACTOR_URN); - _mockClient = Mockito.mock(SystemRestliEntityClient.class); - _businessAttributeServiceHook = - new BusinessAttributeUpdateService(_mockGraphService, _mockEntityService, ENTITY_REGISTRY); - _businessAttributeUpdateHook = new BusinessAttributeUpdateHook(_businessAttributeServiceHook); + mockClient = Mockito.mock(SystemRestliEntityClient.class); + businessAttributeServiceHook = + new BusinessAttributeUpdateHookService( + mockGraphService, mockEntityService, ENTITY_REGISTRY, 100); + businessAttributeUpdateHook = new BusinessAttributeUpdateHook(businessAttributeServiceHook); } @Test @@ -93,39 +89,35 @@ public void testMCLOnBusinessAttributeUpdate() throws Exception { 0, 1, 1, - ImmutableList.of(new RelatedEntity(IsPartOfRelationship, datasetUrn.toString()))); + ImmutableList.of( + new RelatedEntity(BUSINESS_ATTRIBUTE_OF, SCHEMA_FIELD_URN.toString()))); // mock response Mockito.when( - _mockGraphService.findRelatedEntities( + mockGraphService.findRelatedEntities( null, newFilter("urn", TEST_BUSINESS_ATTRIBUTE_URN), null, EMPTY_FILTER, - Arrays.asList(EDITABLE_SCHEMAFIELD_WITH_BUSINESS_ATTRIBUTE), + Arrays.asList(BUSINESS_ATTRIBUTE_OF), newRelationshipFilter(EMPTY_FILTER, RelationshipDirection.INCOMING), 0, - 100000)) + 100)) .thenReturn(mockRelatedEntities); assertEquals(mockRelatedEntities.getTotal(), 1); - // mock response - Map datasetEntityResponse = datasetEntityResponses(); Mockito.when( - _mockEntityService.getEntitiesV2( - Constants.DATASET_ENTITY_NAME, - new HashSet<>(Collections.singleton(datasetUrn)), - Collections.singleton(Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME))) - .thenReturn(datasetEntityResponse); - assertEquals(datasetEntityResponse.size(), 1); + mockEntityService.getLatestEnvelopedAspect( + eq(SCHEMA_FIELD_ENTITY_NAME), eq(SCHEMA_FIELD_URN), eq(BUSINESS_ATTRIBUTE_ASPECT))) + .thenReturn(envelopedAspect()); // mock response Mockito.when( - _mockEntityService.alwaysProduceMCLAsync( + mockEntityService.alwaysProduceMCLAsync( Mockito.any(Urn.class), Mockito.anyString(), Mockito.anyString(), Mockito.any(AspectSpec.class), - Mockito.eq(null), + eq(null), Mockito.any(), Mockito.any(), Mockito.any(), @@ -134,10 +126,10 @@ public void testMCLOnBusinessAttributeUpdate() throws Exception { .thenReturn(Pair.of(Mockito.mock(Future.class), false)); // invoke - _businessAttributeServiceHook.handleChangeEvent(platformEvent); + businessAttributeServiceHook.handleChangeEvent(platformEvent); // verify - Mockito.verify(_mockGraphService, Mockito.times(1)) + Mockito.verify(mockGraphService, Mockito.times(1)) .findRelatedEntities( Mockito.any(), Mockito.any(), @@ -147,26 +139,19 @@ public void testMCLOnBusinessAttributeUpdate() throws Exception { Mockito.any(), Mockito.anyInt(), Mockito.anyInt()); - } - @Test - private void testMCLOnNonBusinessAttributeUpdate() { - PlatformEvent platformEvent = createBasePlatformEventDataset(); - - // invoke - _businessAttributeServiceHook.handleChangeEvent(platformEvent); - - // verify - Mockito.verify(_mockGraphService, Mockito.times(0)) - .findRelatedEntities( - Mockito.any(), + Mockito.verify(mockEntityService, Mockito.times(1)) + .alwaysProduceMCLAsync( + Mockito.any(Urn.class), + Mockito.anyString(), + Mockito.anyString(), + Mockito.any(AspectSpec.class), + eq(null), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), - Mockito.any(), - Mockito.anyInt(), - Mockito.anyInt()); + Mockito.any(ChangeType.class)); } @Test @@ -174,10 +159,10 @@ private void testMCLOnInvalidCategory() throws Exception { PlatformEvent platformEvent = createPlatformEventInvalidCategory(); // invoke - _businessAttributeServiceHook.handleChangeEvent(platformEvent); + businessAttributeServiceHook.handleChangeEvent(platformEvent); // verify - Mockito.verify(_mockGraphService, Mockito.times(0)) + Mockito.verify(mockGraphService, Mockito.times(0)) .findRelatedEntities( Mockito.any(), Mockito.any(), @@ -187,6 +172,19 @@ private void testMCLOnInvalidCategory() throws Exception { Mockito.any(), Mockito.anyInt(), Mockito.anyInt()); + + Mockito.verify(mockEntityService, Mockito.times(0)) + .alwaysProduceMCLAsync( + Mockito.any(Urn.class), + Mockito.anyString(), + Mockito.anyString(), + Mockito.any(AspectSpec.class), + eq(null), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(ChangeType.class)); } public static PlatformEvent createPlatformEventBusinessAttribute() throws Exception { @@ -206,23 +204,6 @@ public static PlatformEvent createPlatformEventBusinessAttribute() throws Except return platformEvent; } - public static PlatformEvent createBasePlatformEventDataset() { - final GlobalTags newTags = new GlobalTags(); - final TagUrn newTagUrn = new TagUrn(TAG_NAME); - newTags.setTags( - new TagAssociationArray(ImmutableList.of(new TagAssociation().setTag(newTagUrn)))); - PlatformEvent platformEvent = - createChangeEvent( - Constants.DATASET_ENTITY_NAME, - datasetUrn, - ChangeCategory.TAG, - ChangeOperation.ADD, - newTagUrn.toString(), - ImmutableMap.of("tagUrn", newTagUrn.toString()), - actorUrn); - return platformEvent; - } - public static PlatformEvent createPlatformEventInvalidCategory() throws Exception { final GlobalTags newTags = new GlobalTags(); final TagUrn newTagUrn = new TagUrn(TAG_NAME); @@ -270,29 +251,10 @@ private static PlatformEvent createChangeEvent( return platformEvent; } - private Map datasetEntityResponses() { - Map datasetInfoAspects = new HashMap<>(); - datasetInfoAspects.put( - Constants.EDITABLE_SCHEMA_METADATA_ASPECT_NAME, - new EnvelopedAspect().setValue(new Aspect(editableSchemaMetadata().data()))); - Map datasetEntityResponses = new HashMap<>(); - datasetEntityResponses.put( - datasetUrn, - new EntityResponse() - .setUrn(datasetUrn) - .setAspects(new EnvelopedAspectMap(datasetInfoAspects))); - return datasetEntityResponses; - } - - private EditableSchemaMetadata editableSchemaMetadata() { - EditableSchemaMetadata editableSchemaMetadata = new EditableSchemaMetadata(); - EditableSchemaFieldInfoArray editableSchemaFieldInfos = new EditableSchemaFieldInfoArray(); - com.linkedin.schema.EditableSchemaFieldInfo editableSchemaFieldInfo = - new EditableSchemaFieldInfo(); - editableSchemaFieldInfo.setBusinessAttribute(new BusinessAttributeAssociation()); - editableSchemaFieldInfo.setFieldPath(SUB_RESOURCE); - editableSchemaFieldInfos.add(editableSchemaFieldInfo); - editableSchemaMetadata.setEditableSchemaFieldInfo(editableSchemaFieldInfos); - return editableSchemaMetadata; + private EnvelopedAspect envelopedAspect() { + EnvelopedAspect envelopedAspect = new EnvelopedAspect(); + envelopedAspect.setValue(new Aspect(new BusinessAttributes().data())); + envelopedAspect.setSystemMetadata(new SystemMetadata()); + return envelopedAspect; } } diff --git a/metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml b/metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml index 081633a32bff88..f7296ec240750c 100644 --- a/metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml +++ b/metadata-jobs/pe-consumer/src/test/resources/test-entity-registry.yml @@ -3,5 +3,10 @@ entities: keyAspect: datasetKey aspects: - editableSchemaMetadata + - name: schemaField + category: core + keyAspect: schemaFieldKey + aspects: + - businessAttributes events: - name: entityChangeEvent diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl index 5b6403dcc2c0af..8b7df311d24d90 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributes.pdl @@ -12,7 +12,7 @@ record BusinessAttributes { * Business Attribute for this field. */ @Relationship = { - "/destinationUrn": { + "/businessAttributeUrn": { "name": "BusinessAttributeOf", "entityTypes": [ "businessAttribute" ] } diff --git a/metadata-service/configuration/src/main/resources/application.yml b/metadata-service/configuration/src/main/resources/application.yml index c0f82d85369220..a4641faf4c7172 100644 --- a/metadata-service/configuration/src/main/resources/application.yml +++ b/metadata-service/configuration/src/main/resources/application.yml @@ -439,3 +439,6 @@ springdoc.api-docs.groups.enabled: true forms: hook: enabled: { $FORMS_HOOK_ENABLED:true } + +businessAttribute: + fetchRelatedEntitiesCount: ${BUSINESS_ATTRIBUTE_RELATED_ENTITIES_COUNT:100000} From 0cc49c0ba33bda212ba3a70a3a6692753d200713 Mon Sep 17 00:00:00 2001 From: "Bharti, Aakash" Date: Thu, 21 Mar 2024 11:36:16 +0530 Subject: [PATCH 36/50] feature flag for business attribute GENAI=YES --- buildSrc/build.gradle | 3 +- .../io/datahubproject/OpenApiEntities.java | 8 + .../datahub/graphql/GmsGraphQLEngine.java | 390 +++++++++--------- .../graphql/featureflags/FeatureFlags.java | 1 + .../src/main/resources/application.yml | 1 + 5 files changed, 209 insertions(+), 194 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 88900e06d48451..cf49b65b8c1aaa 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -22,7 +22,8 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.5' implementation 'commons-io:commons-io:2.11.0' + implementation 'org.springframework:spring-beans:5.3.32' compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' -} \ No newline at end of file +} diff --git a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java index 13766994c3a032..bc36de5e1ee22e 100644 --- a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java +++ b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java @@ -11,6 +11,7 @@ import com.linkedin.metadata.models.registry.config.Entities; import com.linkedin.metadata.models.registry.config.Entity; import org.gradle.internal.Pair; +import org.springframework.beans.factory.annotation.Value; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -44,6 +45,9 @@ public class OpenApiEntities { private String entityRegistryYaml; private Path combinedDirectory; + @Value("${featureFlags.businessAttributeEntityEnabled:false}") + private boolean _businessAttributeEntityEnabled; + private final static ImmutableSet SUPPORTED_ASPECT_PATHS = ImmutableSet.builder() .add("domains") .add("ownership") @@ -117,6 +121,10 @@ public ObjectNode entityExtension(List nodesList, ObjectNode schemas Pair> parameters = buildParameters(schemasNode, modelDefinitions); ObjectNode componentsNode = writeComponentsYaml(schemasNode, parameters.left()); + if (!_businessAttributeEntityEnabled) { + modelDefinitions.remove("BusinessAttribute"); + } + // Just the entity paths writePathsYaml(modelDefinitions, parameters.right()); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index b7391795df4f2b..c538b73befbbd3 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -1075,199 +1075,203 @@ private String getUrnField(DataFetchingEnvironment env) { private void configureMutationResolvers(final RuntimeWiring.Builder builder) { builder.type( - "Mutation", - typeWiring -> - typeWiring - .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) - .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) - .dataFetcher( - "createTag", new CreateTagResolver(this.entityClient, this.entityService)) - .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) - .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) - .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) - .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) - .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) - .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) - .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) - .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) - .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) - .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) - .dataFetcher("addTag", new AddTagResolver(entityService)) - .dataFetcher("addTags", new AddTagsResolver(entityService)) - .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) - .dataFetcher("removeTag", new RemoveTagResolver(entityService)) - .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) - .dataFetcher("addTerm", new AddTermResolver(entityService)) - .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) - .dataFetcher("addTerms", new AddTermsResolver(entityService)) - .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) - .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) - .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) - .dataFetcher( - "updateDescription", - new UpdateDescriptionResolver(entityService, this.entityClient)) - .dataFetcher("addOwner", new AddOwnerResolver(entityService)) - .dataFetcher("addOwners", new AddOwnersResolver(entityService)) - .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) - .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) - .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) - .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) - .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) - .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) - .dataFetcher( - "removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) - .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) - .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) - .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) - .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) - .dataFetcher( - "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) - .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) - .dataFetcher( - "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) - .dataFetcher( - "updateDeprecation", - new UpdateDeprecationResolver(this.entityClient, this.entityService)) - .dataFetcher( - "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) - .dataFetcher( - "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) - .dataFetcher( - "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher( - "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) - .dataFetcher( - "revokeAccessToken", - new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) - .dataFetcher( - "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "createIngestionExecutionRequest", - new CreateIngestionExecutionRequestResolver( - this.entityClient, this.ingestionConfiguration)) - .dataFetcher( - "cancelIngestionExecutionRequest", - new CancelIngestionExecutionRequestResolver(this.entityClient)) - .dataFetcher( - "createTestConnectionRequest", - new CreateTestConnectionRequestResolver( - this.entityClient, this.ingestionConfiguration)) - .dataFetcher( - "deleteAssertion", - new DeleteAssertionResolver(this.entityClient, this.entityService)) - .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) - .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) - .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) - .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) - .dataFetcher( - "createGlossaryTerm", - new CreateGlossaryTermResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createGlossaryNode", - new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateParentNode", - new UpdateParentNodeResolver(this.entityService, this.entityClient)) - .dataFetcher( - "deleteGlossaryEntity", - new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) - .dataFetcher("addRelatedTerms", new AddRelatedTermsResolver(this.entityService)) - .dataFetcher( - "removeRelatedTerms", new RemoveRelatedTermsResolver(this.entityService)) - .dataFetcher( - "createNativeUserResetToken", - new CreateNativeUserResetTokenResolver(this.nativeUserService)) - .dataFetcher( - "batchUpdateSoftDeleted", - new BatchUpdateSoftDeletedResolver(this.entityService)) - .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) - .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) - .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) - .dataFetcher( - "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) - .dataFetcher( - "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) - .dataFetcher("createPost", new CreatePostResolver(this.postService)) - .dataFetcher("deletePost", new DeletePostResolver(this.postService)) - .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) - .dataFetcher( - "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) - .dataFetcher("createView", new CreateViewResolver(this.viewService)) - .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) - .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) - .dataFetcher( - "updateGlobalViewsSettings", - new UpdateGlobalViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateCorpUserViewsSettings", - new UpdateCorpUserViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateLineage", - new UpdateLineageResolver(this.entityService, this.lineageService)) - .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) - .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) - .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) - .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) - .dataFetcher( - "createDataProduct", new CreateDataProductResolver(this.dataProductService)) - .dataFetcher( - "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) - .dataFetcher( - "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) - .dataFetcher( - "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) - .dataFetcher( - "createOwnershipType", - new CreateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "updateOwnershipType", - new UpdateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "deleteOwnershipType", - new DeleteOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) - .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) - .dataFetcher( - "createDynamicFormAssignment", - new CreateDynamicFormAssignmentResolver(this.formService)) - .dataFetcher( - "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) - .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) - .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) - .dataFetcher( - "updateIncidentStatus", - new UpdateIncidentStatusResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createBusinessAttribute", - new CreateBusinessAttributeResolver( - this.entityClient, this.entityService, this.businessAttributeService)) - .dataFetcher( - "updateBusinessAttribute", - new UpdateBusinessAttributeResolver( - this.entityClient, this.businessAttributeService)) - .dataFetcher( - "deleteBusinessAttribute", - new DeleteBusinessAttributeResolver(this.entityClient)) - .dataFetcher( - "addBusinessAttribute", - new AddBusinessAttributeResolver(this.entityClient, this.entityService)) - .dataFetcher( - "removeBusinessAttribute", - new RemoveBusinessAttributeResolver(this.entityClient, this.entityService))); + "Mutation", + typeWiring -> { + typeWiring + .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) + .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) + .dataFetcher( + "createTag", new CreateTagResolver(this.entityClient, this.entityService)) + .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) + .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) + .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) + .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) + .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) + .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) + .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) + .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) + .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) + .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) + .dataFetcher("addTag", new AddTagResolver(entityService)) + .dataFetcher("addTags", new AddTagsResolver(entityService)) + .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) + .dataFetcher("removeTag", new RemoveTagResolver(entityService)) + .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) + .dataFetcher("addTerm", new AddTermResolver(entityService)) + .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) + .dataFetcher("addTerms", new AddTermsResolver(entityService)) + .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) + .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) + .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) + .dataFetcher( + "updateDescription", + new UpdateDescriptionResolver(entityService, this.entityClient)) + .dataFetcher("addOwner", new AddOwnerResolver(entityService)) + .dataFetcher("addOwners", new AddOwnersResolver(entityService)) + .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) + .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) + .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) + .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) + .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) + .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) + .dataFetcher( + "removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) + .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) + .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) + .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) + .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) + .dataFetcher( + "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) + .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) + .dataFetcher( + "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) + .dataFetcher( + "updateDeprecation", + new UpdateDeprecationResolver(this.entityClient, this.entityService)) + .dataFetcher( + "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) + .dataFetcher( + "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) + .dataFetcher( + "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher( + "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) + .dataFetcher( + "revokeAccessToken", + new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) + .dataFetcher( + "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "createIngestionExecutionRequest", + new CreateIngestionExecutionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "cancelIngestionExecutionRequest", + new CancelIngestionExecutionRequestResolver(this.entityClient)) + .dataFetcher( + "createTestConnectionRequest", + new CreateTestConnectionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "deleteAssertion", + new DeleteAssertionResolver(this.entityClient, this.entityService)) + .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) + .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) + .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) + .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) + .dataFetcher( + "createGlossaryTerm", + new CreateGlossaryTermResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createGlossaryNode", + new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateParentNode", + new UpdateParentNodeResolver(this.entityService, this.entityClient)) + .dataFetcher( + "deleteGlossaryEntity", + new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) + .dataFetcher("addRelatedTerms", new AddRelatedTermsResolver(this.entityService)) + .dataFetcher( + "removeRelatedTerms", new RemoveRelatedTermsResolver(this.entityService)) + .dataFetcher( + "createNativeUserResetToken", + new CreateNativeUserResetTokenResolver(this.nativeUserService)) + .dataFetcher( + "batchUpdateSoftDeleted", + new BatchUpdateSoftDeletedResolver(this.entityService)) + .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) + .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) + .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) + .dataFetcher( + "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) + .dataFetcher( + "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) + .dataFetcher("createPost", new CreatePostResolver(this.postService)) + .dataFetcher("deletePost", new DeletePostResolver(this.postService)) + .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) + .dataFetcher( + "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) + .dataFetcher("createView", new CreateViewResolver(this.viewService)) + .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) + .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) + .dataFetcher( + "updateGlobalViewsSettings", + new UpdateGlobalViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateCorpUserViewsSettings", + new UpdateCorpUserViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateLineage", + new UpdateLineageResolver(this.entityService, this.lineageService)) + .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) + .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) + .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) + .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) + .dataFetcher( + "createDataProduct", new CreateDataProductResolver(this.dataProductService)) + .dataFetcher( + "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) + .dataFetcher( + "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) + .dataFetcher( + "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) + .dataFetcher( + "createOwnershipType", + new CreateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "updateOwnershipType", + new UpdateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "deleteOwnershipType", + new DeleteOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) + .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) + .dataFetcher( + "createDynamicFormAssignment", + new CreateDynamicFormAssignmentResolver(this.formService)) + .dataFetcher( + "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) + .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) + .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) + .dataFetcher( + "updateIncidentStatus", + new UpdateIncidentStatusResolver(this.entityClient, this.entityService)); + if (featureFlags.isBusinessAttributeEntityEnabled()) { + typeWiring.dataFetcher( + "createBusinessAttribute", + new CreateBusinessAttributeResolver( + this.entityClient, this.entityService, this.businessAttributeService)) + .dataFetcher( + "updateBusinessAttribute", + new UpdateBusinessAttributeResolver( + this.entityClient, this.businessAttributeService)) + .dataFetcher( + "deleteBusinessAttribute", + new DeleteBusinessAttributeResolver(this.entityClient)) + .dataFetcher( + "addBusinessAttribute", + new AddBusinessAttributeResolver(this.entityClient, this.entityService)) + .dataFetcher( + "removeBusinessAttribute", + new RemoveBusinessAttributeResolver(this.entityClient, this.entityService)); + } + return typeWiring; + }); } private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java index 667ccd368a7291..62067db67e0c66 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java @@ -18,4 +18,5 @@ public class FeatureFlags { private boolean showAccessManagement = false; private boolean nestedDomainsEnabled = false; private boolean schemaFieldEntityFetchEnabled = false; + private boolean businessAttributeEntityEnabled = false; } diff --git a/metadata-service/configuration/src/main/resources/application.yml b/metadata-service/configuration/src/main/resources/application.yml index 9e824303788274..fc4140c4ffc2c6 100644 --- a/metadata-service/configuration/src/main/resources/application.yml +++ b/metadata-service/configuration/src/main/resources/application.yml @@ -363,6 +363,7 @@ featureFlags: showAcrylInfo: ${SHOW_ACRYL_INFO:false} # Show different CTAs within DataHub around moving to Managed DataHub. Set to true for the demo site. nestedDomainsEnabled: ${NESTED_DOMAINS_ENABLED:true} # Enables the nested Domains feature that allows users to have sub-Domains. If this is off, Domains appear "flat" again schemaFieldEntityFetchEnabled: ${SCHEMA_FIELD_ENTITY_FETCH_ENABLED:true} # Enables fetching for schema field entities from the database when we hydrate them on schema fields + businessAttributeEntityEnabled: ${BUSINESS_ATTRIBUTE_ENTITY_ENABLED:false} # Enables business attribute entity which can be associated with field of dataset entityChangeEvents: enabled: ${ENABLE_ENTITY_CHANGE_EVENTS_HOOK:true} From ba03044aa3c2b0322db4291e2ec4680820177a36 Mon Sep 17 00:00:00 2001 From: "Bharti, Aakash" Date: Thu, 21 Mar 2024 15:25:35 +0530 Subject: [PATCH 37/50] rebase --- buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java | 4 ++-- .../java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java index bc36de5e1ee22e..a82aeee29b80b3 100644 --- a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java +++ b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java @@ -46,7 +46,7 @@ public class OpenApiEntities { private Path combinedDirectory; @Value("${featureFlags.businessAttributeEntityEnabled:false}") - private boolean _businessAttributeEntityEnabled; + private boolean businessAttributeEntityEnabled; private final static ImmutableSet SUPPORTED_ASPECT_PATHS = ImmutableSet.builder() .add("domains") @@ -121,7 +121,7 @@ public ObjectNode entityExtension(List nodesList, ObjectNode schemas Pair> parameters = buildParameters(schemasNode, modelDefinitions); ObjectNode componentsNode = writeComponentsYaml(schemasNode, parameters.left()); - if (!_businessAttributeEntityEnabled) { + if (!businessAttributeEntityEnabled) { modelDefinitions.remove("BusinessAttribute"); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index c538b73befbbd3..6612228a1374b0 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -1265,10 +1265,10 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { new DeleteBusinessAttributeResolver(this.entityClient)) .dataFetcher( "addBusinessAttribute", - new AddBusinessAttributeResolver(this.entityClient, this.entityService)) + new AddBusinessAttributeResolver(this.entityService)) .dataFetcher( "removeBusinessAttribute", - new RemoveBusinessAttributeResolver(this.entityClient, this.entityService)); + new RemoveBusinessAttributeResolver(this.entityService)); } return typeWiring; }); From e2651a8ec663a46020d1984447512e4264d42e1c Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Fri, 22 Mar 2024 11:44:54 +0530 Subject: [PATCH 38/50] business-attributes: support for custom urn with graphql --- .../datahub/graphql/GmsGraphQLEngine.java | 388 +++++++++--------- .../CreateBusinessAttributeResolver.java | 3 +- .../src/main/resources/entity.graphql | 5 + .../CreateBusinessAttributeResolverTest.java | 10 +- 4 files changed, 205 insertions(+), 201 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 6612228a1374b0..0fd3a7d6a81e86 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -1075,203 +1075,197 @@ private String getUrnField(DataFetchingEnvironment env) { private void configureMutationResolvers(final RuntimeWiring.Builder builder) { builder.type( - "Mutation", - typeWiring -> { - typeWiring - .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) - .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) - .dataFetcher( - "createTag", new CreateTagResolver(this.entityClient, this.entityService)) - .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) - .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) - .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) - .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) - .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) - .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) - .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) - .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) - .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) - .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) - .dataFetcher("addTag", new AddTagResolver(entityService)) - .dataFetcher("addTags", new AddTagsResolver(entityService)) - .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) - .dataFetcher("removeTag", new RemoveTagResolver(entityService)) - .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) - .dataFetcher("addTerm", new AddTermResolver(entityService)) - .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) - .dataFetcher("addTerms", new AddTermsResolver(entityService)) - .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) - .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) - .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) - .dataFetcher( - "updateDescription", - new UpdateDescriptionResolver(entityService, this.entityClient)) - .dataFetcher("addOwner", new AddOwnerResolver(entityService)) - .dataFetcher("addOwners", new AddOwnersResolver(entityService)) - .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) - .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) - .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) - .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) - .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) - .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) - .dataFetcher( - "removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) - .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) - .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) - .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) - .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) - .dataFetcher( - "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) - .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) - .dataFetcher( - "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) - .dataFetcher( - "updateDeprecation", - new UpdateDeprecationResolver(this.entityClient, this.entityService)) - .dataFetcher( - "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) - .dataFetcher( - "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) - .dataFetcher( - "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher( - "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) - .dataFetcher( - "revokeAccessToken", - new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) - .dataFetcher( - "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "createIngestionExecutionRequest", - new CreateIngestionExecutionRequestResolver( - this.entityClient, this.ingestionConfiguration)) - .dataFetcher( - "cancelIngestionExecutionRequest", - new CancelIngestionExecutionRequestResolver(this.entityClient)) - .dataFetcher( - "createTestConnectionRequest", - new CreateTestConnectionRequestResolver( - this.entityClient, this.ingestionConfiguration)) - .dataFetcher( - "deleteAssertion", - new DeleteAssertionResolver(this.entityClient, this.entityService)) - .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) - .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) - .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) - .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) - .dataFetcher( - "createGlossaryTerm", - new CreateGlossaryTermResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createGlossaryNode", - new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateParentNode", - new UpdateParentNodeResolver(this.entityService, this.entityClient)) - .dataFetcher( - "deleteGlossaryEntity", - new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) - .dataFetcher("addRelatedTerms", new AddRelatedTermsResolver(this.entityService)) - .dataFetcher( - "removeRelatedTerms", new RemoveRelatedTermsResolver(this.entityService)) - .dataFetcher( - "createNativeUserResetToken", - new CreateNativeUserResetTokenResolver(this.nativeUserService)) - .dataFetcher( - "batchUpdateSoftDeleted", - new BatchUpdateSoftDeletedResolver(this.entityService)) - .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) - .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) - .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) - .dataFetcher( - "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) - .dataFetcher( - "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) - .dataFetcher("createPost", new CreatePostResolver(this.postService)) - .dataFetcher("deletePost", new DeletePostResolver(this.postService)) - .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) - .dataFetcher( - "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) - .dataFetcher("createView", new CreateViewResolver(this.viewService)) - .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) - .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) - .dataFetcher( - "updateGlobalViewsSettings", - new UpdateGlobalViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateCorpUserViewsSettings", - new UpdateCorpUserViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateLineage", - new UpdateLineageResolver(this.entityService, this.lineageService)) - .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) - .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) - .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) - .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) - .dataFetcher( - "createDataProduct", new CreateDataProductResolver(this.dataProductService)) - .dataFetcher( - "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) - .dataFetcher( - "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) - .dataFetcher( - "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) - .dataFetcher( - "createOwnershipType", - new CreateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "updateOwnershipType", - new UpdateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "deleteOwnershipType", - new DeleteOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) - .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) - .dataFetcher( - "createDynamicFormAssignment", - new CreateDynamicFormAssignmentResolver(this.formService)) - .dataFetcher( - "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) - .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) - .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) - .dataFetcher( - "updateIncidentStatus", - new UpdateIncidentStatusResolver(this.entityClient, this.entityService)); - if (featureFlags.isBusinessAttributeEntityEnabled()) { - typeWiring.dataFetcher( - "createBusinessAttribute", - new CreateBusinessAttributeResolver( - this.entityClient, this.entityService, this.businessAttributeService)) - .dataFetcher( - "updateBusinessAttribute", - new UpdateBusinessAttributeResolver( - this.entityClient, this.businessAttributeService)) - .dataFetcher( - "deleteBusinessAttribute", - new DeleteBusinessAttributeResolver(this.entityClient)) - .dataFetcher( - "addBusinessAttribute", - new AddBusinessAttributeResolver(this.entityService)) - .dataFetcher( - "removeBusinessAttribute", - new RemoveBusinessAttributeResolver(this.entityService)); - } - return typeWiring; - }); + "Mutation", + typeWiring -> { + typeWiring + .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) + .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) + .dataFetcher( + "createTag", new CreateTagResolver(this.entityClient, this.entityService)) + .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) + .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) + .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) + .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) + .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) + .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) + .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) + .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) + .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) + .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) + .dataFetcher("addTag", new AddTagResolver(entityService)) + .dataFetcher("addTags", new AddTagsResolver(entityService)) + .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) + .dataFetcher("removeTag", new RemoveTagResolver(entityService)) + .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) + .dataFetcher("addTerm", new AddTermResolver(entityService)) + .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) + .dataFetcher("addTerms", new AddTermsResolver(entityService)) + .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) + .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) + .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) + .dataFetcher( + "updateDescription", + new UpdateDescriptionResolver(entityService, this.entityClient)) + .dataFetcher("addOwner", new AddOwnerResolver(entityService)) + .dataFetcher("addOwners", new AddOwnersResolver(entityService)) + .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) + .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) + .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) + .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) + .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) + .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) + .dataFetcher("removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) + .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) + .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) + .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) + .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) + .dataFetcher( + "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) + .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) + .dataFetcher( + "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) + .dataFetcher( + "updateDeprecation", + new UpdateDeprecationResolver(this.entityClient, this.entityService)) + .dataFetcher( + "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) + .dataFetcher( + "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) + .dataFetcher( + "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher( + "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) + .dataFetcher( + "revokeAccessToken", + new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) + .dataFetcher( + "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "createIngestionExecutionRequest", + new CreateIngestionExecutionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "cancelIngestionExecutionRequest", + new CancelIngestionExecutionRequestResolver(this.entityClient)) + .dataFetcher( + "createTestConnectionRequest", + new CreateTestConnectionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "deleteAssertion", + new DeleteAssertionResolver(this.entityClient, this.entityService)) + .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) + .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) + .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) + .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) + .dataFetcher( + "createGlossaryTerm", + new CreateGlossaryTermResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createGlossaryNode", + new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateParentNode", + new UpdateParentNodeResolver(this.entityService, this.entityClient)) + .dataFetcher( + "deleteGlossaryEntity", + new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) + .dataFetcher("addRelatedTerms", new AddRelatedTermsResolver(this.entityService)) + .dataFetcher("removeRelatedTerms", new RemoveRelatedTermsResolver(this.entityService)) + .dataFetcher( + "createNativeUserResetToken", + new CreateNativeUserResetTokenResolver(this.nativeUserService)) + .dataFetcher( + "batchUpdateSoftDeleted", new BatchUpdateSoftDeletedResolver(this.entityService)) + .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) + .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) + .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) + .dataFetcher( + "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) + .dataFetcher( + "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) + .dataFetcher("createPost", new CreatePostResolver(this.postService)) + .dataFetcher("deletePost", new DeletePostResolver(this.postService)) + .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) + .dataFetcher( + "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) + .dataFetcher("createView", new CreateViewResolver(this.viewService)) + .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) + .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) + .dataFetcher( + "updateGlobalViewsSettings", + new UpdateGlobalViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateCorpUserViewsSettings", + new UpdateCorpUserViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateLineage", + new UpdateLineageResolver(this.entityService, this.lineageService)) + .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) + .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) + .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) + .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) + .dataFetcher( + "createDataProduct", new CreateDataProductResolver(this.dataProductService)) + .dataFetcher( + "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) + .dataFetcher( + "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) + .dataFetcher( + "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) + .dataFetcher( + "createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "updateOwnershipType", new UpdateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "deleteOwnershipType", new DeleteOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) + .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) + .dataFetcher( + "createDynamicFormAssignment", + new CreateDynamicFormAssignmentResolver(this.formService)) + .dataFetcher( + "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) + .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) + .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) + .dataFetcher( + "updateIncidentStatus", + new UpdateIncidentStatusResolver(this.entityClient, this.entityService)); + if (featureFlags.isBusinessAttributeEntityEnabled()) { + typeWiring + .dataFetcher( + "createBusinessAttribute", + new CreateBusinessAttributeResolver( + this.entityClient, this.entityService, this.businessAttributeService)) + .dataFetcher( + "updateBusinessAttribute", + new UpdateBusinessAttributeResolver( + this.entityClient, this.businessAttributeService)) + .dataFetcher( + "deleteBusinessAttribute", + new DeleteBusinessAttributeResolver(this.entityClient)) + .dataFetcher( + "addBusinessAttribute", new AddBusinessAttributeResolver(this.entityService)) + .dataFetcher( + "removeBusinessAttribute", + new RemoveBusinessAttributeResolver(this.entityService)); + } + return typeWiring; + }); } private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java index 2103d6d4eceef6..93de29d8c1bf00 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java @@ -55,7 +55,8 @@ public CompletableFuture get(DataFetchingEnvironment environm () -> { try { final BusinessAttributeKey businessAttributeKey = new BusinessAttributeKey(); - businessAttributeKey.setId(UUID.randomUUID().toString()); + String id = input.getId() != null ? input.getId() : UUID.randomUUID().toString(); + businessAttributeKey.setId(id); if (_entityClient.exists( EntityKeyUtils.convertEntityKeyToUrn( diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 81d77151dc668b..fca797902dec41 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -12033,6 +12033,11 @@ type BusinessAttributeInfo { Input required for creating a BusinessAttribute. """ input CreateBusinessAttributeInput { + """ + Optional! A custom id to use as the primary key identifier. If not provided, a random UUID will be generated as the id. + """ + id: String + """ name of the business attribute """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java index 8dfec0f22b5acf..e3dc2cb8a8f2f1 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolverTest.java @@ -38,18 +38,22 @@ public class CreateBusinessAttributeResolverTest { + private static final String BUSINESS_ATTRIBUTE_URN = + "urn:li:businessAttribute:business-attribute-1"; private static final String TEST_BUSINESS_ATTRIBUTE_NAME = "test-business-attribute"; private static final String TEST_BUSINESS_ATTRIBUTE_DESCRIPTION = "test-description"; private static final CreateBusinessAttributeInput TEST_INPUT = new CreateBusinessAttributeInput( + BUSINESS_ATTRIBUTE_URN, TEST_BUSINESS_ATTRIBUTE_NAME, TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, SchemaFieldDataType.BOOLEAN); private static final CreateBusinessAttributeInput TEST_INPUT_NULL_NAME = new CreateBusinessAttributeInput( - null, TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, SchemaFieldDataType.BOOLEAN); - private static final String BUSINESS_ATTRIBUTE_URN = - "urn:li:businessAttribute:7d0c4283-de02-4043-aaf2-698b04274658"; + BUSINESS_ATTRIBUTE_URN, + null, + TEST_BUSINESS_ATTRIBUTE_DESCRIPTION, + SchemaFieldDataType.BOOLEAN); private EntityClient mockClient; private EntityService mockService; private QueryContext mockContext; From 8411393720959cfbe3494a8b09a49fff728cec83 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Wed, 27 Mar 2024 22:51:00 +0530 Subject: [PATCH 39/50] business-attributes: Update Business Attribute changes as per current changes in code --- .../datahub/graphql/GmsGraphQLEngine.java | 398 +++++++++--------- .../datahub/graphql/GmsGraphQLEngineArgs.java | 1 - .../BusinessAttributeAuthorizationUtils.java | 10 +- .../CreateBusinessAttributeResolver.java | 1 + .../UpdateBusinessAttributeResolver.java | 3 +- .../BusinessAttributeType.java | 6 +- .../mappers/BusinessAttributeMapper.java | 32 +- .../types/schemafield/SchemaFieldMapper.java | 10 +- .../SearchDocumentTransformer.java | 3 +- .../indexbuilder/MappingsBuilderTest.java | 2 +- .../hook/BusinessAttributeUpdateHookTest.java | 5 - .../v2/delegates/EntityApiDelegateImpl.java | 9 +- 12 files changed, 243 insertions(+), 237 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 503eb705fd91d0..fab1cdafde2591 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -359,7 +359,6 @@ import com.linkedin.metadata.query.filter.SortCriterion; import com.linkedin.metadata.query.filter.SortOrder; import com.linkedin.metadata.recommendation.RecommendationsService; -import com.linkedin.metadata.secret.SecretService; import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.metadata.service.DataProductService; import com.linkedin.metadata.service.ERModelRelationshipService; @@ -1094,213 +1093,208 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { builder.type( "Mutation", typeWiring -> { + typeWiring + .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) + .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) + .dataFetcher( + "createTag", new CreateTagResolver(this.entityClient, this.entityService)) + .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) + .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) + .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) + .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) + .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) + .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) + .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) + .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) + .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) + .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) + .dataFetcher( + "updateERModelRelationship", + new UpdateERModelRelationshipResolver(this.entityClient)) + .dataFetcher( + "createERModelRelationship", + new CreateERModelRelationshipResolver( + this.entityClient, this.erModelRelationshipService)) + .dataFetcher("addTag", new AddTagResolver(entityService)) + .dataFetcher("addTags", new AddTagsResolver(entityService)) + .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) + .dataFetcher("removeTag", new RemoveTagResolver(entityService)) + .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) + .dataFetcher("addTerm", new AddTermResolver(entityService)) + .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) + .dataFetcher("addTerms", new AddTermsResolver(entityService)) + .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) + .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) + .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) + .dataFetcher( + "updateDescription", + new UpdateDescriptionResolver(entityService, this.entityClient)) + .dataFetcher("addOwner", new AddOwnerResolver(entityService)) + .dataFetcher("addOwners", new AddOwnersResolver(entityService)) + .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) + .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) + .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) + .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) + .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) + .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) + .dataFetcher("removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) + .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) + .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) + .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) + .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) + .dataFetcher( + "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) + .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) + .dataFetcher( + "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) + .dataFetcher( + "updateDeprecation", + new UpdateDeprecationResolver(this.entityClient, this.entityService)) + .dataFetcher( + "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) + .dataFetcher( + "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) + .dataFetcher( + "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher( + "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) + .dataFetcher( + "revokeAccessToken", + new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) + .dataFetcher( + "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "createIngestionExecutionRequest", + new CreateIngestionExecutionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "cancelIngestionExecutionRequest", + new CancelIngestionExecutionRequestResolver(this.entityClient)) + .dataFetcher( + "createTestConnectionRequest", + new CreateTestConnectionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "deleteAssertion", + new DeleteAssertionResolver(this.entityClient, this.entityService)) + .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) + .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) + .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) + .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) + .dataFetcher( + "createGlossaryTerm", + new CreateGlossaryTermResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createGlossaryNode", + new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateParentNode", + new UpdateParentNodeResolver(this.entityService, this.entityClient)) + .dataFetcher( + "deleteGlossaryEntity", + new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) + .dataFetcher( + "addRelatedTerms", + new AddRelatedTermsResolver(this.entityService, this.entityClient)) + .dataFetcher( + "removeRelatedTerms", + new RemoveRelatedTermsResolver(this.entityService, this.entityClient)) + .dataFetcher( + "createNativeUserResetToken", + new CreateNativeUserResetTokenResolver(this.nativeUserService)) + .dataFetcher( + "batchUpdateSoftDeleted", new BatchUpdateSoftDeletedResolver(this.entityService)) + .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) + .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) + .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) + .dataFetcher( + "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) + .dataFetcher( + "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) + .dataFetcher("createPost", new CreatePostResolver(this.postService)) + .dataFetcher("deletePost", new DeletePostResolver(this.postService)) + .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) + .dataFetcher( + "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) + .dataFetcher("createView", new CreateViewResolver(this.viewService)) + .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) + .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) + .dataFetcher( + "updateGlobalViewsSettings", + new UpdateGlobalViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateCorpUserViewsSettings", + new UpdateCorpUserViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateLineage", + new UpdateLineageResolver(this.entityService, this.lineageService)) + .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) + .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) + .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) + .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) + .dataFetcher( + "createDataProduct", new CreateDataProductResolver(this.dataProductService)) + .dataFetcher( + "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) + .dataFetcher( + "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) + .dataFetcher( + "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) + .dataFetcher( + "createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "updateOwnershipType", new UpdateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "deleteOwnershipType", new DeleteOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) + .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) + .dataFetcher( + "createDynamicFormAssignment", + new CreateDynamicFormAssignmentResolver(this.formService)) + .dataFetcher( + "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) + .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) + .dataFetcher( + "upsertStructuredProperties", + new UpsertStructuredPropertiesResolver(this.entityClient)) + .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) + .dataFetcher( + "updateIncidentStatus", + new UpdateIncidentStatusResolver(this.entityClient, this.entityService)); + if (featureFlags.isBusinessAttributeEntityEnabled()) { typeWiring - .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) - .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) - .dataFetcher( - "createTag", new CreateTagResolver(this.entityClient, this.entityService)) - .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) - .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) - .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) - .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) - .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) - .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) - .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) - .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) - .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) - .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) .dataFetcher( - "updateERModelRelationship", - new UpdateERModelRelationshipResolver(this.entityClient)) + "createBusinessAttribute", + new CreateBusinessAttributeResolver( + this.entityClient, this.entityService, this.businessAttributeService)) .dataFetcher( - "createERModelRelationship", - new CreateERModelRelationshipResolver( - this.entityClient, this.erModelRelationshipService)) - .dataFetcher("addTag", new AddTagResolver(entityService)) - .dataFetcher("addTags", new AddTagsResolver(entityService)) - .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) - .dataFetcher("removeTag", new RemoveTagResolver(entityService)) - .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) - .dataFetcher("addTerm", new AddTermResolver(entityService)) - .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) - .dataFetcher("addTerms", new AddTermsResolver(entityService)) - .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) - .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) - .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) + "updateBusinessAttribute", + new UpdateBusinessAttributeResolver( + this.entityClient, this.businessAttributeService)) .dataFetcher( - "updateDescription", - new UpdateDescriptionResolver(entityService, this.entityClient)) - .dataFetcher("addOwner", new AddOwnerResolver(entityService)) - .dataFetcher("addOwners", new AddOwnersResolver(entityService)) - .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) - .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) - .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) - .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) - .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) - .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) + "deleteBusinessAttribute", + new DeleteBusinessAttributeResolver(this.entityClient)) .dataFetcher( - "removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) - .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) - .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) - .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) - .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) - .dataFetcher( - "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) - .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) + "addBusinessAttribute", new AddBusinessAttributeResolver(this.entityService)) .dataFetcher( - "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) - .dataFetcher( - "updateDeprecation", - new UpdateDeprecationResolver(this.entityClient, this.entityService)) - .dataFetcher( - "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) - .dataFetcher( - "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) - .dataFetcher( - "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher( - "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) - .dataFetcher( - "revokeAccessToken", - new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) - .dataFetcher( - "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "createIngestionExecutionRequest", - new CreateIngestionExecutionRequestResolver( - this.entityClient, this.ingestionConfiguration)) - .dataFetcher( - "cancelIngestionExecutionRequest", - new CancelIngestionExecutionRequestResolver(this.entityClient)) - .dataFetcher( - "createTestConnectionRequest", - new CreateTestConnectionRequestResolver( - this.entityClient, this.ingestionConfiguration)) - .dataFetcher( - "deleteAssertion", - new DeleteAssertionResolver(this.entityClient, this.entityService)) - .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) - .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) - .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) - .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) - .dataFetcher( - "createGlossaryTerm", - new CreateGlossaryTermResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createGlossaryNode", - new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateParentNode", - new UpdateParentNodeResolver(this.entityService, this.entityClient)) - .dataFetcher( - "deleteGlossaryEntity", - new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) - .dataFetcher( - "addRelatedTerms", - new AddRelatedTermsResolver(this.entityService, this.entityClient)) - .dataFetcher( - "removeRelatedTerms", - new RemoveRelatedTermsResolver(this.entityService, this.entityClient)) - .dataFetcher( - "createNativeUserResetToken", - new CreateNativeUserResetTokenResolver(this.nativeUserService)) - .dataFetcher( - "batchUpdateSoftDeleted", - new BatchUpdateSoftDeletedResolver(this.entityService)) - .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) - .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) - .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) - .dataFetcher( - "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) - .dataFetcher( - "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) - .dataFetcher("createPost", new CreatePostResolver(this.postService)) - .dataFetcher("deletePost", new DeletePostResolver(this.postService)) - .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) - .dataFetcher( - "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) - .dataFetcher("createView", new CreateViewResolver(this.viewService)) - .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) - .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) - .dataFetcher( - "updateGlobalViewsSettings", - new UpdateGlobalViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateCorpUserViewsSettings", - new UpdateCorpUserViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateLineage", - new UpdateLineageResolver(this.entityService, this.lineageService)) - .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) - .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) - .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) - .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) - .dataFetcher( - "createDataProduct", new CreateDataProductResolver(this.dataProductService)) - .dataFetcher( - "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) - .dataFetcher( - "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) - .dataFetcher( - "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) - .dataFetcher( - "createOwnershipType", - new CreateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "updateOwnershipType", - new UpdateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "deleteOwnershipType", - new DeleteOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) - .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) - .dataFetcher( - "createDynamicFormAssignment", - new CreateDynamicFormAssignmentResolver(this.formService)) - .dataFetcher( - "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) - .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) - .dataFetcher( - "upsertStructuredProperties", - new UpsertStructuredPropertiesResolver(this.entityClient)) - .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) - .dataFetcher( - "updateIncidentStatus", - new UpdateIncidentStatusResolver(this.entityClient, this.entityService))); - if (featureFlags.isBusinessAttributeEntityEnabled()) { - typeWiring - .dataFetcher( - "createBusinessAttribute", - new CreateBusinessAttributeResolver( - this.entityClient, this.entityService, this.businessAttributeService)) - .dataFetcher( - "updateBusinessAttribute", - new UpdateBusinessAttributeResolver( - this.entityClient, this.businessAttributeService)) - .dataFetcher( - "deleteBusinessAttribute", - new DeleteBusinessAttributeResolver(this.entityClient)) - .dataFetcher( - "addBusinessAttribute", new AddBusinessAttributeResolver(this.entityService)) - .dataFetcher( - "removeBusinessAttribute", - new RemoveBusinessAttributeResolver(this.entityService)); - } - return typeWiring; + "removeBusinessAttribute", + new RemoveBusinessAttributeResolver(this.entityService)); + } + return typeWiring; }); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java index 1bb231b76336b2..abb491814c278e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java @@ -23,7 +23,6 @@ import com.linkedin.metadata.graph.SiblingGraphService; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.recommendation.RecommendationsService; -import com.linkedin.metadata.secret.SecretService; import com.linkedin.metadata.service.BusinessAttributeService; import com.linkedin.metadata.service.DataProductService; import com.linkedin.metadata.service.ERModelRelationshipService; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java index c5ac56a13040b6..041f5e9ade77f0 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/BusinessAttributeAuthorizationUtils.java @@ -1,10 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.businessattribute; +import com.datahub.authorization.AuthUtil; import com.datahub.authorization.ConjunctivePrivilegeGroup; import com.datahub.authorization.DisjunctivePrivilegeGroup; import com.google.common.collect.ImmutableList; import com.linkedin.datahub.graphql.QueryContext; -import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.metadata.authorization.PoliciesConfig; import javax.annotation.Nonnull; @@ -20,8 +20,8 @@ public static boolean canCreateBusinessAttribute(@Nonnull QueryContext context) new ConjunctivePrivilegeGroup( ImmutableList.of( PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())))); - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), context.getActorUrn(), orPrivilegeGroups); + return AuthUtil.isAuthorized( + context.getAuthorizer(), context.getActorUrn(), orPrivilegeGroups, null); } public static boolean canManageBusinessAttribute(@Nonnull QueryContext context) { @@ -31,7 +31,7 @@ public static boolean canManageBusinessAttribute(@Nonnull QueryContext context) new ConjunctivePrivilegeGroup( ImmutableList.of( PoliciesConfig.MANAGE_BUSINESS_ATTRIBUTE_PRIVILEGE.getType())))); - return AuthorizationUtils.isAuthorized( - context.getAuthorizer(), context.getActorUrn(), orPrivilegeGroups); + return AuthUtil.isAuthorized( + context.getAuthorizer(), context.getActorUrn(), orPrivilegeGroups, null); } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java index 93de29d8c1bf00..3c4f6315016fd2 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/CreateBusinessAttributeResolver.java @@ -91,6 +91,7 @@ public CompletableFuture get(DataFetchingEnvironment environm OwnerEntityType.CORP_USER, _entityService); return BusinessAttributeMapper.map( + context, businessAttributeService.getBusinessAttributeEntityResponse( businessAttributeUrn, context.getAuthentication())); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java index eff3a213adb073..32724ba13e8ac4 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/businessattribute/UpdateBusinessAttributeResolver.java @@ -58,6 +58,7 @@ public CompletableFuture get(DataFetchingEnvironment environm Urn updatedBusinessAttributeUrn = updateBusinessAttribute(input, businessAttributeUrn, context); return BusinessAttributeMapper.map( + context, businessAttributeService.getBusinessAttributeEntityResponse( updatedBusinessAttributeUrn, context.getAuthentication())); } catch (DataHubGraphQLException e) { @@ -124,7 +125,7 @@ private Urn updateBusinessAttribute( } @Nullable - public BusinessAttributeInfo getBusinessAttributeInfo( + private BusinessAttributeInfo getBusinessAttributeInfo( @Nonnull final Urn businessAttributeUrn, @Nonnull final Authentication authentication) { Objects.requireNonNull(businessAttributeUrn, "businessAttributeUrn must not be null"); Objects.requireNonNull(authentication, "authentication must not be null"); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java index 63575ea08336fc..5acfba5a1536ea 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/BusinessAttributeType.java @@ -91,7 +91,7 @@ public List> batchLoad( gmsResult == null ? null : DataFetcherResult.newResult() - .data(BusinessAttributeMapper.map(gmsResult)) + .data(BusinessAttributeMapper.map(context, gmsResult)) .build()) .collect(Collectors.toList()); } catch (Exception e) { @@ -116,7 +116,7 @@ public SearchResults search( facetFilters, start, count); - return UrnSearchResultsMapper.map(searchResult); + return UrnSearchResultsMapper.map(context, searchResult); } @Override @@ -130,6 +130,6 @@ public AutoCompleteResults autoComplete( final AutoCompleteResult result = _entityClient.autoComplete( context.getOperationContext(), "businessAttribute", query, filters, limit); - return AutoCompleteResultsMapper.map(result); + return AutoCompleteResultsMapper.map(context, result); } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java index 1c5c2e7eb14d65..87230b24577163 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/businessattribute/mappers/BusinessAttributeMapper.java @@ -9,6 +9,7 @@ import com.linkedin.common.Ownership; import com.linkedin.common.urn.Urn; import com.linkedin.data.DataMap; +import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.BusinessAttribute; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.SchemaFieldDataType; @@ -23,17 +24,20 @@ import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EnvelopedAspectMap; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class BusinessAttributeMapper implements ModelMapper { public static final BusinessAttributeMapper INSTANCE = new BusinessAttributeMapper(); - public static BusinessAttribute map(@Nonnull final EntityResponse entityResponse) { - return INSTANCE.apply(entityResponse); + public static BusinessAttribute map( + @Nullable final QueryContext context, @Nonnull final EntityResponse entityResponse) { + return INSTANCE.apply(context, entityResponse); } @Override - public BusinessAttribute apply(@Nonnull final EntityResponse entityResponse) { + public BusinessAttribute apply( + @Nullable final QueryContext context, @Nonnull final EntityResponse entityResponse) { BusinessAttribute result = new BusinessAttribute(); result.setUrn(entityResponse.getUrn().toString()); result.setType(EntityType.BUSINESS_ATTRIBUTE); @@ -43,23 +47,27 @@ public BusinessAttribute apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult( BUSINESS_ATTRIBUTE_INFO_ASPECT_NAME, ((businessAttribute, dataMap) -> - mapBusinessAttributeInfo(businessAttribute, dataMap, entityResponse.getUrn()))); + mapBusinessAttributeInfo( + context, businessAttribute, dataMap, entityResponse.getUrn()))); mappingHelper.mapToResult( OWNERSHIP_ASPECT_NAME, (businessAttribute, dataMap) -> businessAttribute.setOwnership( - OwnershipMapper.map(new Ownership(dataMap), entityResponse.getUrn()))); + OwnershipMapper.map(context, new Ownership(dataMap), entityResponse.getUrn()))); mappingHelper.mapToResult( INSTITUTIONAL_MEMORY_ASPECT_NAME, (dataset, dataMap) -> dataset.setInstitutionalMemory( InstitutionalMemoryMapper.map( - new InstitutionalMemory(dataMap), entityResponse.getUrn()))); + context, new InstitutionalMemory(dataMap), entityResponse.getUrn()))); return mappingHelper.getResult(); } private void mapBusinessAttributeInfo( - BusinessAttribute businessAttribute, DataMap dataMap, Urn entityUrn) { + final QueryContext context, + BusinessAttribute businessAttribute, + DataMap dataMap, + Urn entityUrn) { BusinessAttributeInfo businessAttributeInfo = new BusinessAttributeInfo(dataMap); com.linkedin.datahub.graphql.generated.BusinessAttributeInfo attributeInfo = new com.linkedin.datahub.graphql.generated.BusinessAttributeInfo(); @@ -70,17 +78,19 @@ private void mapBusinessAttributeInfo( attributeInfo.setDescription(businessAttributeInfo.getDescription()); } if (businessAttributeInfo.hasCreated()) { - attributeInfo.setCreated(AuditStampMapper.map(businessAttributeInfo.getCreated())); + attributeInfo.setCreated(AuditStampMapper.map(context, businessAttributeInfo.getCreated())); } if (businessAttributeInfo.hasLastModified()) { - attributeInfo.setLastModified(AuditStampMapper.map(businessAttributeInfo.getLastModified())); + attributeInfo.setLastModified( + AuditStampMapper.map(context, businessAttributeInfo.getLastModified())); } if (businessAttributeInfo.hasGlobalTags()) { - attributeInfo.setTags(GlobalTagsMapper.map(businessAttributeInfo.getGlobalTags(), entityUrn)); + attributeInfo.setTags( + GlobalTagsMapper.map(context, businessAttributeInfo.getGlobalTags(), entityUrn)); } if (businessAttributeInfo.hasGlossaryTerms()) { attributeInfo.setGlossaryTerms( - GlossaryTermsMapper.map(businessAttributeInfo.getGlossaryTerms(), entityUrn)); + GlossaryTermsMapper.map(context, businessAttributeInfo.getGlossaryTerms(), entityUrn)); } if (businessAttributeInfo.hasType()) { attributeInfo.setType(mapSchemaFieldDataType(businessAttributeInfo.getType())); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java index 047494663f5a58..85a6b9108cb54e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/schemafield/SchemaFieldMapper.java @@ -41,11 +41,11 @@ public SchemaFieldEntity apply( ((schemaField, dataMap) -> schemaField.setStructuredProperties( StructuredPropertiesMapper.map(context, new StructuredProperties(dataMap))))); - mappingHelper.mapToResult( - BUSINESS_ATTRIBUTE_ASPECT, - (((schemaField, dataMap) -> - schemaField.setBusinessAttributes( - BusinessAttributesMapper.map(new BusinessAttributes(dataMap), entityUrn))))); + mappingHelper.mapToResult( + BUSINESS_ATTRIBUTE_ASPECT, + (((schemaField, dataMap) -> + schemaField.setBusinessAttributes( + BusinessAttributesMapper.map(new BusinessAttributes(dataMap), entityUrn))))); return result; } diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java index d090fddb6df3b9..d1c9b4cdc266f1 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/transformer/SearchDocumentTransformer.java @@ -126,7 +126,8 @@ public Optional transformAspect( Optional result = Optional.empty(); - if (!extractedSearchableFields.isEmpty() || !extractedSearchScoreFields.isEmpty() + if (!extractedSearchableFields.isEmpty() + || !extractedSearchScoreFields.isEmpty() || !extractedSearchRefFields.isEmpty()) { final ObjectNode searchDocument = JsonNodeFactory.instance.objectNode(); searchDocument.put("urn", urn.toString()); diff --git a/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java b/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java index 0e7042e7002dab..9185e2e7ee072d 100644 --- a/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java +++ b/metadata-io/src/test/java/com/linkedin/metadata/search/indexbuilder/MappingsBuilderTest.java @@ -288,7 +288,7 @@ public void testRefMappingsBuilder() { Map result = MappingsBuilder.getMappings(entitySpec); assertEquals(result.size(), 1); Map properties = (Map) result.get("properties"); - assertEquals(properties.size(), 6); + assertEquals(properties.size(), 7); ImmutableMap expectedURNField = ImmutableMap.of( "type", diff --git a/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java index f7daf453d4676e..68cd2aa565b9fc 100644 --- a/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java +++ b/metadata-jobs/pe-consumer/src/test/java/com/datahub/event/hook/BusinessAttributeUpdateHookTest.java @@ -22,7 +22,6 @@ import com.linkedin.data.DataMap; import com.linkedin.entity.Aspect; import com.linkedin.entity.EnvelopedAspect; -import com.linkedin.entity.client.SystemRestliEntityClient; import com.linkedin.events.metadata.ChangeType; import com.linkedin.metadata.Constants; import com.linkedin.metadata.entity.EntityService; @@ -61,9 +60,6 @@ public class BusinessAttributeUpdateHookTest { private static final long EVENT_TIME = 123L; private static final String TEST_ACTOR_URN = "urn:li:corpuser:test"; private static Urn actorUrn; - - private static SystemRestliEntityClient mockClient; - private GraphService mockGraphService; private EntityService mockEntityService; private BusinessAttributeUpdateHook businessAttributeUpdateHook; @@ -74,7 +70,6 @@ public void setupTest() throws URISyntaxException { mockGraphService = Mockito.mock(GraphService.class); mockEntityService = Mockito.mock(EntityService.class); actorUrn = Urn.createFromString(TEST_ACTOR_URN); - mockClient = Mockito.mock(SystemRestliEntityClient.class); businessAttributeServiceHook = new BusinessAttributeUpdateHookService( mockGraphService, mockEntityService, ENTITY_REGISTRY, 100); diff --git a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java index e010bbc97170ac..18bd4b3f10a65f 100644 --- a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java +++ b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java @@ -957,7 +957,10 @@ public ResponseEntity deleteFormInfo(String urn) { } public ResponseEntity createBusinessAttributeInfo( - BusinessAttributeInfoAspectRequestV2 body, String urn) { + BusinessAttributeInfoAspectRequestV2 body, + String urn, + @Nullable Boolean createIfNotExists, + @Nullable Boolean createEntityIfNotExists) { String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return createAspect( @@ -965,7 +968,9 @@ public ResponseEntity createBusinessAttri methodNameToAspectName(methodName), body, BusinessAttributeInfoAspectRequestV2.class, - BusinessAttributeInfoAspectResponseV2.class); + BusinessAttributeInfoAspectResponseV2.class, + createIfNotExists, + createEntityIfNotExists); } public ResponseEntity deleteBusinessAttributeInfo(String urn) { From baa052b54def89719d13ffd07725d9c037ca411b Mon Sep 17 00:00:00 2001 From: Kartikey Khandelwal Date: Thu, 28 Mar 2024 17:56:41 +0530 Subject: [PATCH 40/50] Business Attributes: UI Schema Field Entity Changes --- .../shared/tabs/Dataset/Schema/SchemaTable.tsx | 5 ++--- .../SchemaFieldDrawer/FieldAttribute.tsx | 8 +++----- .../SchemaFieldDrawer/FieldDescription.tsx | 4 ++-- .../Schema/utils/useBusinessAttributeRenderer.tsx | 15 ++++----------- .../Schema/utils/useDescriptionRenderer.tsx | 14 +++++--------- .../Schema/utils/useTagsAndTermsRenderer.tsx | 4 ++-- .../shared/businessAttribute/AttributeContent.tsx | 8 +++----- .../businessAttribute/BusinessAttributeGroup.tsx | 6 +++--- 8 files changed, 24 insertions(+), 40 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx index fbeade6ce7df60..e085d9f6249922 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx @@ -100,7 +100,7 @@ export default function SchemaTable({ const schemaFields = schemaMetadata ? schemaMetadata.fields : inputFields; - const descriptionRender = useDescriptionRenderer(editableSchemaMetadata); + const descriptionRender = useDescriptionRenderer(); const usageStatsRenderer = useUsageStatsRenderer(usageStats); const tagRenderer = useTagsAndTermsRenderer( editableSchemaMetadata, @@ -121,9 +121,8 @@ export default function SchemaTable({ false, ); const businessAttributeRenderer = useBusinessAttributeRenderer( - editableSchemaMetadata, filterText, - false, + false ); const schemaTitleRenderer = useSchemaTitleRenderer(schemaMetadata, setSelectedFkFieldPath, filterText); const schemaBlameRenderer = useSchemaBlameRenderer(schemaFieldBlameList); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx index 75a7f586bcf91f..d688a80ce3f5b7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldAttribute.tsx @@ -1,18 +1,16 @@ import React from 'react'; -import { EditableSchemaMetadata, SchemaField } from '../../../../../../../../types.generated'; +import { SchemaField } from '../../../../../../../../types.generated'; import useBusinessAttributeRenderer from '../../utils/useBusinessAttributeRenderer'; import { SectionHeader, StyledDivider } from './components'; import SchemaEditableContext from '../../../../../../../shared/SchemaEditableContext'; interface Props { expandedField: SchemaField; - editableSchemaMetadata?: EditableSchemaMetadata | null; } -export default function FieldAttribute({ expandedField, editableSchemaMetadata }: Props) { +export default function FieldAttribute({ expandedField }: Props) { const isSchemaEditable = React.useContext(SchemaEditableContext); const attributeRenderer = useBusinessAttributeRenderer( - editableSchemaMetadata, '', isSchemaEditable, ); @@ -22,7 +20,7 @@ export default function FieldAttribute({ expandedField, editableSchemaMetadata } Business Attribute {/* pass in globalTags since this is a shared component, tags will not be shown or used */}
- {attributeRenderer(editableSchemaMetadata, expandedField)} + {attributeRenderer('', expandedField)}
diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx index 9c631d769e7791..2cd35c0f5c5b2c 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldDescription.tsx @@ -77,8 +77,8 @@ export default function FieldDescription({ expandedField, editableFieldInfo }: P }); const displayedDescription = editableFieldInfo?.description || expandedField.description; - const baDescription = editableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.description; - const baUrn = editableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.urn; + const baDescription = expandedField?.schemaFieldEntity?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.description; + const baUrn = expandedField?.schemaFieldEntity?.businessAttributes?.businessAttribute?.businessAttribute?.urn; return ( <> diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx index 9ac8f91bb7a677..a8c1f9216a7d60 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx @@ -1,16 +1,13 @@ import React from 'react'; -import { EditableSchemaMetadata, EntityType, SchemaField } from '../../../../../../../types.generated'; -import { pathMatchesNewPath } from '../../../../../dataset/profile/schema/utils/utils'; -import { useMutationUrn, useRefetch } from '../../../../EntityContext'; +import { EntityType, SchemaField } from '../../../../../../../types.generated'; +import { useRefetch } from '../../../../EntityContext'; import { useSchemaRefetch } from '../SchemaContext'; import BusinessAttributeGroup from '../../../../../../shared/businessAttribute/BusinessAttributeGroup'; export default function useBusinessAttributeRenderer( - editableSchemaMetadata: EditableSchemaMetadata | null | undefined, filterText: string, canEdit: boolean, ) { - const urn = useMutationUrn(); const refetch = useRefetch(); const schemaRefetch = useSchemaRefetch(); @@ -20,17 +17,13 @@ export default function useBusinessAttributeRenderer( }; return (businessAttribute: string, record: SchemaField): JSX.Element => { - const relevantEditableFieldInfo = editableSchemaMetadata?.editableSchemaFieldInfo.find( - (candidateEditableFieldInfo) => pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, record.fieldPath), - ); - return ( { - const relevantEditableFieldInfo = editableSchemaMetadata?.editableSchemaFieldInfo.find( - (candidateEditableFieldInfo) => pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, record.fieldPath), - ); - const displayedDescription = relevantEditableFieldInfo?.description || description; + const displayedDescription = record?.description || description; const sanitizedDescription = DOMPurify.sanitize(displayedDescription); const original = record.description ? DOMPurify.sanitize(record.description) : undefined; const businessAttributeDescription = - relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties + record?.schemaFieldEntity?.businessAttributes?.businessAttribute?.businessAttribute?.properties ?.description || ''; const handleExpandedRows = (expanded) => setExpandedRows((prev) => ({ ...prev, [index]: expanded })); @@ -43,7 +39,7 @@ export default function useDescriptionRenderer(editableSchemaMetadata: EditableS baExpanded={!!expandedBARows[index]} description={sanitizedDescription} original={original} - isEdited={!!relevantEditableFieldInfo?.description} + isEdited={!!record.description} onUpdate={(updatedDescription) => updateDescription({ variables: { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx index bd452dfb492d0c..4dd11e3ee80c57 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useTagsAndTermsRenderer.tsx @@ -25,8 +25,8 @@ export default function useTagsAndTermsRenderer( (candidateEditableFieldInfo) => pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, record.fieldPath), ); - const businessAttributeTags = relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.tags?.tags || []; - const businessAttributeTerms = relevantEditableFieldInfo?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.glossaryTerms?.terms || []; + const businessAttributeTags = record?.schemaFieldEntity?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.tags?.tags || []; + const businessAttributeTerms = record?.schemaFieldEntity?.businessAttributes?.businessAttribute?.businessAttribute?.properties?.glossaryTerms?.terms || []; return ( From ee865dd595051e2f307488483f4a0e519a96783e Mon Sep 17 00:00:00 2001 From: "Shukla, Amit" Date: Wed, 3 Apr 2024 03:47:18 +0530 Subject: [PATCH 41/50] feat(search): Add SchemaFieldEntity to search functionality --- .../graphql/resolvers/search/SearchUtils.java | 3 +- .../src/app/buildEntityRegistry.ts | 2 + .../SchemaFieldPropertiesEntity.tsx | 53 +++++++++++++++++++ .../entity/schemaField/preview/Preview.tsx | 50 +++++++++++++++++ datahub-web-react/src/graphql/search.graphql | 23 ++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx create mode 100644 datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java index ef7df22538acc3..c0c56cdf8dd2c9 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java @@ -72,7 +72,8 @@ private SearchUtils() {} EntityType.DOMAIN, EntityType.DATA_PRODUCT, EntityType.NOTEBOOK, - EntityType.BUSINESS_ATTRIBUTE); + EntityType.BUSINESS_ATTRIBUTE, + EntityType.SCHEMA_FIELD); /** Entities that are part of autocomplete by default in Auto Complete Across Entities */ public static final List AUTO_COMPLETE_ENTITY_TYPES = diff --git a/datahub-web-react/src/app/buildEntityRegistry.ts b/datahub-web-react/src/app/buildEntityRegistry.ts index d072f125fce664..ed207220830326 100644 --- a/datahub-web-react/src/app/buildEntityRegistry.ts +++ b/datahub-web-react/src/app/buildEntityRegistry.ts @@ -23,6 +23,7 @@ import { ERModelRelationshipEntity } from './entity/ermodelrelationships/ERModel import { RoleEntity } from './entity/Access/RoleEntity'; import { RestrictedEntity } from './entity/restricted/RestrictedEntity'; import {BusinessAttributeEntity} from "./entity/businessAttribute/BusinessAttributeEntity"; +import { SchemaFieldPropertiesEntity } from './entity/schemaField/SchemaFieldPropertiesEntity'; export default function buildEntityRegistry() { const registry = new EntityRegistry(); @@ -50,5 +51,6 @@ export default function buildEntityRegistry() { registry.register(new ERModelRelationshipEntity()) registry.register(new RestrictedEntity()); registry.register(new BusinessAttributeEntity()); + registry.register(new SchemaFieldPropertiesEntity()); return registry; } \ No newline at end of file diff --git a/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx b/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx new file mode 100644 index 00000000000000..4be0fa81a23f98 --- /dev/null +++ b/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { PicCenterOutlined } from '@ant-design/icons'; +import { EntityType, SchemaFieldEntity, SearchResult } from '../../../types.generated'; +import { Entity, IconStyleType, PreviewType } from '../Entity'; +import { getDataForEntityType } from '../shared/containers/profile/utils'; +import { Preview } from './preview/Preview'; + +export class SchemaFieldPropertiesEntity implements Entity { + type: EntityType = EntityType.SchemaField; + + icon = (fontSize: number, styleType: IconStyleType, color = '#BFBFBF') => ( + + ); + + isSearchEnabled = () => true; + + isBrowseEnabled = () => false; + + isLineageEnabled = () => false; + + // Currently unused. + getAutoCompleteFieldName = () => 'schemaField'; + + // Currently unused. + getPathName = () => 'schemaField'; + + // Currently unused. + getEntityName = () => 'schemaField'; + + // Currently unused. + getCollectionName = () => 'schemaFields'; + + // Currently unused. + renderProfile = (_: string) => <>; + + renderPreview = (previewType: PreviewType, data: SchemaFieldEntity) => ( + + ); + + renderSearch = (result: SearchResult) => this.renderPreview(PreviewType.SEARCH, result.entity as SchemaFieldEntity); + + displayName = (data: SchemaFieldEntity) => data?.fieldPath || data.urn; + + getGenericEntityProperties = (data: SchemaFieldEntity) => + getDataForEntityType({ data, entityType: this.type, getOverrideProperties: (newData) => newData }); + + supportedCapabilities = () => new Set([]); +} diff --git a/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx b/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx new file mode 100644 index 00000000000000..2ac2be19ece89c --- /dev/null +++ b/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { PicCenterOutlined } from '@ant-design/icons'; +import { useLocation } from 'react-router-dom'; +import { EntityType, Owner } from '../../../../types.generated'; +import DefaultPreviewCard from '../../../preview/DefaultPreviewCard'; +import { useEntityRegistry } from '../../../useEntityRegistry'; +import { IconStyleType, PreviewType } from '../../Entity'; +import UrlButton from '../../shared/UrlButton'; +import { getRelatedEntitiesUrl } from '../../../businessAttribute/businessAttributeUtils'; + +export const Preview = ({ + datasetUrn, + businessAttributeUrn, + name, + description, + owners, + previewType, +}: { + datasetUrn: string; + businessAttributeUrn: string; + name: string; + description?: string | null; + owners?: Array | null; + previewType: PreviewType; +}): JSX.Element => { + const entityRegistry = useEntityRegistry(); + const location = useLocation(); + const relatedEntitiesUrl = getRelatedEntitiesUrl(entityRegistry, businessAttributeUrn); + + const url = `${entityRegistry.getEntityUrl(EntityType.Dataset, datasetUrn)}/${encodeURIComponent('Schema')}?schemaFilter=${encodeURIComponent('customer_id')}`; + + return ( + } + type="Column" + typeIcon={entityRegistry.getIcon(EntityType.SchemaField, 14, IconStyleType.ACCENT)} + entityTitleSuffix={ + decodeURIComponent(location.pathname) !== decodeURIComponent(relatedEntitiesUrl) && ( + View Related Entities + ) + } + /> + ); +}; \ No newline at end of file diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index 2415cee7f9e006..aff506779094f3 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -854,6 +854,9 @@ fragment searchResultFields on Entity { ... on BusinessAttribute { ...businessAttributeFields } + ... on SchemaFieldEntity { + ...entityField + } } fragment facetFields on FacetMetadata { @@ -961,6 +964,26 @@ fragment searchResults on SearchResults { } } +fragment entityField on SchemaFieldEntity { + urn + type + parent { + urn + type + } + fieldPath + structuredProperties { + properties { + ...structuredPropertiesFields + } + } + businessAttributes { + businessAttribute { + ...businessAttribute + } + } +} + fragment schemaFieldEntityFields on SchemaFieldEntity { urn type From 7997a1273d32652bb20af13a01cc3b5c4e3021e9 Mon Sep 17 00:00:00 2001 From: Kartikey Khandelwal Date: Fri, 29 Mar 2024 01:57:34 +0530 Subject: [PATCH 42/50] Business Attributes: UI Show|Hide Feature Flag Implementation | Modified Test Cases --- .../resolvers/config/AppConfigResolver.java | 1 + .../src/main/resources/app.graphql | 5 ++ datahub-web-react/src/Mocks.tsx | 18 +++++- datahub-web-react/src/app/SearchRoutes.tsx | 15 ++++- .../businessAttribute/BusinessAttributes.tsx | 5 +- .../__tests__/AccessManagement.test.ts | 6 ++ .../tabs/Dataset/Schema/SchemaTable.tsx | 7 ++- .../SchemaFieldDrawer/FieldAttribute.tsx | 7 ++- .../SchemaFieldDrawer/SchemaFieldDrawer.tsx | 2 +- .../utils/useBusinessAttributeRenderer.tsx | 11 ++-- .../src/app/home/HomePageRecommendations.tsx | 24 +++++++- .../src/app/shared/admin/HeaderLinks.tsx | 8 ++- .../AddBusinessAttributeModal.tsx | 6 +- .../businessAttribute/AttributeContent.tsx | 4 +- datahub-web-react/src/app/useAppConfig.ts | 10 ++++ datahub-web-react/src/appConfigContext.tsx | 1 + datahub-web-react/src/graphql/app.graphql | 1 + .../businessAttribute/attribute_mutations.js | 26 ++++++++- .../businessAttribute/businessAttribute.js | 51 ++++++++++++---- .../cypress/e2e/mutations/mutations.js | 58 +++++++++++++------ .../tests/cypress/cypress/support/commands.js | 11 ++++ 21 files changed, 221 insertions(+), 56 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java index 90c6445060621f..c05009e146308e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java @@ -176,6 +176,7 @@ public CompletableFuture get(final DataFetchingEnvironment environmen final FeatureFlagsConfig featureFlagsConfig = FeatureFlagsConfig.builder() .setShowSearchFiltersV2(_featureFlags.isShowSearchFiltersV2()) + .setBusinessAttributeEntityEnabled(_featureFlags.isBusinessAttributeEntityEnabled()) .setReadOnlyModeEnabled(_featureFlags.isReadOnlyModeEnabled()) .setShowBrowseV2(_featureFlags.isShowBrowseV2()) .setShowAcrylInfo(_featureFlags.isShowAcrylInfo()) diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index 1f1c5fc5a3a7bc..c8fb2dedd59284 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -482,6 +482,11 @@ type FeatureFlagsConfig { If this is off, Domains appear "flat" again. """ nestedDomainsEnabled: Boolean! + + """ + Whether business attribute entity should be shown + """ + businessAttributeEntityEnabled: Boolean! } """ diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 69d880dabfe220..c7e0a89ab38ea0 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -1462,7 +1462,10 @@ export const businessAttribute = { terms: [ { term: { - urn: 'urn:li:glossaryTerm:1' + urn: 'urn:li:glossaryTerm:1', + type: EntityType.GlossaryTerm, + hierarchicalName: 'SampleHierarchicalName', + name: 'SampleName', }, associatedUrn: 'urn:li:businessAttribute:ba1' } @@ -1475,7 +1478,9 @@ export const businessAttribute = { { tag: { urn: 'urn:li:tag:abc-sample-tag', - __typename: 'Tag' + __typename: 'Tag', + type: EntityType.Tag, + name: 'abc-sample-tag', }, __typename: 'TagAssociation', associatedUrn: 'urn:li:businessAttribute:ba1' @@ -1483,7 +1488,9 @@ export const businessAttribute = { { tag: { urn: 'urn:li:tag:TestTag', - __typename: 'Tag' + __typename: 'Tag', + type: EntityType.Tag, + name: 'TestTag', }, __typename: 'TagAssociation', associatedUrn: 'urn:li:businessAttribute:ba1' @@ -1494,16 +1501,19 @@ export const businessAttribute = { { key: 'prop2', value: 'val2', + associatedUrn: 'urn:li:businessAttribute:ba1', __typename: 'CustomPropertiesEntry' }, { key: 'prop1', value: 'val1', + associatedUrn: 'urn:li:businessAttribute:ba1', __typename: 'CustomPropertiesEntry' }, { key: 'prop3', value: 'val3', + associatedUrn: 'urn:li:businessAttribute:ba1', __typename: 'CustomPropertiesEntry' } ] @@ -3615,6 +3625,8 @@ export const mocks = [ manageGlobalViews: true, manageOwnershipTypes: true, manageGlobalAnnouncements: true, + createBusinessAttributes: true, + manageBusinessAttributes: true, }, }, }, diff --git a/datahub-web-react/src/app/SearchRoutes.tsx b/datahub-web-react/src/app/SearchRoutes.tsx index 766c6689c3fcac..4ebcc6f090a4bc 100644 --- a/datahub-web-react/src/app/SearchRoutes.tsx +++ b/datahub-web-react/src/app/SearchRoutes.tsx @@ -12,7 +12,7 @@ import { ManageIngestionPage } from './ingest/ManageIngestionPage'; import GlossaryRoutes from './glossary/GlossaryRoutes'; import { SettingsPage } from './settings/SettingsPage'; import DomainRoutes from './domain/DomainRoutes'; -import { useIsNestedDomainsEnabled } from './useAppConfig'; +import { useBusinessAttributesFlag, useIsAppConfigContextLoaded, useIsNestedDomainsEnabled } from './useAppConfig'; import { ManageDomainsPage } from './domain/ManageDomainsPage'; import { BusinessAttributes } from './businessAttribute/BusinessAttributes'; /** @@ -25,6 +25,9 @@ export const SearchRoutes = (): JSX.Element => { ? entityRegistry.getEntitiesForSearchRoutes() : entityRegistry.getNonGlossaryEntities(); + const businessAttributesFlag = useBusinessAttributesFlag(); + const appConfigContextLoaded = useIsAppConfigContextLoaded(); + return ( @@ -50,7 +53,15 @@ export const SearchRoutes = (): JSX.Element => { } /> } /> } /> - } /> + { + if (!appConfigContextLoaded) { + return null; + } + if (businessAttributesFlag) { + return ; + } + return ; + }}/> diff --git a/datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx b/datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx index 7533d67f7b69a3..b16593f5497f6e 100644 --- a/datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx +++ b/datahub-web-react/src/app/businessAttribute/BusinessAttributes.tsx @@ -118,7 +118,7 @@ export const BusinessAttributes = () => { const totalBusinessAttributes = businessAttributeData?.listBusinessAttributes?.total || 0; const businessAttributes = useMemo( - () => businessAttributeData?.listBusinessAttributes?.businessAttributes || [], + () => (businessAttributeData?.listBusinessAttributes?.businessAttributes || []) as BusinessAttribute[], [businessAttributeData], ); @@ -136,7 +136,7 @@ export const BusinessAttributes = () => { businessAttributeRefetch?.(); }, 2000); }; - const tableData = businessAttributes; + const tableData = businessAttributes || []; const tableColumns = [ { width: '20%', @@ -151,6 +151,7 @@ export const BusinessAttributes = () => { title: 'Description', dataIndex: ['properties', 'description'], key: 'description', + width: '20%', // render: (description: string) => description || '', render: descriptionRender, }, diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/__tests__/AccessManagement.test.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/__tests__/AccessManagement.test.ts index 38770fb16b5df9..d34c317e403d2e 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/__tests__/AccessManagement.test.ts +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/__tests__/AccessManagement.test.ts @@ -82,6 +82,8 @@ describe('handleAccessRoles', () => { manageOwnershipTypes: true, manageGlobalAnnouncements: true, manageTokens: true, + createBusinessAttributes: true, + manageBusinessAttributes: true, __typename: 'PlatformPrivileges', }, __typename: 'AuthenticatedUser', @@ -159,6 +161,8 @@ describe('handleAccessRoles', () => { manageOwnershipTypes: true, manageGlobalAnnouncements: true, manageTokens: true, + createBusinessAttributes: true, + manageBusinessAttributes: true, __typename: 'PlatformPrivileges', }, __typename: 'AuthenticatedUser', @@ -252,6 +256,8 @@ describe('handleAccessRoles', () => { manageOwnershipTypes: true, manageGlobalAnnouncements: true, manageTokens: true, + createBusinessAttributes: true, + manageBusinessAttributes: true, __typename: 'PlatformPrivileges', }, __typename: 'AuthenticatedUser', diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx index e085d9f6249922..a2176b5637be86 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx @@ -26,6 +26,7 @@ import translateFieldPath from '../../../../dataset/profile/schema/utils/transla import PropertiesColumn from './components/PropertiesColumn'; import SchemaFieldDrawer from './components/SchemaFieldDrawer/SchemaFieldDrawer'; import useBusinessAttributeRenderer from './utils/useBusinessAttributeRenderer'; +import { useBusinessAttributesFlag } from '../../../../../useAppConfig'; const TableContainer = styled.div` overflow: inherit; @@ -90,6 +91,7 @@ export default function SchemaTable({ hasProperties, inputFields, }: Props): JSX.Element { + const businessAttributesFlag = useBusinessAttributesFlag(); const hasUsageStats = useMemo(() => (usageStats?.aggregations?.fields?.length || 0) > 0, [usageStats]); const [tableHeight, setTableHeight] = useState(0); const [selectedFkFieldPath, setSelectedFkFieldPath] = useState Business Attribute {/* pass in globalTags since this is a shared component, tags will not be shown or used */} @@ -24,5 +27,5 @@ export default function FieldAttribute({ expandedField }: Props) { - ); + ) : null; } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx index 72914781617507..47e9c7716281e0 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx @@ -76,7 +76,7 @@ export default function SchemaFieldDrawer({ - + )} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx index a8c1f9216a7d60..6bedb96796d41a 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/utils/useBusinessAttributeRenderer.tsx @@ -3,6 +3,7 @@ import { EntityType, SchemaField } from '../../../../../../../types.generated'; import { useRefetch } from '../../../../EntityContext'; import { useSchemaRefetch } from '../SchemaContext'; import BusinessAttributeGroup from '../../../../../../shared/businessAttribute/BusinessAttributeGroup'; +import { useBusinessAttributesFlag } from '../../../../../../useAppConfig'; export default function useBusinessAttributeRenderer( filterText: string, @@ -11,15 +12,17 @@ export default function useBusinessAttributeRenderer( const refetch = useRefetch(); const schemaRefetch = useSchemaRefetch(); + const businessAttributesFlag = useBusinessAttributesFlag(); + const refresh: any = () => { refetch?.(); schemaRefetch?.(); }; - return (businessAttribute: string, record: SchemaField): JSX.Element => { - return ( + return (businessAttribute: string, record: SchemaField): JSX.Element | null => { + return businessAttributesFlag ? ( - ); + ) : null; }; } diff --git a/datahub-web-react/src/app/home/HomePageRecommendations.tsx b/datahub-web-react/src/app/home/HomePageRecommendations.tsx index cc9f4b265455b2..6574b70b20de6a 100644 --- a/datahub-web-react/src/app/home/HomePageRecommendations.tsx +++ b/datahub-web-react/src/app/home/HomePageRecommendations.tsx @@ -21,6 +21,7 @@ import { HOME_PAGE_PLATFORMS_ID, } from '../onboarding/config/HomePageOnboardingConfig'; import { useToggleEducationStepIdsAllowList } from '../onboarding/useToggleEducationStepIdsAllowList'; +import { useBusinessAttributesFlag } from '../useAppConfig'; const PLATFORMS_MODULE_ID = 'Platforms'; const MOST_POPULAR_MODULE_ID = 'HighUsageEntities'; @@ -104,6 +105,8 @@ export const HomePageRecommendations = ({ user }: Props) => { const browseEntityList = entityRegistry.getBrowseEntityTypes(); const userUrn = user?.urn; + const businessAttributesFlag = useBusinessAttributesFlag(); + const showSimplifiedHomepage = user?.settings?.appearance?.showSimplifiedHomepage; const { data: entityCountData } = useGetEntityCountsQuery({ @@ -182,7 +185,22 @@ export const HomePageRecommendations = ({ user }: Props) => { {orderedEntityCounts.map( (entityCount) => entityCount && - entityCount.count !== 0 && ( + entityCount.count !== 0 && + entityCount.entityType !== EntityType.BusinessAttribute && + ( + + ), + )} + {orderedEntityCounts.map( + (entityCount) => + entityCount && + entityCount.count !== 0 && + entityCount.entityType === EntityType.BusinessAttribute && + businessAttributesFlag && ( { (entityCount) => entityCount.entityType === EntityType.GlossaryTerm, ) && } - ) : ( + ) : ( - )} + )} )} {recommendationModules && diff --git a/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx b/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx index 3826776b108957..467e535f9bad46 100644 --- a/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx +++ b/datahub-web-react/src/app/shared/admin/HeaderLinks.tsx @@ -11,7 +11,7 @@ import { } from '@ant-design/icons'; import { Link } from 'react-router-dom'; import { Button, Dropdown, Menu, Tooltip } from 'antd'; -import { useAppConfig } from '../../useAppConfig'; +import { useAppConfig, useBusinessAttributesFlag } from '../../useAppConfig'; import { ANTD_GRAY } from '../../entity/shared/constants'; import { HOME_PAGE_INGESTION_ID } from '../../onboarding/config/HomePageOnboardingConfig'; import { useToggleEducationStepIdsAllowList } from '../../onboarding/useToggleEducationStepIdsAllowList'; @@ -67,6 +67,8 @@ export function HeaderLinks(props: Props) { const me = useUserContext(); const { config } = useAppConfig(); + const businessAttributesFlag = useBusinessAttributesFlag(); + const isAnalyticsEnabled = config?.analyticsConfig.enabled; const isIngestionEnabled = config?.managedIngestionConfig.enabled; @@ -120,7 +122,7 @@ export function HeaderLinks(props: Props) { Manage related groups of data assets - + {businessAttributesFlag && ( Universal field for data consistency - + )} } > diff --git a/datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx b/datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx index 731cd40f33c5e0..88f6a4c9660d3a 100644 --- a/datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx +++ b/datahub-web-react/src/app/shared/businessAttribute/AddBusinessAttributeModal.tsx @@ -201,7 +201,7 @@ export default function EditBusinessAttributeModal({ variables: { input: { businessAttributeUrn: urn, - resourceUrn: resources[0], + resourceUrn: resources, }, }, }) @@ -229,11 +229,11 @@ export default function EditBusinessAttributeModal({ variables: { input: { businessAttributeUrn: urn, - resourceUrn: { + resourceUrn: [{ resourceUrn: resources[0].resourceUrn, subResource: resources[0].subResource, subResourceType: resources[0].subResourceType, - }, + }], }, }, }) diff --git a/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx b/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx index 4aed0ec9bbad8a..61306c9cf64d3a 100644 --- a/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx +++ b/datahub-web-react/src/app/shared/businessAttribute/AttributeContent.tsx @@ -67,11 +67,11 @@ export default function AttributeContent({ variables: { input: { businessAttributeUrn: attributeToRemove.businessAttribute.urn, - resourceUrn: { + resourceUrn: [{ resourceUrn: attributeToRemove.associatedUrn || entityUrn || '', subResource: null, subResourceType: null, - }, + }], }, }, }) diff --git a/datahub-web-react/src/app/useAppConfig.ts b/datahub-web-react/src/app/useAppConfig.ts index 821d00b9017c31..f167ccad16474d 100644 --- a/datahub-web-react/src/app/useAppConfig.ts +++ b/datahub-web-react/src/app/useAppConfig.ts @@ -17,3 +17,13 @@ export function useIsNestedDomainsEnabled() { const appConfig = useAppConfig(); return appConfig.config.featureFlags.nestedDomainsEnabled; } + +export function useBusinessAttributesFlag() { + const appConfig = useAppConfig(); + return appConfig.config.featureFlags.businessAttributeEntityEnabled; +} + +export function useIsAppConfigContextLoaded() { + const appConfig = useAppConfig(); + return appConfig.loaded; +} diff --git a/datahub-web-react/src/appConfigContext.tsx b/datahub-web-react/src/appConfigContext.tsx index 00feaf82234100..b4f16e2d2a8240 100644 --- a/datahub-web-react/src/appConfigContext.tsx +++ b/datahub-web-react/src/appConfigContext.tsx @@ -52,6 +52,7 @@ export const DEFAULT_APP_CONFIG = { showAccessManagement: false, nestedDomainsEnabled: true, platformBrowseV2: false, + businessAttributeEntityEnabled: false, }, }; diff --git a/datahub-web-react/src/graphql/app.graphql b/datahub-web-react/src/graphql/app.graphql index b7527d53b5705f..7b47fc0302247b 100644 --- a/datahub-web-react/src/graphql/app.graphql +++ b/datahub-web-react/src/graphql/app.graphql @@ -67,6 +67,7 @@ query appConfig { showAccessManagement nestedDomainsEnabled platformBrowseV2 + businessAttributeEntityEnabled } } } diff --git a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js index 4b4faaf607e8fb..5bbb19e85d9bc1 100644 --- a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js @@ -1,5 +1,23 @@ +import { aliasQuery, hasOperationName } from "../utils"; + describe("attribute list adding tags and terms", () => { + beforeEach(() => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + aliasQuery(req, "appConfig"); + }); + }); + + const setBusinessAttributeFeatureFlag = (isOn) => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + if (hasOperationName(req, "appConfig")) { + req.reply((res) => { + res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled = isOn; + }); + } + }); + }; it("can create and add a tag to business attribute and visit new tag page", () => { + setBusinessAttributeFeatureFlag(true); cy.login(); cy.goToBusinessAttributeList(); @@ -18,17 +36,17 @@ describe("attribute list adding tags and terms", () => { // wait a breath for elasticsearch to index the tag being applied to the business attribute- if we navigate too quick ES // wont know and we'll see applied to 0 entities - cy.wait(2000); + cy.wait(3000); // go to tag drawer cy.contains("CypressAddTagToAttribute").click({ force: true }); - cy.wait(1000); + cy.wait(3000); // Click the Tag Details to launch full profile cy.contains("Tag Details").click({ force: true }); - cy.wait(1000); + cy.wait(3000); // title of tag page cy.contains("CypressAddTagToAttribute"); @@ -36,6 +54,7 @@ describe("attribute list adding tags and terms", () => { // description of tag page cy.contains("CypressAddTagToAttribute Test Description"); + cy.wait(3000); // used by panel - click to search cy.contains("1 Business Attributes").click({ force: true }); @@ -55,6 +74,7 @@ describe("attribute list adding tags and terms", () => { }); it("can add and remove terms from a business attribute", () => { + setBusinessAttributeFeatureFlag(true); cy.login(); cy.addTermToBusinessAttribute( "urn:li:businessAttribute:cypressTestAttribute", diff --git a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js index 84abde2bfe5b22..d7ac9b0085b186 100644 --- a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js +++ b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js @@ -1,16 +1,37 @@ +import { aliasQuery, hasOperationName } from "../utils"; + describe("businessAttribute", () => { + beforeEach(() => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + aliasQuery(req, "appConfig"); + }); + }); + + const setBusinessAttributeFeatureFlag = (isOn) => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + if (hasOperationName(req, "appConfig")) { + req.reply((res) => { + res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled = isOn; + }); + } + }); + }; + it('go to business attribute page, create attribute ', function () { const urn="urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; const businessAttribute="CypressBusinessAttribute"; const datasetName = "cypress_logging_events"; + setBusinessAttributeFeatureFlag(true); cy.login(); cy.goToBusinessAttributeList(); - cy.clickOptionWithText("Create Business Attribute"); - cy.addViaModal(businessAttribute, "Create Business Attribute", businessAttribute, "create-business-attribute-button"); + cy.addBusinessAttributeViaModal(businessAttribute, "Create Business Attribute", businessAttribute, "create-business-attribute-button"); cy.wait(3000); - cy.goToBusinessAttributeList().contains(businessAttribute).should("be.visible"); + cy.goToBusinessAttributeList() + + cy.wait(3000) + cy.contains(businessAttribute).should("be.visible"); cy.addAttributeToDataset(urn, datasetName, businessAttribute); @@ -38,7 +59,7 @@ describe("businessAttribute", () => { const datasetName = "cypress_logging_events"; const term="CypressTerm"; const tag="Cypress"; - + setBusinessAttributeFeatureFlag(true); cy.login(); cy.addAttributeToDataset(urn, datasetName, businessAttribute); @@ -49,37 +70,46 @@ describe("businessAttribute", () => { it("can visit related entities", () => { const businessAttribute="CypressAttribute"; + setBusinessAttributeFeatureFlag(true); cy.login(); cy.goToBusinessAttributeList(); cy.clickOptionWithText(businessAttribute); cy.clickOptionWithText("Related Entities"); //cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); //cy.wait(5000); - cy.contains("of 0").should("not.exist"); - cy.contains(/of [0-9]+/); + //Uncomment below two lines once schema Field Entity is fixed + // cy.contains("of 0").should("not.exist"); + // cy.contains(/of [0-9]+/); }); it("can search related entities by query", () => { + setBusinessAttributeFeatureFlag(true); cy.login(); cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); cy.get('[placeholder="Filter entities..."]').click().type( "logging{enter}" ); cy.wait(5000); - cy.contains("of 0").should("not.exist"); - cy.contains(/of 1/); - cy.contains("cypress_logging_events"); + //Uncomment below three lines once schema Field Entity is fixed + // cy.contains("of 0").should("not.exist"); + // cy.contains(/of 1/); + // cy.contains("cypress_logging_events"); }); it("remove business attribute from dataset", () => { const urn="urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; const datasetName = "cypress_logging_events"; + setBusinessAttributeFeatureFlag(true); cy.login(); cy.goToDataset(urn, datasetName); cy.wait(3000); - + cy.get('body').then(($body) => { + if ($body.find('button[aria-label="Close"]').length > 0) { + cy.get('button[aria-label="Close"]').click(); + } + }); cy.clickOptionWithText("event_name"); cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => cy @@ -94,6 +124,7 @@ describe("businessAttribute", () => { it("update the data type of a business attribute", () => { const businessAttribute="cypressTestAttribute"; + setBusinessAttributeFeatureFlag(true); cy.login(); cy.goToBusinessAttributeList(); diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js index fb59783ebfba9c..81d4fb159368c1 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js @@ -2,15 +2,30 @@ describe("mutations", () => { before(() => { // warm up elastic by issuing a `*` search cy.login(); - cy.goToStarSearchList(); - cy.wait(5000); + //Commented below function, and used individual commands below with wait + // cy.goToStarSearchList(); + cy.visit("/search?query=%2A"); + cy.wait(3000) + cy.waitTextVisible("Showing") + cy.waitTextVisible("results") + cy.wait(2000); + cy.get('body').then(($body) => { + if ($body.find('button[aria-label="Close"]').length > 0) { + cy.get('button[aria-label="Close"]').click(); + } + }); + cy.wait(2000); }); it("can create and add a tag to dataset and visit new tag page", () => { - cy.deleteUrn("urn:li:tag:CypressTestAddTag"); + // cy.deleteUrn("urn:li:tag:CypressTestAddTag"); cy.login(); cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); - + cy.get('body').then(($body) => { + if ($body.find('button[aria-label="Close"]').length > 0) { + cy.get('button[aria-label="Close"]').click(); + } + }); cy.contains("Add Tag").click({ force: true }); cy.enterTextInTestId("tag-term-modal-input", "CypressTestAddTag"); @@ -28,12 +43,12 @@ describe("mutations", () => { // go to tag drawer cy.contains("CypressTestAddTag").click({ force: true }); - cy.wait(1000); + cy.wait(2000); // Click the Tag Details to launch full profile cy.contains("Tag Details").click({ force: true }); - cy.wait(1000); + cy.wait(2000); // title of tag page cy.contains("CypressTestAddTag"); @@ -42,19 +57,23 @@ describe("mutations", () => { cy.contains("CypressTestAddTag Test Description"); // used by panel - click to search - cy.contains("1 Datasets").click({ force: true }); + //Uncomment below line once schema Field Entity is fixed + // cy.contains("1 Datasets").click({ force: true }); // verify dataset shows up in search now - cy.contains("of 1 result").click({ force: true }); - cy.contains("cypress_logging_events").click({ force: true }); + //Uncomment below line once schema Field Entity is fixed + // cy.contains("of 1 result").click({ force: true }); + //Uncomment below line once schema Field Entity is fixed + // cy.contains("cypress_logging_events").click({ force: true }); + //Remove below line once schema Field Entity is fixed + cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); cy.get('[data-testid="tag-CypressTestAddTag"]').within(() => cy.get("span[aria-label=close]").click() ); cy.contains("Yes").click(); cy.contains("CypressTestAddTag").should("not.exist"); - - cy.deleteUrn("urn:li:tag:CypressTestAddTag"); + // cy.deleteUrn("urn:li:tag:CypressTestAddTag"); }); it("can add and remove terms from a dataset", () => { @@ -97,12 +116,12 @@ describe("mutations", () => { // go to tag drawer cy.contains("CypressTestAddTag2").click({ force: true }); - cy.wait(1000); + cy.wait(2000); // Click the Tag Details to launch full profile cy.contains("Tag Details").click({ force: true }); - cy.wait(1000); + cy.wait(2000); // title of tag page cy.contains("CypressTestAddTag2"); @@ -111,11 +130,16 @@ describe("mutations", () => { cy.contains("CypressTestAddTag2 Test Description"); // used by panel - click to search - cy.contains("1 Datasets").click(); + //Uncomment below line once schema Field Entity is fixed + // cy.contains("1 Datasets").click(); // verify dataset shows up in search now - cy.contains("of 1 result").click(); - cy.contains("cypress_logging_events").click(); + //Uncomment below line once schema Field Entity is fixed + // cy.contains("of 1 result").click(); + //Uncomment below line once schema Field Entity is fixed + // cy.contains("cypress_logging_events").click(); + //Remove below line once schema Field Entity is fixed + cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); cy.clickOptionWithText("event_name"); cy.get('[data-testid="schema-field-event_name-tags"]').within(() => cy @@ -127,7 +151,7 @@ describe("mutations", () => { cy.contains("CypressTestAddTag2").should("not.exist"); - cy.deleteUrn("urn:li:tag:CypressTestAddTag2"); + // cy.deleteUrn("urn:li:tag:CypressTestAddTag2"); }); it("can add and remove terms from a dataset field", () => { diff --git a/smoke-test/tests/cypress/cypress/support/commands.js b/smoke-test/tests/cypress/cypress/support/commands.js index 3b0df3ffdf6503..c670e1b5732450 100644 --- a/smoke-test/tests/cypress/cypress/support/commands.js +++ b/smoke-test/tests/cypress/cypress/support/commands.js @@ -110,6 +110,7 @@ Cypress.Commands.add("goToDataset", (urn, dataset_name) => { cy.visit( "/dataset/" + urn ); + cy.wait(5000); cy.waitTextVisible(dataset_name); }); @@ -117,6 +118,7 @@ Cypress.Commands.add("goToBusinessAttribute", (urn, attribute_name) => { cy.visit( "/business-attribute/" + urn ); + cy.wait(5000); cy.waitTextVisible(attribute_name); }); @@ -124,6 +126,7 @@ Cypress.Commands.add("goToTag", (urn, tag_name) => { cy.visit( "/tag/" + urn ); + cy.wait(5000); cy.waitTextVisible(tag_name); }); @@ -218,6 +221,14 @@ Cypress.Commands.add("addViaModal", (text, modelHeader, value, dataTestId) => { cy.contains(value).should('be.visible'); }); +Cypress.Commands.add("addBusinessAttributeViaModal", (text, modelHeader, value, dataTestId) => { + cy.waitTextVisible(modelHeader); + cy.get(".ant-input-affix-wrapper > input[type='text']").first().type(text); + cy.get('[data-testid="' + dataTestId + '"]').click(); + cy.wait(3000); + cy.contains(value).should('be.visible'); +}); + Cypress.Commands.add("ensureTextNotPresent", (text) => { cy.contains(text).should("not.exist"); }); From 982f7d548a5fad3c3141e80c6d9b471bb050e182 Mon Sep 17 00:00:00 2001 From: Kartikey Khandelwal Date: Wed, 3 Apr 2024 13:51:19 +0530 Subject: [PATCH 43/50] Business Attributes: Customized URNs Support for Business Attributes --- .../CreateBusinessAttributeModal.tsx | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx index a2078b87893339..61595045646c4b 100644 --- a/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx +++ b/datahub-web-react/src/app/businessAttribute/CreateBusinessAttributeModal.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { message, Button, Input, Modal, Typography, Form, Select } from 'antd'; +import { message, Button, Input, Modal, Typography, Form, Select, Collapse } from 'antd'; import styled from 'styled-components'; import { EditOutlined } from '@ant-design/icons'; import DOMPurify from 'dompurify'; @@ -10,6 +10,7 @@ import analytics, { EventType } from '../analytics'; import { useEntityRegistry } from '../useEntityRegistry'; import DescriptionModal from '../entity/shared/components/legacy/DescriptionModal'; import { SchemaFieldDataType } from './businessAttributeUtils'; +import { validateCustomUrnId } from '../shared/textUtil'; type Props = { visible: boolean; @@ -63,6 +64,8 @@ export default function CreateBusinessAttributeModal({ visible, onClose, onCreat const entityRegistry = useEntityRegistry(); + const [stagedId, setStagedId] = useState(undefined); + // Function to handle the close or cross button of Create Business Attribute Modal const onModalClose = () => { form.resetFields(); @@ -73,6 +76,7 @@ export default function CreateBusinessAttributeModal({ visible, onClose, onCreat const { name, dataType } = form.getFieldsValue(); const sanitizedDescription = DOMPurify.sanitize(documentation); const input: CreateBusinessAttributeInput = { + id: stagedId?.length ? stagedId : undefined, name, description: sanitizedDescription, type: dataType, @@ -208,6 +212,42 @@ export default function CreateBusinessAttributeModal({ visible, onClose, onCreat /> )} + + Advanced} key="1"> + + {entityRegistry.getEntityName(EntityType.BusinessAttribute)} Id + + } + > + + By default, a random UUID will be generated to uniquely identify this entity. If + you'd like to provide a custom id, you may provide it here. Note that it should be + unique across the entire Business Attributes. Be careful, you cannot easily change the id after + creation. + + ({ + validator(_, value) { + if (value && validateCustomUrnId(value)) { + return Promise.resolve(); + } + return Promise.reject(new Error('Please enter a valid entity id')); + }, + }), + ]} + > + setStagedId(event.target.value)} + /> + + + + From 8195acfe4ad86f6925cae75298a3e5d7f512ac8d Mon Sep 17 00:00:00 2001 From: Kartikey Khandelwal Date: Wed, 3 Apr 2024 17:47:22 +0530 Subject: [PATCH 44/50] Business Attributes: Fixed Schema Field Cypress Test Cases --- .../entity/schemaField/preview/Preview.tsx | 2 +- .../businessAttribute/businessAttribute.js | 12 ++++----- .../cypress/e2e/mutations/mutations.js | 26 +++++++------------ 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx b/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx index 2ac2be19ece89c..9fbf7b96473454 100644 --- a/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx +++ b/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx @@ -27,7 +27,7 @@ export const Preview = ({ const location = useLocation(); const relatedEntitiesUrl = getRelatedEntitiesUrl(entityRegistry, businessAttributeUrn); - const url = `${entityRegistry.getEntityUrl(EntityType.Dataset, datasetUrn)}/${encodeURIComponent('Schema')}?schemaFilter=${encodeURIComponent('customer_id')}`; + const url = `${entityRegistry.getEntityUrl(EntityType.Dataset, datasetUrn)}/${encodeURIComponent('Schema')}?schemaFilter=${encodeURIComponent(name)}`; return ( { cy.clickOptionWithText("Related Entities"); //cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); //cy.wait(5000); - //Uncomment below two lines once schema Field Entity is fixed - // cy.contains("of 0").should("not.exist"); - // cy.contains(/of [0-9]+/); + cy.contains("of 0").should("not.exist"); + cy.contains(/of [0-9]+/); }); @@ -91,10 +90,9 @@ describe("businessAttribute", () => { "logging{enter}" ); cy.wait(5000); - //Uncomment below three lines once schema Field Entity is fixed - // cy.contains("of 0").should("not.exist"); - // cy.contains(/of 1/); - // cy.contains("cypress_logging_events"); + cy.contains("of 0").should("not.exist"); + cy.contains(/of 1/); + cy.contains("cypress_logging_events"); }); it("remove business attribute from dataset", () => { diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js index 81d4fb159368c1..c674ee75f61dfb 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js @@ -53,20 +53,17 @@ describe("mutations", () => { // title of tag page cy.contains("CypressTestAddTag"); + cy.wait(2000); // description of tag page cy.contains("CypressTestAddTag Test Description"); // used by panel - click to search - //Uncomment below line once schema Field Entity is fixed - // cy.contains("1 Datasets").click({ force: true }); + cy.wait(3000); + cy.contains("1 Datasets").click({ force: true }); // verify dataset shows up in search now - //Uncomment below line once schema Field Entity is fixed - // cy.contains("of 1 result").click({ force: true }); - //Uncomment below line once schema Field Entity is fixed - // cy.contains("cypress_logging_events").click({ force: true }); - //Remove below line once schema Field Entity is fixed - cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); + cy.contains("of 1 result").click({ force: true }); + cy.contains("cypress_logging_events").click({ force: true }); cy.get('[data-testid="tag-CypressTestAddTag"]').within(() => cy.get("span[aria-label=close]").click() ); @@ -130,16 +127,12 @@ describe("mutations", () => { cy.contains("CypressTestAddTag2 Test Description"); // used by panel - click to search - //Uncomment below line once schema Field Entity is fixed - // cy.contains("1 Datasets").click(); + cy.wait(3000); + cy.contains("1 Datasets").click(); // verify dataset shows up in search now - //Uncomment below line once schema Field Entity is fixed - // cy.contains("of 1 result").click(); - //Uncomment below line once schema Field Entity is fixed - // cy.contains("cypress_logging_events").click(); - //Remove below line once schema Field Entity is fixed - cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); + cy.contains("of 1 result").click(); + cy.contains("cypress_logging_events").click(); cy.clickOptionWithText("event_name"); cy.get('[data-testid="schema-field-event_name-tags"]').within(() => cy @@ -186,6 +179,7 @@ describe("mutations", () => { cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); cy.clickOptionWithText("event_data"); + cy.wait(2000); cy.get('[data-testid="schema-field-event_data-businessAttribute"]').trigger( "mouseover", { force: true } From 799759e46d422e0ed8c502ea50c3069a81f2903b Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Thu, 4 Apr 2024 13:18:05 +0530 Subject: [PATCH 45/50] business-attributes: introduce fieldNamealias for schemafieldentity --- .../businessattribute/BusinessAttributeInfo.pdl | 1 + .../com/linkedin/schemafield/schemafieldInfo.pdl | 13 +++++++++++++ .../src/main/resources/entity-registry.yml | 1 + 3 files changed, 15 insertions(+) create mode 100644 metadata-models/src/main/pegasus/com/linkedin/schemafield/schemafieldInfo.pdl diff --git a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl index 6236c9e77f455e..388164bc8ca6e4 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/businessattribute/BusinessAttributeInfo.pdl @@ -19,6 +19,7 @@ record BusinessAttributeInfo includes EditableSchemaFieldInfo, CustomProperties, "fieldType": "WORD_GRAM", "enableAutocomplete": true, "boostScore": 10.0, + "fieldNameAliases": [ "_entityName" ] } name: string type: optional SchemaFieldDataType diff --git a/metadata-models/src/main/pegasus/com/linkedin/schemafield/schemafieldInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/schemafield/schemafieldInfo.pdl new file mode 100644 index 00000000000000..086d9df34deadd --- /dev/null +++ b/metadata-models/src/main/pegasus/com/linkedin/schemafield/schemafieldInfo.pdl @@ -0,0 +1,13 @@ +namespace com.linkedin.schemafield + +@Aspect = { + "name": "schemafieldInfo" +} + +record SchemaFieldInfo { + @Searchable = { + "fieldType": "KEYWORD", + "fieldNameAliases": [ "_entityName" ] + } + name: optional string +} \ No newline at end of file diff --git a/metadata-models/src/main/resources/entity-registry.yml b/metadata-models/src/main/resources/entity-registry.yml index 04a4dd835715ad..d7ab1f948b411a 100644 --- a/metadata-models/src/main/resources/entity-registry.yml +++ b/metadata-models/src/main/resources/entity-registry.yml @@ -448,6 +448,7 @@ entities: category: core keyAspect: schemaFieldKey aspects: + - schemafieldInfo - structuredProperties - forms - businessAttributes From dafa1c7fdba7dd90c90b08b8be83ec4056770275 Mon Sep 17 00:00:00 2001 From: "Shukla, Amit" Date: Thu, 4 Apr 2024 14:39:45 +0530 Subject: [PATCH 46/50] feat(search/schema_field): Update schema field card in search results --- .../schemaField/SchemaFieldPropertiesEntity.tsx | 1 - .../src/app/entity/schemaField/preview/Preview.tsx | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx b/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx index 4be0fa81a23f98..91638d4997003e 100644 --- a/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx +++ b/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx @@ -37,7 +37,6 @@ export class SchemaFieldPropertiesEntity implements Entity { ); diff --git a/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx b/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx index 9fbf7b96473454..3f24b3a06e3a42 100644 --- a/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx +++ b/datahub-web-react/src/app/entity/schemaField/preview/Preview.tsx @@ -1,31 +1,24 @@ import React from 'react'; import { PicCenterOutlined } from '@ant-design/icons'; -import { useLocation } from 'react-router-dom'; import { EntityType, Owner } from '../../../../types.generated'; import DefaultPreviewCard from '../../../preview/DefaultPreviewCard'; import { useEntityRegistry } from '../../../useEntityRegistry'; import { IconStyleType, PreviewType } from '../../Entity'; -import UrlButton from '../../shared/UrlButton'; -import { getRelatedEntitiesUrl } from '../../../businessAttribute/businessAttributeUtils'; export const Preview = ({ datasetUrn, - businessAttributeUrn, name, description, owners, previewType, }: { datasetUrn: string; - businessAttributeUrn: string; name: string; description?: string | null; owners?: Array | null; previewType: PreviewType; }): JSX.Element => { const entityRegistry = useEntityRegistry(); - const location = useLocation(); - const relatedEntitiesUrl = getRelatedEntitiesUrl(entityRegistry, businessAttributeUrn); const url = `${entityRegistry.getEntityUrl(EntityType.Dataset, datasetUrn)}/${encodeURIComponent('Schema')}?schemaFilter=${encodeURIComponent(name)}`; @@ -40,11 +33,6 @@ export const Preview = ({ logoComponent={} type="Column" typeIcon={entityRegistry.getIcon(EntityType.SchemaField, 14, IconStyleType.ACCENT)} - entityTitleSuffix={ - decodeURIComponent(location.pathname) !== decodeURIComponent(relatedEntitiesUrl) && ( - View Related Entities - ) - } /> ); }; \ No newline at end of file From ec2d7eae467690e51a1d62ee0837877417efd93b Mon Sep 17 00:00:00 2001 From: Kartikey Khandelwal Date: Wed, 3 Apr 2024 17:54:29 +0530 Subject: [PATCH 47/50] Business Attributes: Feature Flag Cypress Test Cases Fix --- .../businessAttribute/attribute_mutations.js | 147 ++++++----- .../businessAttribute/businessAttribute.js | 228 +++++++++++------- .../tests/cypress/cypress/e2e/home/home.js | 30 ++- .../cypress/e2e/mutations/mutations.js | 87 ++++--- 4 files changed, 308 insertions(+), 184 deletions(-) diff --git a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js index 5bbb19e85d9bc1..decee024f050b0 100644 --- a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/attribute_mutations.js @@ -1,98 +1,119 @@ import { aliasQuery, hasOperationName } from "../utils"; describe("attribute list adding tags and terms", () => { + let businessAttributeEntityEnabled; + beforeEach(() => { cy.intercept("POST", "/api/v2/graphql", (req) => { aliasQuery(req, "appConfig"); }); }); - const setBusinessAttributeFeatureFlag = (isOn) => { - cy.intercept("POST", "/api/v2/graphql", (req) => { - if (hasOperationName(req, "appConfig")) { - req.reply((res) => { - res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled = isOn; - }); - } - }); + const setBusinessAttributeFeatureFlag = () => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + if (hasOperationName(req, "appConfig")) { + req.reply((res) => { + businessAttributeEntityEnabled = res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled; + return res; + }); + } + }).as('apiCall'); }; + + it("can create and add a tag to business attribute and visit new tag page", () => { - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); - cy.goToBusinessAttributeList(); + cy.visit("/business-attribute"); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.wait(3000); + cy.waitTextVisible("Business Attribute"); + cy.wait(3000); - cy.mouseover('[data-testid="schema-field-cypressTestAttribute-tags"]'); - cy.get('[data-testid="schema-field-cypressTestAttribute-tags"]').within(() => - cy.contains("Add Tags").click() - ); + cy.mouseover('[data-testid="schema-field-cypressTestAttribute-tags"]'); + cy.get('[data-testid="schema-field-cypressTestAttribute-tags"]').within(() => + cy.contains("Add Tags").click() + ); - cy.enterTextInTestId("tag-term-modal-input", "CypressAddTagToAttribute"); + cy.enterTextInTestId("tag-term-modal-input", "CypressAddTagToAttribute"); - cy.contains("Create CypressAddTagToAttribute").click({ force: true }); + cy.contains("Create CypressAddTagToAttribute").click({ force: true }); - cy.get("textarea").type("CypressAddTagToAttribute Test Description"); + cy.get("textarea").type("CypressAddTagToAttribute Test Description"); - cy.contains(/Create$/).click({ force: true }); + cy.contains(/Create$/).click({ force: true }); - // wait a breath for elasticsearch to index the tag being applied to the business attribute- if we navigate too quick ES - // wont know and we'll see applied to 0 entities - cy.wait(3000); + // wait a breath for elasticsearch to index the tag being applied to the business attribute- if we navigate too quick ES + // wont know and we'll see applied to 0 entities + cy.wait(3000); - // go to tag drawer - cy.contains("CypressAddTagToAttribute").click({ force: true }); + // go to tag drawer + cy.contains("CypressAddTagToAttribute").click({ force: true }); - cy.wait(3000); + cy.wait(3000); - // Click the Tag Details to launch full profile - cy.contains("Tag Details").click({ force: true }); + // Click the Tag Details to launch full profile + cy.contains("Tag Details").click({ force: true }); - cy.wait(3000); + cy.wait(3000); - // title of tag page - cy.contains("CypressAddTagToAttribute"); + // title of tag page + cy.contains("CypressAddTagToAttribute"); - // description of tag page - cy.contains("CypressAddTagToAttribute Test Description"); + // description of tag page + cy.contains("CypressAddTagToAttribute Test Description"); - cy.wait(3000); - // used by panel - click to search - cy.contains("1 Business Attributes").click({ force: true }); + cy.wait(3000); + // used by panel - click to search + cy.contains("1 Business Attributes").click({ force: true }); - // verify business attribute shows up in search now - cy.contains("of 1 result").click({ force: true }); - cy.contains("cypressTestAttribute").click({ force: true }); - cy.get('[data-testid="tag-CypressAddTagToAttribute"]').within(() => - cy.get("span[aria-label=close]").click() - ); - cy.contains("Yes").click(); + // verify business attribute shows up in search now + cy.contains("of 1 result").click({ force: true }); + cy.contains("cypressTestAttribute").click({ force: true }); + cy.get('[data-testid="tag-CypressAddTagToAttribute"]').within(() => + cy.get("span[aria-label=close]").click() + ); + cy.contains("Yes").click(); - cy.contains("CypressAddTagToAttribute").should("not.exist"); + cy.contains("CypressAddTagToAttribute").should("not.exist"); - cy.goToTag("urn:li:tag:CypressAddTagToAttribute", "CypressAddTagToAttribute"); - cy.deleteFromDropdown(); + cy.goToTag("urn:li:tag:CypressAddTagToAttribute", "CypressAddTagToAttribute"); + cy.deleteFromDropdown(); + }); }); + it("can add and remove terms from a business attribute", () => { - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); - cy.addTermToBusinessAttribute( - "urn:li:businessAttribute:cypressTestAttribute", - "cypressTestAttribute", - "CypressTerm" - ) - - cy.goToBusinessAttributeList(); - cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').contains("CypressTerm"); - - cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').within(() => - cy - .get("span[aria-label=close]") - .trigger("mouseover", { force: true }) - .click({ force: true }) - ); - cy.contains("Yes").click({ force: true }); - - cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').contains("CypressTerm").should("not.exist"); + cy.visit("/business-attribute/" + "urn:li:businessAttribute:cypressTestAttribute"); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.wait(3000); + cy.waitTextVisible("cypressTestAttribute"); + cy.wait(3000); + cy.clickOptionWithText("Add Terms"); + cy.selectOptionInTagTermModal("CypressTerm"); + cy.contains("CypressTerm"); + + cy.goToBusinessAttributeList(); + cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').contains("CypressTerm"); + + cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + cy.contains("Yes").click({ force: true }); + + cy.get('[data-testid="schema-field-cypressTestAttribute-terms"]').contains("CypressTerm").should("not.exist"); + }); }); }); diff --git a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js index 7974a3ef7717b3..0657dc238a1541 100644 --- a/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js +++ b/smoke-test/tests/cypress/cypress/e2e/businessAttribute/businessAttribute.js @@ -1,56 +1,67 @@ import { aliasQuery, hasOperationName } from "../utils"; describe("businessAttribute", () => { - beforeEach(() => { - cy.intercept("POST", "/api/v2/graphql", (req) => { - aliasQuery(req, "appConfig"); - }); - }); + let businessAttributeEntityEnabled; - const setBusinessAttributeFeatureFlag = (isOn) => { - cy.intercept("POST", "/api/v2/graphql", (req) => { - if (hasOperationName(req, "appConfig")) { - req.reply((res) => { - res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled = isOn; + beforeEach(() => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + aliasQuery(req, "appConfig"); }); - } - }); - }; + }); + + const setBusinessAttributeFeatureFlag = () => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + if (hasOperationName(req, "appConfig")) { + req.reply((res) => { + businessAttributeEntityEnabled = res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled; + return res; + }); + } + }).as('apiCall'); + }; it('go to business attribute page, create attribute ', function () { const urn="urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; const businessAttribute="CypressBusinessAttribute"; const datasetName = "cypress_logging_events"; - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); - cy.goToBusinessAttributeList(); - cy.clickOptionWithText("Create Business Attribute"); - cy.addBusinessAttributeViaModal(businessAttribute, "Create Business Attribute", businessAttribute, "create-business-attribute-button"); + cy.visit("/business-attribute"); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.wait(3000); + cy.waitTextVisible("Business Attribute"); + cy.wait(3000); + cy.clickOptionWithText("Create Business Attribute"); + cy.addBusinessAttributeViaModal(businessAttribute, "Create Business Attribute", businessAttribute, "create-business-attribute-button"); - cy.wait(3000); - cy.goToBusinessAttributeList() + cy.wait(3000); + cy.goToBusinessAttributeList() - cy.wait(3000) - cy.contains(businessAttribute).should("be.visible"); + cy.wait(3000) + cy.contains(businessAttribute).should("be.visible"); - cy.addAttributeToDataset(urn, datasetName, businessAttribute); + cy.addAttributeToDataset(urn, datasetName, businessAttribute); - cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => - cy - .get("span[aria-label=close]") - .trigger("mouseover", { force: true }) - .click({ force: true }) - ); - cy.contains("Yes").click({ force: true }); + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + cy.contains("Yes").click({ force: true }); - cy.get('[data-testid="schema-field-event_name-businessAttribute"]').contains("CypressBusinessAttribute").should("not.exist"); + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').contains("CypressBusinessAttribute").should("not.exist"); - cy.goToBusinessAttributeList(); - cy.clickOptionWithText(businessAttribute); - cy.deleteFromDropdown(); + cy.goToBusinessAttributeList(); + cy.clickOptionWithText(businessAttribute); + cy.deleteFromDropdown(); - cy.goToBusinessAttributeList(); - cy.ensureTextNotPresent(businessAttribute); + cy.goToBusinessAttributeList(); + cy.ensureTextNotPresent(businessAttribute); + }); }); it('Inheriting tags and terms from business attribute to dataset ', function () { @@ -59,89 +70,128 @@ describe("businessAttribute", () => { const datasetName = "cypress_logging_events"; const term="CypressTerm"; const tag="Cypress"; - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); - - cy.addAttributeToDataset(urn, datasetName, businessAttribute); - cy.contains(term); - cy.contains(tag); - + cy.visit("/dataset/" + urn); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.wait(5000); + cy.waitTextVisible(datasetName); + cy.clickOptionWithText("event_name"); + cy.contains("Business Attribute"); + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => + cy.contains("Add Attribute").click() + ); + cy.selectOptionInAttributeModal(businessAttribute); + cy.contains(businessAttribute); + cy.contains(term); + cy.contains(tag); + }); }); it("can visit related entities", () => { const businessAttribute="CypressAttribute"; - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); - cy.goToBusinessAttributeList(); - cy.clickOptionWithText(businessAttribute); - cy.clickOptionWithText("Related Entities"); - //cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); - //cy.wait(5000); - cy.contains("of 0").should("not.exist"); - cy.contains(/of [0-9]+/); + cy.visit("/business-attribute"); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.wait(3000); + cy.waitTextVisible("Business Attribute"); + cy.wait(3000); + cy.clickOptionWithText(businessAttribute); + cy.clickOptionWithText("Related Entities"); + //cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); + //cy.wait(5000); + cy.contains("of 0").should("not.exist"); + cy.contains(/of [0-9]+/); + }); }); it("can search related entities by query", () => { - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); cy.visit("/business-attribute/urn:li:businessAttribute:37c81832-06e0-40b1-a682-858e1dd0d449/Related%20Entities"); - cy.get('[placeholder="Filter entities..."]').click().type( - "logging{enter}" - ); - cy.wait(5000); - cy.contains("of 0").should("not.exist"); - cy.contains(/of 1/); - cy.contains("cypress_logging_events"); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.get('[placeholder="Filter entities..."]').click().type( + "event_n{enter}" + ); + cy.wait(5000); + cy.contains("of 0").should("not.exist"); + cy.contains(/of 1/); + cy.contains("event_name"); + }); }); it("remove business attribute from dataset", () => { const urn="urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"; const datasetName = "cypress_logging_events"; - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); - cy.goToDataset(urn, datasetName); - - cy.wait(3000); - cy.get('body').then(($body) => { - if ($body.find('button[aria-label="Close"]').length > 0) { - cy.get('button[aria-label="Close"]').click(); + cy.visit("/dataset/" + urn); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; } + cy.wait(5000); + cy.waitTextVisible(datasetName); + + cy.wait(3000); + cy.get('body').then(($body) => { + if ($body.find('button[aria-label="Close"]').length > 0) { + cy.get('button[aria-label="Close"]').click(); + } + }); + cy.clickOptionWithText("event_name"); + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + cy.contains("Yes").click({ force: true }); + + cy.get('[data-testid="schema-field-event_name-businessAttribute"]').contains("CypressAttribute").should("not.exist"); }); - cy.clickOptionWithText("event_name"); - cy.get('[data-testid="schema-field-event_name-businessAttribute"]').within(() => - cy - .get("span[aria-label=close]") - .trigger("mouseover", { force: true }) - .click({ force: true }) - ); - cy.contains("Yes").click({ force: true }); - - cy.get('[data-testid="schema-field-event_name-businessAttribute"]').contains("CypressAttribute").should("not.exist"); }); it("update the data type of a business attribute", () => { const businessAttribute="cypressTestAttribute"; - setBusinessAttributeFeatureFlag(true); + setBusinessAttributeFeatureFlag(); cy.login(); - cy.goToBusinessAttributeList(); - - cy.clickOptionWithText(businessAttribute); + cy.visit("/business-attribute"); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.wait(3000); + cy.waitTextVisible("Business Attribute"); + cy.wait(3000); - cy.get('[data-testid="edit-data-type-button"]').within(() => - cy - .get("span[aria-label=edit]") - .trigger("mouseover", { force: true }) - .click({ force: true }) - ); + cy.clickOptionWithText(businessAttribute); - cy.get('[data-testid="add-data-type-option"]').get('.ant-select-selection-search-input').click({multiple: true}); + cy.get('[data-testid="edit-data-type-button"]').within(() => + cy + .get("span[aria-label=edit]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); - cy.get('.ant-select-item-option-content') - .contains('STRING') - .click(); + cy.get('[data-testid="add-data-type-option"]').get('.ant-select-selection-search-input').click({multiple: true}); - cy.contains("STRING"); + cy.get('.ant-select-item-option-content') + .contains('STRING') + .click(); + cy.contains("STRING"); + }); }); }); diff --git a/smoke-test/tests/cypress/cypress/e2e/home/home.js b/smoke-test/tests/cypress/cypress/e2e/home/home.js index 0039114ff9c14c..05140486e189b6 100644 --- a/smoke-test/tests/cypress/cypress/e2e/home/home.js +++ b/smoke-test/tests/cypress/cypress/e2e/home/home.js @@ -1,13 +1,39 @@ +import { aliasQuery, hasOperationName } from "../utils"; + describe('home', () => { + let businessAttributeEntityEnabled; + + beforeEach(() => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + aliasQuery(req, "appConfig"); + }); + }); + + const setBusinessAttributeFeatureFlag = () => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + if (hasOperationName(req, "appConfig")) { + req.reply((res) => { + businessAttributeEntityEnabled = res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled; + return res; + }); + } + }).as('apiCall'); + }; it('home page shows ', () => { + setBusinessAttributeFeatureFlag(); cy.login(); cy.visit('/'); - cy.get('img[src="/assets/platforms/datahublogo.png"]').should('exist'); + // cy.get('img[src="/assets/platforms/datahublogo.png"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-DATASET"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-DASHBOARD"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-CHART"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-DATA_FLOW"]').should('exist'); cy.get('[data-testid="entity-type-browse-card-GLOSSARY_TERM"]').should('exist'); - cy.get('[data-testid="entity-type-browse-card-BUSINESS_ATTRIBUTE"]').should('exist'); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.get('[data-testid="entity-type-browse-card-BUSINESS_ATTRIBUTE"]').should('exist'); + }); }); }) diff --git a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js index c674ee75f61dfb..e2a74a15d3dfcf 100644 --- a/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js +++ b/smoke-test/tests/cypress/cypress/e2e/mutations/mutations.js @@ -1,4 +1,25 @@ +import { aliasQuery, hasOperationName } from "../utils"; + describe("mutations", () => { + let businessAttributeEntityEnabled; + + beforeEach(() => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + aliasQuery(req, "appConfig"); + }); + }); + + const setBusinessAttributeFeatureFlag = () => { + cy.intercept("POST", "/api/v2/graphql", (req) => { + if (hasOperationName(req, "appConfig")) { + req.reply((res) => { + businessAttributeEntityEnabled = res.body.data.appConfig.featureFlags.businessAttributeEntityEnabled; + return res; + }); + } + }).as('apiCall'); + }; + before(() => { // warm up elastic by issuing a `*` search cy.login(); @@ -173,34 +194,40 @@ describe("mutations", () => { }); it("can add and remove business attribute from a dataset field", () => { - cy.login(); - // make space for the glossary term column - cy.viewport(2000, 800); - - cy.goToDataset("urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)", "cypress_logging_events"); - cy.clickOptionWithText("event_data"); - cy.wait(2000); - cy.get('[data-testid="schema-field-event_data-businessAttribute"]').trigger( - "mouseover", - { force: true } - ); - cy.get('[data-testid="schema-field-event_data-businessAttribute"]').within(() => - cy.contains("Add Attribute").click({ force: true }) - ); - - cy.selectOptionInAttributeModal("cypressTestAttribute"); - - cy.contains("cypressTestAttribute"); - - cy.get('[data-testid="schema-field-event_data-businessAttribute"]'). - within(() => - cy - .get("span[aria-label=close]") - .trigger("mouseover", { force: true }) - .click({ force: true }) - ); - cy.contains("Yes").click({ force: true }); - - cy.contains("cypressTestAttribute").should("not.exist"); - }); + setBusinessAttributeFeatureFlag(); + cy.login(); + // make space for the glossary term column + cy.viewport(2000, 800); + cy.visit("/dataset/" + "urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)"); + cy.wait('@apiCall').then(() => { + if (!businessAttributeEntityEnabled) { + return; + } + cy.wait(5000); + cy.waitTextVisible("cypress_logging_events"); + cy.clickOptionWithText("event_data"); + cy.get('[data-testid="schema-field-event_data-businessAttribute"]').trigger( + "mouseover", + { force: true } + ); + cy.get('[data-testid="schema-field-event_data-businessAttribute"]').within(() => + cy.contains("Add Attribute").click({ force: true }) + ); + + cy.selectOptionInAttributeModal("cypressTestAttribute"); + cy.wait(2000); + cy.contains("cypressTestAttribute"); + + cy.get('[data-testid="schema-field-event_data-businessAttribute"]'). + within(() => + cy + .get("span[aria-label=close]") + .trigger("mouseover", { force: true }) + .click({ force: true }) + ); + cy.contains("Yes").click({ force: true }); + + cy.contains("cypressTestAttribute").should("not.exist"); + }); + }); }); From cd1520872e871ba3033722d856bb06f9a27d3ac4 Mon Sep 17 00:00:00 2001 From: "Bharti, Aakash" Date: Thu, 4 Apr 2024 12:43:42 +0530 Subject: [PATCH 48/50] business-attribute-flag-for-openapi --- buildSrc/build.gradle | 1 - .../io/datahubproject/OpenApiEntities.java | 8 -- .../v2/delegates/EntityApiDelegateImpl.java | 87 ++++++++++++++++++- 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index cf49b65b8c1aaa..1f0d1b409fe0b2 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -22,7 +22,6 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.5' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.5' implementation 'commons-io:commons-io:2.11.0' - implementation 'org.springframework:spring-beans:5.3.32' compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' diff --git a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java index 4d988035dedd74..01d61b6119b0a2 100644 --- a/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java +++ b/buildSrc/src/main/java/io/datahubproject/OpenApiEntities.java @@ -11,7 +11,6 @@ import com.linkedin.metadata.models.registry.config.Entities; import com.linkedin.metadata.models.registry.config.Entity; import org.gradle.internal.Pair; -import org.springframework.beans.factory.annotation.Value; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -45,9 +44,6 @@ public class OpenApiEntities { private String entityRegistryYaml; private Path combinedDirectory; - @Value("${featureFlags.businessAttributeEntityEnabled:false}") - private boolean businessAttributeEntityEnabled; - private final static ImmutableSet SUPPORTED_ASPECT_PATHS = ImmutableSet.builder() .add("domains") .add("ownership") @@ -121,10 +117,6 @@ public ObjectNode entityExtension(List nodesList, ObjectNode schemas Pair> parameters = buildParameters(schemasNode, modelDefinitions); ObjectNode componentsNode = writeComponentsYaml(schemasNode, parameters.left()); - if (!businessAttributeEntityEnabled) { - modelDefinitions.remove("BusinessAttribute"); - } - // Just the entity paths writePathsYaml(modelDefinitions, parameters.right()); diff --git a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java index 18bd4b3f10a65f..3dfe40d6b9b4be 100644 --- a/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java +++ b/metadata-service/openapi-entity-servlet/src/main/java/io/datahubproject/openapi/v2/delegates/EntityApiDelegateImpl.java @@ -68,10 +68,12 @@ import javax.annotation.Nullable; import javax.validation.Valid; import javax.validation.constraints.Min; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +@Slf4j public class EntityApiDelegateImpl { private final OperationContext systemOperationContext; private final EntityRegistry _entityRegistry; @@ -83,6 +85,8 @@ public class EntityApiDelegateImpl { private final Class _respClazz; private final Class _scrollRespClazz; + private static final String BUSINESS_ATTRIBUTE_ERROR_MESSAGE = + "business attribute is disabled, enable it using featureflag : BUSINESS_ATTRIBUTE_ENTITY_ENABLED"; private final StackWalker walker = StackWalker.getInstance(); public EntityApiDelegateImpl( @@ -106,6 +110,9 @@ public EntityApiDelegateImpl( } public ResponseEntity get(String urn, Boolean systemMetadata, List aspects) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String[] requestedAspects = Optional.ofNullable(aspects) .map(asp -> asp.stream().distinct().toArray(String[]::new)) @@ -130,6 +137,14 @@ public ResponseEntity> create( OpenApiEntitiesUtil.convertEntityToUpsert(b, _reqClazz, _entityRegistry) .stream()) .collect(Collectors.toList()); + + Optional aspect = aspects.stream().findFirst(); + if (aspect.isPresent()) { + String entityType = aspect.get().getEntityType(); + if (checkBusinessAttributeFlagFromEntityType(entityType)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } + } _v1Controller.postEntities(aspects, false, createIfNotExists, createEntityIfNotExists); List responses = body.stream() @@ -139,14 +154,19 @@ public ResponseEntity> create( } public ResponseEntity delete(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } _v1Controller.deleteEntities(new String[] {urn}, false, false); return new ResponseEntity<>(HttpStatus.OK); } public ResponseEntity head(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } try { Urn entityUrn = Urn.createFromString(urn); - final Authentication auth = AuthenticationContext.getAuthentication(); if (!AuthUtil.isAPIAuthorizedEntityUrns( auth, _authorizationChain, EXISTS, List.of(entityUrn))) { @@ -280,6 +300,9 @@ public ResponseEntity createOwnership( String urn, @Nullable Boolean createIfNotExists, @Nullable Boolean createEntityIfNotExists) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return createAspect( @@ -297,6 +320,9 @@ public ResponseEntity createStatus( String urn, @Nullable Boolean createIfNotExists, @Nullable Boolean createEntityIfNotExists) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return createAspect( @@ -328,12 +354,18 @@ public ResponseEntity deleteGlossaryTerms(String urn) { } public ResponseEntity deleteOwnership(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return deleteAspect(urn, methodNameToAspectName(methodName)); } public ResponseEntity deleteStatus(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return deleteAspect(urn, methodNameToAspectName(methodName)); @@ -376,6 +408,9 @@ public ResponseEntity getGlossaryTerms( public ResponseEntity getOwnership( String urn, Boolean systemMetadata) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return getAspect( @@ -387,6 +422,9 @@ public ResponseEntity getOwnership( } public ResponseEntity getStatus(String urn, Boolean systemMetadata) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return getAspect( @@ -416,12 +454,18 @@ public ResponseEntity headGlossaryTerms(String urn) { } public ResponseEntity headOwnership(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return headAspect(urn, methodNameToAspectName(methodName)); } public ResponseEntity headStatus(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return headAspect(urn, methodNameToAspectName(methodName)); @@ -626,6 +670,9 @@ public ResponseEntity createInstitutionalMe String urn, @Nullable Boolean createIfNotExists, @Nullable Boolean createEntityIfNotExists) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return createAspect( @@ -702,6 +749,9 @@ public ResponseEntity deleteEditableDatasetProperties(String urn) { } public ResponseEntity deleteInstitutionalMemory(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return deleteAspect(urn, methodNameToAspectName(methodName)); @@ -739,6 +789,9 @@ public ResponseEntity getEditableData public ResponseEntity getInstitutionalMemory( String urn, Boolean systemMetadata) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return getAspect( @@ -798,6 +851,9 @@ public ResponseEntity headEditableDatasetProperties(String urn) { } public ResponseEntity headInstitutionalMemory(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return headAspect(urn, methodNameToAspectName(methodName)); @@ -961,6 +1017,9 @@ public ResponseEntity createBusinessAttri String urn, @Nullable Boolean createIfNotExists, @Nullable Boolean createEntityIfNotExists) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return createAspect( @@ -974,6 +1033,9 @@ public ResponseEntity createBusinessAttri } public ResponseEntity deleteBusinessAttributeInfo(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return deleteAspect(urn, methodNameToAspectName(methodName)); @@ -981,6 +1043,9 @@ public ResponseEntity deleteBusinessAttributeInfo(String urn) { public ResponseEntity getBusinessAttributeInfo( String urn, Boolean systemMetadata) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return getAspect( @@ -992,8 +1057,28 @@ public ResponseEntity getBusinessAttribut } public ResponseEntity headBusinessAttributeInfo(String urn) { + if (checkBusinessAttributeFlagFromUrn(urn)) { + throw new UnsupportedOperationException(BUSINESS_ATTRIBUTE_ERROR_MESSAGE); + } String methodName = walker.walk(frames -> frames.findFirst().map(StackWalker.StackFrame::getMethodName)).get(); return headAspect(urn, methodNameToAspectName(methodName)); } + + private boolean checkBusinessAttributeFlagFromUrn(String urn) { + try { + return checkBusinessAttributeFlagFromEntityType(Urn.createFromString(urn).getEntityType()); + } catch (URISyntaxException e) { + return true; + } + } + + private boolean checkBusinessAttributeFlagFromEntityType(String entityType) { + return entityType.equals("businessAttribute") && !businessAttributeEntityEnabled(); + } + + private boolean businessAttributeEntityEnabled() { + return System.getenv("BUSINESS_ATTRIBUTE_ENTITY_ENABLED") != null + && Boolean.parseBoolean(System.getenv("BUSINESS_ATTRIBUTE_ENTITY_ENABLED")); + } } From 3267fdfff386b423e07d74ff4dee51f43b81d139 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Sun, 14 Apr 2024 18:26:49 +0530 Subject: [PATCH 49/50] business-attributes: changes due to merge resolve conflicts --- .../datahub/graphql/GmsGraphQLEngine.java | 410 +++++++++--------- .../models/OpenApiSpecBuilderTest.java | 6 +- .../java/com/linkedin/metadata/Constants.java | 3 +- 3 files changed, 207 insertions(+), 212 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 6717ff383395b7..3296853145a470 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -1039,10 +1039,10 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) { .dataFetcher( "browseV2", new BrowseV2Resolver(this.entityClient, this.viewService, this.formService)) - .dataFetcher("businessAttribute", getResolver(businessAttributeType)) - .dataFetcher( - "listBusinessAttributes", - new ListBusinessAttributesResolver(this.entityClient))); + .dataFetcher("businessAttribute", getResolver(businessAttributeType)) + .dataFetcher( + "listBusinessAttributes", + new ListBusinessAttributesResolver(this.entityClient))); } private DataFetcher getEntitiesResolver() { @@ -1095,216 +1095,210 @@ private String getUrnField(DataFetchingEnvironment env) { private void configureMutationResolvers(final RuntimeWiring.Builder builder) { builder.type( "Mutation", - typeWiring -> + typeWiring -> { + typeWiring + .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) + .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) + .dataFetcher( + "createTag", new CreateTagResolver(this.entityClient, this.entityService)) + .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) + .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) + .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) + .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) + .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) + .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) + .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) + .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) + .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) + .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) + .dataFetcher( + "updateERModelRelationship", + new UpdateERModelRelationshipResolver(this.entityClient)) + .dataFetcher( + "createERModelRelationship", + new CreateERModelRelationshipResolver( + this.entityClient, this.erModelRelationshipService)) + .dataFetcher("addTag", new AddTagResolver(entityService)) + .dataFetcher("addTags", new AddTagsResolver(entityService)) + .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) + .dataFetcher("removeTag", new RemoveTagResolver(entityService)) + .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) + .dataFetcher("addTerm", new AddTermResolver(entityService)) + .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) + .dataFetcher("addTerms", new AddTermsResolver(entityService)) + .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) + .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) + .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) + .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) + .dataFetcher( + "updateDescription", + new UpdateDescriptionResolver(entityService, this.entityClient)) + .dataFetcher("addOwner", new AddOwnerResolver(entityService)) + .dataFetcher("addOwners", new AddOwnersResolver(entityService)) + .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) + .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) + .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) + .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) + .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) + .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) + .dataFetcher("removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) + .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) + .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) + .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) + .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) + .dataFetcher( + "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) + .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) + .dataFetcher( + "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) + .dataFetcher( + "updateDeprecation", + new UpdateDeprecationResolver(this.entityClient, this.entityService)) + .dataFetcher( + "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) + .dataFetcher( + "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) + .dataFetcher( + "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) + .dataFetcher( + "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) + .dataFetcher( + "revokeAccessToken", + new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) + .dataFetcher( + "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) + .dataFetcher( + "createIngestionExecutionRequest", + new CreateIngestionExecutionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "cancelIngestionExecutionRequest", + new CancelIngestionExecutionRequestResolver(this.entityClient)) + .dataFetcher( + "createTestConnectionRequest", + new CreateTestConnectionRequestResolver( + this.entityClient, this.ingestionConfiguration)) + .dataFetcher( + "deleteAssertion", + new DeleteAssertionResolver(this.entityClient, this.entityService)) + .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) + .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) + .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) + .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) + .dataFetcher( + "createGlossaryTerm", + new CreateGlossaryTermResolver(this.entityClient, this.entityService)) + .dataFetcher( + "createGlossaryNode", + new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateParentNode", + new UpdateParentNodeResolver(this.entityService, this.entityClient)) + .dataFetcher( + "deleteGlossaryEntity", + new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) + .dataFetcher( + "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) + .dataFetcher( + "addRelatedTerms", + new AddRelatedTermsResolver(this.entityService, this.entityClient)) + .dataFetcher( + "removeRelatedTerms", + new RemoveRelatedTermsResolver(this.entityService, this.entityClient)) + .dataFetcher( + "createNativeUserResetToken", + new CreateNativeUserResetTokenResolver(this.nativeUserService)) + .dataFetcher( + "batchUpdateSoftDeleted", new BatchUpdateSoftDeletedResolver(this.entityService)) + .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) + .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) + .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) + .dataFetcher( + "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) + .dataFetcher( + "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) + .dataFetcher("createPost", new CreatePostResolver(this.postService)) + .dataFetcher("deletePost", new DeletePostResolver(this.postService)) + .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) + .dataFetcher( + "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) + .dataFetcher("createView", new CreateViewResolver(this.viewService)) + .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) + .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) + .dataFetcher( + "updateGlobalViewsSettings", + new UpdateGlobalViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateCorpUserViewsSettings", + new UpdateCorpUserViewsSettingsResolver(this.settingsService)) + .dataFetcher( + "updateLineage", + new UpdateLineageResolver(this.entityService, this.lineageService)) + .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) + .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) + .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) + .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) + .dataFetcher( + "createDataProduct", new CreateDataProductResolver(this.dataProductService)) + .dataFetcher( + "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) + .dataFetcher( + "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) + .dataFetcher( + "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) + .dataFetcher( + "createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "updateOwnershipType", new UpdateOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher( + "deleteOwnershipType", new DeleteOwnershipTypeResolver(this.ownershipTypeService)) + .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) + .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) + .dataFetcher( + "createDynamicFormAssignment", + new CreateDynamicFormAssignmentResolver(this.formService)) + .dataFetcher( + "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) + .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) + .dataFetcher( + "upsertStructuredProperties", + new UpsertStructuredPropertiesResolver(this.entityClient)) + .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) + .dataFetcher( + "updateIncidentStatus", + new UpdateIncidentStatusResolver(this.entityClient, this.entityService)); + if (featureFlags.isBusinessAttributeEntityEnabled()) { typeWiring - .dataFetcher("updateDataset", new MutableTypeResolver<>(datasetType)) - .dataFetcher("updateDatasets", new MutableTypeBatchResolver<>(datasetType)) - .dataFetcher( - "createTag", new CreateTagResolver(this.entityClient, this.entityService)) - .dataFetcher("updateTag", new MutableTypeResolver<>(tagType)) - .dataFetcher("setTagColor", new SetTagColorResolver(entityClient, entityService)) - .dataFetcher("deleteTag", new DeleteTagResolver(entityClient)) - .dataFetcher("updateChart", new MutableTypeResolver<>(chartType)) - .dataFetcher("updateDashboard", new MutableTypeResolver<>(dashboardType)) - .dataFetcher("updateNotebook", new MutableTypeResolver<>(notebookType)) - .dataFetcher("updateDataJob", new MutableTypeResolver<>(dataJobType)) - .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) - .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) - .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) - .dataFetcher( - "updateERModelRelationship", - new UpdateERModelRelationshipResolver(this.entityClient)) - .dataFetcher( - "createERModelRelationship", - new CreateERModelRelationshipResolver( - this.entityClient, this.erModelRelationshipService)) - .dataFetcher("addTag", new AddTagResolver(entityService)) - .dataFetcher("addTags", new AddTagsResolver(entityService)) - .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) - .dataFetcher("removeTag", new RemoveTagResolver(entityService)) - .dataFetcher("batchRemoveTags", new BatchRemoveTagsResolver(entityService)) - .dataFetcher("addTerm", new AddTermResolver(entityService)) - .dataFetcher("batchAddTerms", new BatchAddTermsResolver(entityService)) - .dataFetcher("addTerms", new AddTermsResolver(entityService)) - .dataFetcher("removeTerm", new RemoveTermResolver(entityService)) - .dataFetcher("batchRemoveTerms", new BatchRemoveTermsResolver(entityService)) - .dataFetcher("createPolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("updatePolicy", new UpsertPolicyResolver(this.entityClient)) - .dataFetcher("deletePolicy", new DeletePolicyResolver(this.entityClient)) - .dataFetcher( - "updateDescription", - new UpdateDescriptionResolver(entityService, this.entityClient)) - .dataFetcher("addOwner", new AddOwnerResolver(entityService)) - .dataFetcher("addOwners", new AddOwnersResolver(entityService)) - .dataFetcher("batchAddOwners", new BatchAddOwnersResolver(entityService)) - .dataFetcher("removeOwner", new RemoveOwnerResolver(entityService)) - .dataFetcher("batchRemoveOwners", new BatchRemoveOwnersResolver(entityService)) - .dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient)) - .dataFetcher("removeLink", new RemoveLinkResolver(entityService)) - .dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService)) - .dataFetcher( - "removeGroupMembers", new RemoveGroupMembersResolver(this.groupService)) - .dataFetcher("createGroup", new CreateGroupResolver(this.groupService)) - .dataFetcher("removeUser", new RemoveUserResolver(this.entityClient)) - .dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient)) - .dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient)) - .dataFetcher( - "createDomain", new CreateDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "moveDomain", new MoveDomainResolver(this.entityService, this.entityClient)) - .dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient)) - .dataFetcher( - "setDomain", new SetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService)) - .dataFetcher( - "updateDeprecation", - new UpdateDeprecationResolver(this.entityClient, this.entityService)) - .dataFetcher( - "batchUpdateDeprecation", new BatchUpdateDeprecationResolver(entityService)) - .dataFetcher( - "unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createSecret", new CreateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher("deleteSecret", new DeleteSecretResolver(this.entityClient)) - .dataFetcher( - "updateSecret", new UpdateSecretResolver(this.entityClient, this.secretService)) - .dataFetcher( - "createAccessToken", new CreateAccessTokenResolver(this.statefulTokenService)) - .dataFetcher( - "revokeAccessToken", - new RevokeAccessTokenResolver(this.entityClient, this.statefulTokenService)) - .dataFetcher( - "createIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "updateIngestionSource", new UpsertIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "deleteIngestionSource", new DeleteIngestionSourceResolver(this.entityClient)) - .dataFetcher( - "createIngestionExecutionRequest", - new CreateIngestionExecutionRequestResolver( - this.entityClient, this.ingestionConfiguration)) - .dataFetcher( - "cancelIngestionExecutionRequest", - new CancelIngestionExecutionRequestResolver(this.entityClient)) .dataFetcher( - "createTestConnectionRequest", - new CreateTestConnectionRequestResolver( - this.entityClient, this.ingestionConfiguration)) + "createBusinessAttribute", + new CreateBusinessAttributeResolver( + this.entityClient, this.entityService, this.businessAttributeService)) .dataFetcher( - "deleteAssertion", - new DeleteAssertionResolver(this.entityClient, this.entityService)) - .dataFetcher("createTest", new CreateTestResolver(this.entityClient)) - .dataFetcher("updateTest", new UpdateTestResolver(this.entityClient)) - .dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient)) - .dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient)) - .dataFetcher( - "createGlossaryTerm", - new CreateGlossaryTermResolver(this.entityClient, this.entityService)) - .dataFetcher( - "createGlossaryNode", - new CreateGlossaryNodeResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateParentNode", - new UpdateParentNodeResolver(this.entityService, this.entityClient)) - .dataFetcher( - "deleteGlossaryEntity", - new DeleteGlossaryEntityResolver(this.entityClient, this.entityService)) - .dataFetcher( - "updateName", new UpdateNameResolver(this.entityService, this.entityClient)) + "updateBusinessAttribute", + new UpdateBusinessAttributeResolver( + this.entityClient, this.businessAttributeService)) .dataFetcher( - "addRelatedTerms", - new AddRelatedTermsResolver(this.entityService, this.entityClient)) + "deleteBusinessAttribute", + new DeleteBusinessAttributeResolver(this.entityClient)) .dataFetcher( - "removeRelatedTerms", - new RemoveRelatedTermsResolver(this.entityService, this.entityClient)) - .dataFetcher( - "createNativeUserResetToken", - new CreateNativeUserResetTokenResolver(this.nativeUserService)) + "addBusinessAttribute", new AddBusinessAttributeResolver(this.entityService)) .dataFetcher( - "batchUpdateSoftDeleted", - new BatchUpdateSoftDeletedResolver(this.entityService)) - .dataFetcher("updateUserSetting", new UpdateUserSettingResolver(this.entityService)) - .dataFetcher("rollbackIngestion", new RollbackIngestionResolver(this.entityClient)) - .dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService)) - .dataFetcher( - "createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService)) - .dataFetcher( - "acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService)) - .dataFetcher("createPost", new CreatePostResolver(this.postService)) - .dataFetcher("deletePost", new DeletePostResolver(this.postService)) - .dataFetcher("updatePost", new UpdatePostResolver(this.postService)) - .dataFetcher( - "batchUpdateStepStates", new BatchUpdateStepStatesResolver(this.entityClient)) - .dataFetcher("createView", new CreateViewResolver(this.viewService)) - .dataFetcher("updateView", new UpdateViewResolver(this.viewService)) - .dataFetcher("deleteView", new DeleteViewResolver(this.viewService)) - .dataFetcher( - "updateGlobalViewsSettings", - new UpdateGlobalViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateCorpUserViewsSettings", - new UpdateCorpUserViewsSettingsResolver(this.settingsService)) - .dataFetcher( - "updateLineage", - new UpdateLineageResolver(this.entityService, this.lineageService)) - .dataFetcher("updateEmbed", new UpdateEmbedResolver(this.entityService)) - .dataFetcher("createQuery", new CreateQueryResolver(this.queryService)) - .dataFetcher("updateQuery", new UpdateQueryResolver(this.queryService)) - .dataFetcher("deleteQuery", new DeleteQueryResolver(this.queryService)) - .dataFetcher( - "createDataProduct", new CreateDataProductResolver(this.dataProductService)) - .dataFetcher( - "updateDataProduct", new UpdateDataProductResolver(this.dataProductService)) - .dataFetcher( - "deleteDataProduct", new DeleteDataProductResolver(this.dataProductService)) - .dataFetcher( - "batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService)) - .dataFetcher( - "createOwnershipType", - new CreateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "updateOwnershipType", - new UpdateOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher( - "deleteOwnershipType", - new DeleteOwnershipTypeResolver(this.ownershipTypeService)) - .dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService)) - .dataFetcher("batchAssignForm", new BatchAssignFormResolver(this.formService)) - .dataFetcher( - "createDynamicFormAssignment", - new CreateDynamicFormAssignmentResolver(this.formService)) - .dataFetcher( - "verifyForm", new VerifyFormResolver(this.formService, this.groupService)) - .dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService)) - .dataFetcher( - "upsertStructuredProperties", - new UpsertStructuredPropertiesResolver(this.entityClient)) - .dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient)) - .dataFetcher( - "updateIncidentStatus", - new UpdateIncidentStatusResolver(this.entityClient, this.entityService)); - if (featureFlags.isBusinessAttributeEntityEnabled()) { - typeWiring - .dataFetcher( - "createBusinessAttribute", - new CreateBusinessAttributeResolver( - this.entityClient, this.entityService, this.businessAttributeService)) - .dataFetcher( - "updateBusinessAttribute", - new UpdateBusinessAttributeResolver( - this.entityClient, this.businessAttributeService)) - .dataFetcher( - "deleteBusinessAttribute", - new DeleteBusinessAttributeResolver(this.entityClient)) - .dataFetcher( - "addBusinessAttribute", new AddBusinessAttributeResolver(this.entityService)) - .dataFetcher( - "removeBusinessAttribute", - new RemoveBusinessAttributeResolver(this.entityService)); - } - return typeWiring; - }); - + "removeBusinessAttribute", + new RemoveBusinessAttributeResolver(this.entityService)); + } + return typeWiring; + }); } private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder) { diff --git a/entity-registry/src/test/java/com/linkedin/metadata/models/OpenApiSpecBuilderTest.java b/entity-registry/src/test/java/com/linkedin/metadata/models/OpenApiSpecBuilderTest.java index c482b75956c191..bc39d0b4bb1685 100644 --- a/entity-registry/src/test/java/com/linkedin/metadata/models/OpenApiSpecBuilderTest.java +++ b/entity-registry/src/test/java/com/linkedin/metadata/models/OpenApiSpecBuilderTest.java @@ -108,9 +108,9 @@ public void testOpenApiSpecBuilder() throws Exception { Path.of(getClass().getResource("/").getPath(), "open-api.yaml"), openapiYaml.getBytes(StandardCharsets.UTF_8)); - assertEquals(openAPI.getComponents().getSchemas().size(), 914); - assertEquals(openAPI.getComponents().getParameters().size(), 56); - assertEquals(openAPI.getPaths().size(), 102); + assertEquals(openAPI.getComponents().getSchemas().size(), 930); + assertEquals(openAPI.getComponents().getParameters().size(), 57); + assertEquals(openAPI.getPaths().size(), 104); } private OpenAPI generateOpenApiSpec(EntityRegistry entityRegistry) { diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index 03cf6c4b439c74..34fe5493a24be3 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -2,6 +2,7 @@ import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; +import java.util.Arrays; import java.util.List; /** Static class containing commonly-used constants across DataHub services. */ @@ -387,7 +388,7 @@ public class Constants { public static final String BUSINESS_ATTRIBUTE_ASSOCIATION = "businessAttributeAssociation"; public static final String BUSINESS_ATTRIBUTE_ASPECT = "businessAttributes"; public static final List SKIP_REFERENCE_ASPECT = - List.of("ownership", "status", "institutionalMemory"); + Arrays.asList("ownership", "status", "institutionalMemory"); // Posts public static final String POST_INFO_ASPECT_NAME = "postInfo"; From 6640d2b45fd7dbac5e7282dc41a735a149c3af71 Mon Sep 17 00:00:00 2001 From: Deepak Garg Date: Mon, 15 Apr 2024 20:41:38 +0530 Subject: [PATCH 50/50] business-attributes: fix failing frontend test --- .../app/entity/businessAttribute/BusinessAttributeEntity.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx index 1b719a7a4b91e9..b827a3c37d6a5c 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx @@ -61,7 +61,7 @@ export class BusinessAttributeEntity implements Entity { getCustomCardUrlPath = () => PageRoutes.BUSINESS_ATTRIBUTE; - isBrowseEnabled = () => true; + isBrowseEnabled = () => false; isLineageEnabled = () => false;