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 b0b26f073876c..665c6b5385045 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 b0f23e63177e6..1b0c942f15c42 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 02921b453e315..4fa9018d91b85 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 0000000000000..24e60a5aee767 --- /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 0000000000000..c1cf3443e4365 --- /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 0000000000000..63f274251dac7 --- /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 0000000000000..de3a32b783238 --- /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 0000000000000..b15b817682fb6 --- /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 75c09d0cf7e43..f3a976577627c 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 7174f3edffee6..71338e4afbf8c 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 9c5cddb3c50bc..67531a2053274 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 59d5d6939c04c..cd2054e92230e 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 d6e6e5610da56..f8b0adf29be8c 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 0e316ac1296ee..9bcfc7d5ee55b 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 0000000000000..ff9e7827a2770 --- /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 a93c7d5b333da..4d0312db754a7 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 fb146ef72877d..0533a51512822 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 0000000000000..eb98bbd5fc9ca --- /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 0000000000000..1008fcf18cf29 --- /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 34bf56a396b62..e8c6b7666a804 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 075a3b0fac43b..e2df6deec0d27 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 035f756a10d55..c8a80234a2d4e 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 af850c9c3ce28..2bcd58e396ff4 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 972f52b8824ce..c06215c8aea70 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 0000000000000..9a7d892294030 --- /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 0000000000000..648a35d79534a --- /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 0000000000000..c68ca97c939be --- /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 4e6e135ae05da..2f3253deeb5fc 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 a5296d074093b..40b4f98debe71 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 df960808d8a41..a47617dd7dd9c 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 665c6b5385045..438e4673ba3f9 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 157fb10ce7078..f04559ce4dec3 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 c1cf3443e4365..dbe45a61676ec 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 b15b817682fb6..e015d472d0bce 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 eb98bbd5fc9ca..4c57f34af9552 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 1008fcf18cf29..ad89071c19488 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 c8a80234a2d4e..880c2d0b0ef1a 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 0000000000000..1eee928e734c7 --- /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 c50b4c9088bc2..2861e0ddbf32e 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 0000000000000..5aa10eef0603b --- /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 438e4673ba3f9..99a46c1a41a4d 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 0000000000000..2af48612f7856 --- /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 0000000000000..565f542015027 --- /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 880c2d0b0ef1a..dc75a94da0e3b 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 0000000000000..139a77d463df5 --- /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 2f3253deeb5fc..3b05e5a616b04 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 3e1b975311b11..27581334814ce 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 3a0df137a0469..917540aca8728 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 a9de21d08aedc..8b009434ef3c3 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 0c92a981c7356..33cfba0f27802 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 579f1d7c7dddc..f3eb9d38dc6ae 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 5eaa34bc7a2e9..7284cd2bac48f 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 68f9fe8ae152e..7056368d82c7d 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 958ec13b37fca..0fb6a18a7974b 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 3346ddd23e3ba..9fbb3e9b6698e 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 2a4cf40b58412..42f0894fbb7a6 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 d75ec58546465..c4532cba9e6be 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 0403fa2ceea6f..cafd5b61c9dbf 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 d79a4a1919af9..972017bdcaa78 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 c7618e5d3c5a1..3eac87e268f5d 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 45e542883b723..1733537e68f30 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 b20953749ac35..4352959f5fb2e 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 6febf225ad77d..9aa40edd0b118 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 e29dd6809b968..44da57d0bdfb9 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 8391af60f8ece..9e90a8910db87 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 a21b0c1cd30be..e8e68dae4c368 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 99a46c1a41a4d..b0d96d400ad40 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 de3a32b783238..6afedb7b2e3a5 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 ad89071c19488..93a4301b1a362 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 dc75a94da0e3b..f10cec261e5d2 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 342a89f350429..471b0bced3f4c 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 a2e14308e8cee..72eb6176bd4f5 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 d2ad4ab6f4db1..766c6689c3fca 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 2734026400933..a06a99916b9ef 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 0000000000000..ae306998910da --- /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 0000000000000..7533d67f7b69a --- /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 0000000000000..975d707831e73 --- /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 0000000000000..ef665e45aeefd --- /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 0000000000000..7c138c99dbd1a --- /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 5920919a9cdab..4d43ded678a2f 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 6642c2c7b0467..f7bffa4159c15 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 0000000000000..c75393ff013cc --- /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 0000000000000..d402fef600d52 --- /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 080ee5889aec9..27759f49fce97 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 76da58e1ed6d2..beacce04b4340 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 4a7a4938ea970..3d141fac1d503 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 37a3758712ad6..a831f9338a53f 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 82378bb621427..ac63744be056d 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 0000000000000..9464029cdb3fb --- /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 876be12fd335b..f978139c41b1b 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 9464029cdb3fb..d801f7a92d755 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 2af48612f7856..ee05fbda70b19 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 dbe45a61676ec..8a4916b0e2856 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 63f274251dac7..ebbe68e8ea414 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 565f542015027..f497d63fbd6ec 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 e015d472d0bce..c1a31ef0ae05a 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 f10cec261e5d2..4cb247389abbf 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 0000000000000..7064d12bca322 --- /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 0000000000000..c59afc0d6134b --- /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 0000000000000..b6cad4c57b286 --- /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 0000000000000..a76250031f429 --- /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 0000000000000..d6f664169c90a --- /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 0000000000000..ccff7b1c9f630 --- /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 0000000000000..970ef07525ea7 --- /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 efd75a7fb07f5..20142cfbb799c 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 b0d96d400ad40..b60fc9f1b4984 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 0533a51512822..c8e5bcadebd10 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 0000000000000..00e850517212d --- /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 922574d5051d3..3ad74bacd1e82 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 4cb247389abbf..a1bac2433b439 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 0000000000000..4d8f722aec988 --- /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 0000000000000..051979d696f49 --- /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 975d707831e73..c23fca800cb6a 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 0000000000000..938cb34a86d2d --- /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 c75393ff013cc..cf6f3718f42b2 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 d402fef600d52..323c287a0acd7 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 0000000000000..fc8208becc56b --- /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 9df5923d18542..edf71aa608a16 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 41b92aea93b5a..c654cdeb15759 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 0000000000000..2bc28fe681183 --- /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 a57344e5733b4..212b556a2b7e9 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 0000000000000..9653d371e37d5 --- /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 0000000000000..0f70f8a2b5630 --- /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 0000000000000..ca2d532b06dc3 --- /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 0000000000000..1a69ed23b2f00 --- /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 658ce2b47c567..044a9b639c5ba 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 72474911b9310..f7316bbbd9000 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 439d20810ef7c..4071ae38e898a 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 c23fca800cb6a..60dd4cee72889 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 fc8208becc56b..46d9d4ea51d24 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 c654cdeb15759..730de331086be 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 2bc28fe681183..785a80fba681d 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 53765acb8e29e..35f49eff1c897 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 3b05e5a616b04..5d8916fcaf7b5 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 888c4a0e99931..497f69ba0cec7 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 207c2284e2673..5d9bd9e1f0dfd 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 93a4301b1a362..f5a3764698232 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 4c57f34af9552..964c943369ef3 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 0000000000000..e7cdbafdd54cc --- /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 69389f5dcf6fc..09fa23dbc9f57 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 6596711d4e82a..2dfdb2468d07a 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 c32dca8ba0537..bd9ac540b0220 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 0f70f8a2b5630..0d426ba7c7c77 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 f978139c41b1b..aabf8639919d4 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 ff9e7827a2770..d3fab88e91e2a 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 f5a3764698232..e881a3a24594b 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 60dd4cee72889..a2078b8789333 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 938cb34a86d2d..ec8c44d79901c 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 e7cdbafdd54cc..0b90665fe3a3b 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 d801f7a92d755..c58b2cd8451a5 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 c06215c8aea70..e6c3fdc5bc7de 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 0000000000000..65e6afd7ba66c --- /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 0000000000000..f2775f0b3478a --- /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 0000000000000..dd31629ff116e --- /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 4a1de4c3421ed..3c862430d2645 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 3b65ecccad336..6358659c6c6a1 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 89a7e7dd8d71a..500954aedcfee 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 72218c37fe5ce..cb2488b3e092f 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 ee05fbda70b19..d07e858dcde15 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 24e60a5aee767..c30e26a3bf9e6 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 f497d63fbd6ec..86028869d0715 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 f6bc68caa0821..d01717e4ae128 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 9bcfc7d5ee55b..a7c766b5b713e 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 0fc4399ac902d..d4bb1b1440773 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 a47617dd7dd9c..4d5565e537dd0 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 32e68e7b13343..68d6807a2ddc2 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 00e850517212d..71f5390d13901 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 3ad74bacd1e82..eb73dc558b341 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 dd31629ff116e..5c4abde5c1e2b 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 6358659c6c6a1..4bd1c9788085d 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 500954aedcfee..90e86a27483f1 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 86028869d0715..8a3d936fc1250 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 72eb6176bd4f5..02bce714ae23f 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 ae306998910da..4e56d81203b6f 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 0000000000000..51a6db654129f --- /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 0b90665fe3a3b..db7204abfd933 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 0903aeeaf4fe5..1d8db5f399422 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 0000000000000..4b4faaf607e8f --- /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 0000000000000..3c8ec3a87c4bd --- /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 8fa6b43e5b5d2..0039114ff9c14 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 1baa33901724f..40af628f3f5a1 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 5e3664f944edf..e2130d98b0496 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 3b2ee1afaba58..22a3af1584771 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 db7204abfd933..da2b108c2d8d0 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 6bae9cdc5626e..9f686af6c33b8 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 431a87aed3b62..095f728387afc 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 d07e858dcde15..a213dd224648f 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 c30e26a3bf9e6..b545c08a622e3 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 8a4916b0e2856..2103d6d4eceef 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 ebbe68e8ea414..b397c27834392 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 6afedb7b2e3a5..23b17f999c98d 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 8a3d936fc1250..a434bb11afd4f 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 c1a31ef0ae05a..eff3a213adb07 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 1cffb3c585e9f..5f2177994ffc0 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 9a36d0b70f1a6..5f1ffb6a94b99 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 85fa5c34fae13..d1cf5ed9feb2a 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 40b3797929742..e501ac7ae87e7 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 d3fab88e91e2a..a01fe020fd8bd 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 72bfb10463c8e..963d90c2e5692 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 ee8a23ce4bb2a..43b8c60454fc4 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 964c943369ef3..063e29c70648a 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 e881a3a24594b..59815900e1dff 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 71f5390d13901..c374d6a99aedb 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 5ad5451de27bf..0a6b5302e7417 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 0bf025d9243f7..c452316894f2b 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 7064d12bca322..9fcda136d2d05 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 c59afc0d6134b..abed58aa88376 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 b6cad4c57b286..a5fb7fbe54cff 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 a76250031f429..114402a5b24db 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 d6f664169c90a..b0b53c2d77213 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 ccff7b1c9f630..7535576a0bdce 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 970ef07525ea7..c3267e060801d 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 f6d247e5a1d28..11418caa19857 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 26705fdb363ef..72f0149df23f2 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 65e6afd7ba66c..6749f44b3ee52 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 f2775f0b3478a..03a30c95477ab 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 5c4abde5c1e2b..d797c2d1668d9 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 f6d60d0b4a3c9..a7c4cf2e863d6 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 1eee928e734c7..a9381af48b3d2 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 ba554e14b6c39..7d0c291ecd7eb 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 e69de29bb2d1d..8b137891791fe 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 39a7e4722988e..4d2bb2f3faf64 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 5aa10eef0603b..9cb47ded0819e 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 f0941a3202f1d..cc14b1841405d 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 0000000000000..677b7a011a82c --- /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 955d5c67c09a7..3d3ea4461bf60 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 0000000000000..cb26a65fd82f7 --- /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 3083642c5bfb6..d9f2ecdfebc61 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 4f74681570802..f97268d3d24b5 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 2cd4cbd6dcb6c..ce8d03fbdc960 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 a9c406306880d..8263467290cbb 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 c5b1cd28864e3..35f3bbd7571da 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 0000000000000..75a7f586bcf91 --- /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 410d2801d51c8..9c631d769e779 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 785a80fba681d..9ac8f91bb7a67 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 5f2b5d23771c0..3709449605c9b 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 0039114ff9c14..3b6aef97de040 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 c6cfda75bdd6f..fb59783ebfba9 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 4c09a698981e9..aeafe95d722ff 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 1e6d8bec01813..c8691499b71f9 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 0000000000000..9561b3f9589ba --- /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 11cd2352025ef..169a7ed17c46a 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 9cf8b4174ecfb..a2ff81da56401 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 2546674f9835c..38a3cbdeb458a 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 9a75cc1f751d3..02fd1b22b52d6 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 54f2206798da0..fcad25156884a 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 0000000000000..d4093b27cb939 --- /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 0000000000000..1b7de5695b5be --- /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 0000000000000..db6cf46dfc96f --- /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 450b299b48b34..556fad6fa1f7b 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 1a64359008dd8..b657bd2c274fd 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 11418caa19857..1badd0d8627b7 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 974406c0be0df..42ae82e0cf43f 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 4322ea90edf1f..afc831b004ec3 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 79f530f18a345..15bf6a411af80 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 0a9a9fbbad086..6266eaab0d96b 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 76153a8d2adb3..62cabc8b04e0c 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 7709ff16f7940..9ffdb8b600222 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 7ddccb0d56724..9c10ae9c97c5a 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 3ac05ed122cd7..534dca8dc00f5 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 d52a80d685fd5..c47fa13f86b8c 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 72f0149df23f2..b01239d79ae43 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 38d630bc302f4..ba13d41244795 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 02c9ea800f0af..113196c3d38c7 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 5d8916fcaf7b5..4b0cf93800484 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 c5539b001e9e3..9656c7d2f60ef 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 d9b0f4b73d580..57d2082273e80 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 27aa9ee04cc75..6e450f2b480b9 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 92321cce3d905..8be6f83832abc 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 0013d6615a71d..f34faea89a870 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 9561b3f9589ba..e6debe9a88d1e 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 0000000000000..54b4c3eb3c2ad --- /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 0000000000000..62f6fd0fceda2 --- /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 0000000000000..081633a32bff8 --- /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 2fd19af92971e..3c9e916a96dfa 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 23b17f999c98d..00ea5975d260e 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 a01fe020fd8bd..25dc36f74ef73 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 063e29c70648a..63575ea08336f 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 a5fb7fbe54cff..8dfec0f22b5ac 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 7535576a0bdce..44474956eec0b 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 c3267e060801d..efc84c9140957 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 fcad25156884a..c79ea5de69e27 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 1b7de5695b5be..4f03df973467a 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 f31a04367636b..8b2eb5a781701 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 fc1a8199a39f3..33cc1f028258e 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 0000000000000..062298796dd7c --- /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 8d504c562c99c..49a15b43d06aa 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 6e2d90287d5d9..312314d431fb4 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 0000000000000..e9bd46a7cf43a --- /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 0000000000000..2921cc2e389ab --- /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 0000000000000..9384a7d0d9a9c --- /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 0000000000000..2197ef81c4031 --- /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 0000000000000..eab805e4fc7b5 --- /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 0000000000000..e04faeab3b0e7 --- /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 0000000000000..b128f6780e4fb --- /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 0000000000000..9c732c9678c6f --- /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 0000000000000..8116753a4b727 --- /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 0000000000000..0aab3d091d0ff --- /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 d14bf635208e3..bd452dfb492d0 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 33cc1f028258e..3fb95996f5354 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 248872908a39c..a7ba78230e9de 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 5683b571888e0..52067003eef14 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 59815900e1dff..1c5c2e7eb14d6 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 c58b2cd8451a5..544a5083d1f2b 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 33cc1f028258e..3fb95996f5354 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 8b2eb5a781701..8696d5a1d557c 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 a7ba78230e9de..7415c6e5ce5aa 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 52067003eef14..1aa92b71b7a52 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 1f9487be28e91..9651198bb7984 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 b7391795df4f2..96c6262a4bd1d 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 a213dd224648f..eb477dff088ab 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 b545c08a622e3..c5ac56a13040b 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 a434bb11afd4f..63e9ac562a658 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 c374d6a99aedb..104bc6ecd9222 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 3c6d5cd9c0715..0ae41eef6b1b1 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 c452316894f2b..f54adbe8ba26c 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 254a1ed1767f1..7e145e4ba650e 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 9f14bf52733ea..04b3567df41b6 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 064a52f1e18a6..f3366f74c9fdb 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 9fcda136d2d05..f787879b7a0a6 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 b0b53c2d77213..78909a6910c13 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 20142cfbb799c..4df822377ddf2 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 4dec644cf3711..989ec31961b57 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 3deffe4f6c50b..9d18342d9b27d 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 3fb95996f5354..ed9410da2d9c5 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 0000000000000..31893e95e1a97 --- /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 0000000000000..105fb1fefec21 --- /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 1f9487be28e91..9c5cc5fc9b203 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 3d7a87a04e2c8..c45df945937ba 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 03a30c95477ab..f0369bc4ace13 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 a7c4cf2e863d6..1f094bb6ca989 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 139a77d463df5..5422864185f14 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 9a7d892294030..6236c9e77f455 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 648a35d79534a..5c134804af19d 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 0000000000000..5b6403dcc2c0a --- /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 c68ca97c939be..0000000000000 --- 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 4b0cf93800484..4e6e135ae05da 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 0e91018969ce3..87c18ca17b33f 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 6d3cab816aa35..468613181aef9 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 e953d8bc8ebf8..fff84d00494e7 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 aa81b072da904..48fcf63110229 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 1a09456fa6740..d7199bed56d2c 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 94d3e18df7c20..c9733639c8909 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 9eeadd3e22a1a..b2e0b604b7c32 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 0000000000000..c12a1be0d96ac --- /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 a638644d7aa11..0000000000000 --- 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 50cb49d0bd81c..b5317dd0ac78c 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 54b4c3eb3c2ad..f7daf453d4676 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 081633a32bff8..f7296ec240750 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 5b6403dcc2c0a..8b7df311d24d9 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 c0f82d8536922..a4641faf4c717 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 88900e06d4845..cf49b65b8c1aa 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 13766994c3a03..bc36de5e1ee22 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 b7391795df4f2..c538b73befbbd 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 667ccd368a729..62067db67e0c6 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 9e82430378827..fc4140c4ffc2c 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 bc36de5e1ee22..a82aeee29b80b 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 c538b73befbbd..6612228a1374b 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 6612228a1374b..0fd3a7d6a81e8 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 2103d6d4eceef..93de29d8c1bf0 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 81d77151dc668..fca797902dec4 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 8dfec0f22b5ac..e3dc2cb8a8f2f 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 503eb705fd91d..fab1cdafde259 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 1bb231b76336b..abb491814c278 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 c5ac56a13040b..041f5e9ade77f 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 93de29d8c1bf0..3c4f6315016fd 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 eff3a213adb07..32724ba13e8ac 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 63575ea08336f..5acfba5a1536e 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 1c5c2e7eb14d6..87230b2457716 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 047494663f5a5..85a6b9108cb54 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 d090fddb6df3b..d1c9b4cdc266f 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 0e7042e7002da..9185e2e7ee072 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 f7daf453d4676..68cd2aa565b9f 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 e010bbc97170a..18bd4b3f10a65 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 fbeade6ce7df6..e085d9f624992 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 75a7f586bcf91..d688a80ce3f5b 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 9c631d769e779..2cd35c0f5c5b2 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 9ac8f91bb7a67..a8c1f9216a7d6 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 bd452dfb492d0..4dd11e3ee80c5 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 ef7df22538acc..c0c56cdf8dd2c 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 d072f125fce66..ed20722083032 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 0000000000000..4be0fa81a23f9 --- /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 0000000000000..2ac2be19ece89 --- /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 2415cee7f9e00..aff506779094f 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 90c6445060621..c05009e146308 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 1f1c5fc5a3a7b..c8fb2dedd5928 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 69d880dabfe22..c7e0a89ab38ea 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 766c6689c3fca..4ebcc6f090a4b 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 7533d67f7b69a..b16593f5497f6 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 38770fb16b5df..d34c317e403d2 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 e085d9f624992..a2176b5637be8 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 7291478161750..47e9c7716281e 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 a8c1f9216a7d6..6bedb96796d41 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 cc9f4b265455b..6574b70b20de6 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 3826776b10895..467e535f9bad4 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 731cd40f33c5e..88f6a4c9660d3 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 4aed0ec9bbad8..61306c9cf64d3 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 821d00b9017c3..f167ccad16474 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 00feaf8223410..b4f16e2d2a824 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 b7527d53b5705..7b47fc0302247 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 4b4faaf607e8f..5bbb19e85d9bc 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 84abde2bfe5b2..d7ac9b0085b18 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 fb59783ebfba9..81d4fb159368c 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 3b0df3ffdf650..c670e1b573245 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 a2078b8789333..61595045646c4 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 2ac2be19ece89..9fbf7b9647345 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 81d4fb159368c..c674ee75f61df 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 6236c9e77f455..388164bc8ca6e 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 0000000000000..086d9df34dead --- /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 04a4dd835715a..d7ab1f948b411 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 4be0fa81a23f9..91638d4997003 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 9fbf7b9647345..3f24b3a06e3a4 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 5bbb19e85d9bc..decee024f050b 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 7974a3ef7717b..0657dc238a154 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 0039114ff9c14..05140486e189b 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 c674ee75f61df..e2a74a15d3dfc 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 cf49b65b8c1aa..1f0d1b409fe0b 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 4d988035dedd7..01d61b6119b0a 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 18bd4b3f10a65..3dfe40d6b9b4b 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 6717ff383395b..3296853145a47 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 c482b75956c19..bc39d0b4bb168 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 03cf6c4b439c7..34fe5493a24be 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 1b719a7a4b91e..b827a3c37d6a5 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;