graphql & backend + models

This commit is contained in:
Gabe Lyons 2025-05-29 16:49:28 -07:00
parent de75524ca5
commit 6a5b619e1d
29 changed files with 1891 additions and 6 deletions

View File

@ -26,6 +26,11 @@ import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.featureflags.FeatureFlags;
import com.linkedin.datahub.graphql.generated.*;
import com.linkedin.datahub.graphql.resolvers.MeResolver;
import com.linkedin.datahub.graphql.resolvers.application.BatchSetApplicationResolver;
import com.linkedin.datahub.graphql.resolvers.application.CreateApplicationResolver;
import com.linkedin.datahub.graphql.resolvers.application.DeleteApplicationResolver;
import com.linkedin.datahub.graphql.resolvers.application.ListApplicationAssetsResolver;
import com.linkedin.datahub.graphql.resolvers.application.UpdateApplicationResolver;
import com.linkedin.datahub.graphql.resolvers.assertion.AssertionRunEventResolver;
import com.linkedin.datahub.graphql.resolvers.assertion.DeleteAssertionResolver;
import com.linkedin.datahub.graphql.resolvers.assertion.EntityAssertionsResolver;
@ -241,6 +246,7 @@ import com.linkedin.datahub.graphql.types.BrowsableEntityType;
import com.linkedin.datahub.graphql.types.EntityType;
import com.linkedin.datahub.graphql.types.LoadableType;
import com.linkedin.datahub.graphql.types.SearchableEntityType;
import com.linkedin.datahub.graphql.types.application.ApplicationType;
import com.linkedin.datahub.graphql.types.aspect.AspectType;
import com.linkedin.datahub.graphql.types.assertion.AssertionType;
import com.linkedin.datahub.graphql.types.auth.AccessTokenMetadataType;
@ -315,6 +321,7 @@ import com.linkedin.metadata.models.registry.EntityRegistry;
import com.linkedin.metadata.query.filter.SortCriterion;
import com.linkedin.metadata.query.filter.SortOrder;
import com.linkedin.metadata.recommendation.RecommendationsService;
import com.linkedin.metadata.service.ApplicationService;
import com.linkedin.metadata.service.AssertionService;
import com.linkedin.metadata.service.BusinessAttributeService;
import com.linkedin.metadata.service.DataProductService;
@ -395,6 +402,7 @@ public class GmsGraphQLEngine {
private ConnectionService connectionService;
private AssertionService assertionService;
private final EntityVersioningService entityVersioningService;
private final ApplicationService applicationService;
private final BusinessAttributeService businessAttributeService;
private final FeatureFlags featureFlags;
@ -447,6 +455,7 @@ public class GmsGraphQLEngine {
private final DataHubViewType dataHubViewType;
private final QueryType queryType;
private final DataProductType dataProductType;
private final ApplicationType applicationType;
private final OwnershipType ownershipType;
private final StructuredPropertyType structuredPropertyType;
private final DataTypeType dataTypeType;
@ -521,6 +530,7 @@ public class GmsGraphQLEngine {
this.queryService = args.queryService;
this.erModelRelationshipService = args.erModelRelationshipService;
this.dataProductService = args.dataProductService;
this.applicationService = args.applicationService;
this.formService = args.formService;
this.restrictedService = args.restrictedService;
this.connectionService = args.connectionService;
@ -575,6 +585,7 @@ public class GmsGraphQLEngine {
this.dataHubViewType = new DataHubViewType(entityClient);
this.queryType = new QueryType(entityClient);
this.dataProductType = new DataProductType(entityClient);
this.applicationType = new ApplicationType(entityClient);
this.ownershipType = new OwnershipType(entityClient);
this.structuredPropertyType = new StructuredPropertyType(entityClient);
this.dataTypeType = new DataTypeType(entityClient);
@ -640,7 +651,8 @@ public class GmsGraphQLEngine {
restrictedType,
businessAttributeType,
dataProcessInstanceType,
executionRequestType));
executionRequestType,
applicationType));
this.loadableTypes = new ArrayList<>(entityTypes);
this.loadableTypes.add(ingestionSourceType);
// Extend loadable types with types from the plugins
@ -717,6 +729,7 @@ public class GmsGraphQLEngine {
configureGlossaryNodeResolvers(builder);
configureDomainResolvers(builder);
configureDataProductResolvers(builder);
configureApplicationResolvers(builder);
configureAssertionResolvers(builder);
configureContractResolvers(builder);
configurePolicyResolvers(builder);
@ -1047,8 +1060,11 @@ public class GmsGraphQLEngine {
"getQuickFilters",
new GetQuickFiltersResolver(this.entityClient, this.viewService))
.dataFetcher("dataProduct", getResolver(dataProductType))
.dataFetcher("application", getResolver(applicationType))
.dataFetcher(
"listDataProductAssets", new ListDataProductAssetsResolver(this.entityClient))
.dataFetcher(
"listApplicationAssets", new ListApplicationAssetsResolver(this.entityClient))
.dataFetcher(
"listOwnershipTypes", new ListOwnershipTypesResolver(this.entityClient))
.dataFetcher(
@ -1284,6 +1300,15 @@ public class GmsGraphQLEngine {
"deleteDataProduct", new DeleteDataProductResolver(this.dataProductService))
.dataFetcher(
"batchSetDataProduct", new BatchSetDataProductResolver(this.dataProductService))
.dataFetcher(
"createApplication",
new CreateApplicationResolver(this.applicationService, this.entityService))
.dataFetcher(
"updateApplication", new UpdateApplicationResolver(this.applicationService))
.dataFetcher(
"deleteApplication", new DeleteApplicationResolver(this.applicationService))
.dataFetcher(
"batchSetApplication", new BatchSetApplicationResolver(this.applicationService))
.dataFetcher(
"createOwnershipType", new CreateOwnershipTypeResolver(this.ownershipTypeService))
.dataFetcher(
@ -2834,6 +2859,18 @@ public class GmsGraphQLEngine {
.dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)));
}
private void configureApplicationResolvers(final RuntimeWiring.Builder builder) {
builder.type(
"Application",
typeWiring ->
typeWiring
.dataFetcher("entities", new ListApplicationAssetsResolver(this.entityClient))
.dataFetcher("privileges", new EntityPrivilegesResolver(entityClient))
.dataFetcher(
"aspects", new WeaklyTypedAspectsResolver(entityClient, entityRegistry))
.dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)));
}
private void configureAssertionResolvers(final RuntimeWiring.Builder builder) {
builder.type(
"Assertion",

View File

@ -29,6 +29,7 @@ import com.linkedin.metadata.graph.GraphClient;
import com.linkedin.metadata.graph.SiblingGraphService;
import com.linkedin.metadata.models.registry.EntityRegistry;
import com.linkedin.metadata.recommendation.RecommendationsService;
import com.linkedin.metadata.service.ApplicationService;
import com.linkedin.metadata.service.AssertionService;
import com.linkedin.metadata.service.BusinessAttributeService;
import com.linkedin.metadata.service.DataProductService;
@ -96,6 +97,7 @@ public class GmsGraphQLEngineArgs {
ConnectionService connectionService;
AssertionService assertionService;
EntityVersioningService entityVersioningService;
ApplicationService applicationService;
// any fork specific args should go below this line
}

View File

@ -0,0 +1,3 @@
package com.linkedin.datahub.graphql;
public class ListApplicationAssetsResolver {}

View File

@ -0,0 +1,137 @@
package com.linkedin.datahub.graphql.resolvers.application;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
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.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.BatchSetApplicationInput;
import com.linkedin.metadata.authorization.PoliciesConfig;
import com.linkedin.metadata.service.ApplicationService;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class BatchSetApplicationResolver implements DataFetcher<CompletableFuture<Boolean>> {
private final ApplicationService _applicationService;
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
@Override
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();
final BatchSetApplicationInput input =
bindArgument(environment.getArgument("input"), BatchSetApplicationInput.class);
final String maybeApplicationUrn = input.getApplicationUrn();
final List<String> resources = input.getResourceUrns();
return GraphQLConcurrencyUtils.supplyAsync(
() -> {
verifyResources(resources, context);
verifyApplication(maybeApplicationUrn, context);
try {
List<Urn> resourceUrns =
resources.stream().map(UrnUtils::getUrn).collect(Collectors.toList());
if (maybeApplicationUrn != null) {
batchSetApplication(maybeApplicationUrn, resourceUrns, context);
} else {
batchUnsetApplication(resourceUrns, context);
}
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);
}
},
this.getClass().getSimpleName(),
"get");
}
private void verifyResources(List<String> resources, QueryContext context) {
for (String resource : resources) {
if (!_applicationService.verifyEntityExists(
context.getOperationContext(), UrnUtils.getUrn(resource))) {
throw new RuntimeException(
String.format(
"Failed to batch set Application, %s in resources does not exist", resource));
}
Urn resourceUrn = UrnUtils.getUrn(resource);
if (!AuthorizationUtils.isAuthorized(
context,
resourceUrn.getEntityType(),
resourceUrn.toString(),
new DisjunctivePrivilegeGroup(
ImmutableList.of(
ALL_PRIVILEGES_GROUP,
new ConjunctivePrivilegeGroup(
ImmutableList.of(
PoliciesConfig.EDIT_ENTITY_DOMAINS_PRIVILEGE.getType())))))) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}
}
}
private void verifyApplication(String maybeApplicationUrn, QueryContext context) {
if (maybeApplicationUrn != null
&& !_applicationService.verifyEntityExists(
context.getOperationContext(), UrnUtils.getUrn(maybeApplicationUrn))) {
throw new RuntimeException(
String.format(
"Failed to batch set Application, Application urn %s does not exist",
maybeApplicationUrn));
}
}
private void batchSetApplication(
@Nonnull String applicationUrn, List<Urn> resources, QueryContext context) {
log.debug(
"Batch setting Application. application urn: {}, resources: {}", applicationUrn, resources);
try {
_applicationService.batchSetApplicationAssets(
context.getOperationContext(),
UrnUtils.getUrn(applicationUrn),
resources,
UrnUtils.getUrn(context.getActorUrn()));
} catch (Exception e) {
throw new RuntimeException(
String.format(
"Failed to batch set Application %s to resources with urns %s!",
applicationUrn, resources),
e);
}
}
private void batchUnsetApplication(List<Urn> resources, QueryContext context) {
log.debug("Batch unsetting Application. resources: {}", resources);
try {
for (Urn resource : resources) {
_applicationService.unsetApplication(
context.getOperationContext(), resource, UrnUtils.getUrn(context.getActorUrn()));
}
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to batch unset application for resources with urns %s!", resources),
e);
}
}
}

View File

@ -0,0 +1,96 @@
package com.linkedin.datahub.graphql.resolvers.application;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import com.datahub.authentication.Authentication;
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.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.Application;
import com.linkedin.datahub.graphql.generated.CreateApplicationInput;
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
import com.linkedin.datahub.graphql.types.application.mappers.ApplicationMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.metadata.authorization.PoliciesConfig;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.metadata.service.ApplicationService;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class CreateApplicationResolver implements DataFetcher<CompletableFuture<Application>> {
private final ApplicationService _applicationService;
private final EntityService _entityService;
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
@Override
public CompletableFuture<Application> get(final DataFetchingEnvironment environment)
throws Exception {
final QueryContext context = environment.getContext();
final CreateApplicationInput input =
bindArgument(environment.getArgument("input"), CreateApplicationInput.class);
final Authentication authentication = context.getAuthentication();
final Urn domainUrn = UrnUtils.getUrn(input.getDomainUrn());
return GraphQLConcurrencyUtils.supplyAsync(
() -> {
if (!_applicationService.verifyEntityExists(context.getOperationContext(), domainUrn)) {
throw new IllegalArgumentException("The Domain provided dos not exist");
}
final DisjunctivePrivilegeGroup orPrivilegeGroup =
new DisjunctivePrivilegeGroup(ImmutableList.of(ALL_PRIVILEGES_GROUP));
if (!AuthorizationUtils.isAuthorized(
context, domainUrn.getEntityType(), domainUrn.toString(), orPrivilegeGroup)) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}
try {
final Urn applicationUrn =
_applicationService.createApplication(
context.getOperationContext(),
input.getId(),
input.getProperties().getName(),
input.getProperties().getDescription());
_applicationService.setDomain(
context.getOperationContext(),
applicationUrn,
UrnUtils.getUrn(input.getDomainUrn()));
OwnerUtils.addCreatorAsOwner(
context, applicationUrn.toString(), OwnerEntityType.CORP_USER, _entityService);
EntityResponse response =
_applicationService.getApplicationEntityResponse(
context.getOperationContext(), applicationUrn);
if (response != null) {
return ApplicationMapper.map(context, response);
}
// should never happen
log.error(String.format("Unable to find application with urn %s", applicationUrn));
return null;
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to create a new Application from input %s", input), e);
}
},
this.getClass().getSimpleName(),
"get");
}
}

View File

@ -0,0 +1,71 @@
package com.linkedin.datahub.graphql.resolvers.application;
import com.datahub.authentication.Authentication;
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.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.domain.Domains;
import com.linkedin.metadata.authorization.PoliciesConfig;
import com.linkedin.metadata.service.ApplicationService;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class DeleteApplicationResolver implements DataFetcher<CompletableFuture<Boolean>> {
private final ApplicationService _applicationService;
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
@Override
public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment)
throws Exception {
final QueryContext context = environment.getContext();
final Urn applicationUrn = UrnUtils.getUrn(environment.getArgument("urn"));
final Authentication authentication = context.getAuthentication();
return GraphQLConcurrencyUtils.supplyAsync(
() -> {
if (!_applicationService.verifyEntityExists(
context.getOperationContext(), applicationUrn)) {
throw new IllegalArgumentException("The Application provided dos not exist");
}
Domains domains =
_applicationService.getApplicationDomains(
context.getOperationContext(), applicationUrn);
if (domains != null && domains.hasDomains() && domains.getDomains().size() > 0) {
// get first domain since we only allow one domain right now
Urn domainUrn = UrnUtils.getUrn(domains.getDomains().get(0).toString());
final DisjunctivePrivilegeGroup orPrivilegeGroup =
new DisjunctivePrivilegeGroup(ImmutableList.of(ALL_PRIVILEGES_GROUP));
if (!AuthorizationUtils.isAuthorized(
context, domainUrn.getEntityType(), domainUrn.toString(), orPrivilegeGroup)) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}
}
try {
_applicationService.deleteApplication(context.getOperationContext(), applicationUrn);
return true;
} catch (Exception e) {
throw new RuntimeException("Failed to delete Application", e);
}
},
this.getClass().getSimpleName(),
"get");
}
}

View File

@ -0,0 +1,207 @@
package com.linkedin.datahub.graphql.resolvers.application;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import static com.linkedin.metadata.search.utils.QueryUtils.buildFilterWithUrns;
import com.google.common.collect.ImmutableList;
import com.linkedin.application.ApplicationAssociation;
import com.linkedin.application.ApplicationProperties;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.data.DataMap;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.generated.Application;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.FacetFilterInput;
import com.linkedin.datahub.graphql.generated.SearchAcrossEntitiesInput;
import com.linkedin.datahub.graphql.generated.SearchResults;
import com.linkedin.datahub.graphql.resolvers.ResolverUtils;
import com.linkedin.datahub.graphql.types.common.mappers.SearchFlagsInputMapper;
import com.linkedin.datahub.graphql.types.entitytype.EntityTypeMapper;
import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.query.SearchFlags;
import com.linkedin.metadata.query.filter.Filter;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Resolver responsible for getting the assets belonging to an Application. Get the assets from the
* Application aspect, then use search to query and filter for specific assets.
*/
@Slf4j
@RequiredArgsConstructor
public class ListApplicationAssetsResolver
implements DataFetcher<CompletableFuture<SearchResults>> {
private static final int DEFAULT_START = 0;
private static final int DEFAULT_COUNT = 10;
private final EntityClient _entityClient;
@Override
public CompletableFuture<SearchResults> get(DataFetchingEnvironment environment) {
final QueryContext context = environment.getContext();
// get urn from either input or source (in the case of "entities" field)
final String urn =
environment.getArgument("urn") != null
? environment.getArgument("urn")
: ((Application) environment.getSource()).getUrn();
final Urn applicationUrn = UrnUtils.getUrn(urn);
final SearchAcrossEntitiesInput input =
bindArgument(environment.getArgument("input"), SearchAcrossEntitiesInput.class);
// 1. Get urns of assets belonging to Application using an aspect query
List<Urn> assetUrns = new ArrayList<>();
try {
final EntityResponse entityResponse =
_entityClient.getV2(
context.getOperationContext(),
Constants.APPLICATION_ENTITY_NAME,
applicationUrn,
Collections.singleton(Constants.APPLICATION_PROPERTIES_ASPECT_NAME));
if (entityResponse != null
&& entityResponse
.getAspects()
.containsKey(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)) {
final DataMap data =
entityResponse
.getAspects()
.get(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)
.getValue()
.data();
final ApplicationProperties applicationProperties = new ApplicationProperties(data);
if (applicationProperties.hasAssets()) {
assetUrns.addAll(
applicationProperties.getAssets().stream()
.map(ApplicationAssociation::getDestinationUrn)
.collect(Collectors.toList()));
}
}
} catch (Exception e) {
log.error(String.format("Failed to list application assets with urn %s", applicationUrn), e);
throw new RuntimeException(
String.format("Failed to list application assets with urn %s", applicationUrn), e);
}
// 2. Get list of entities that we should query based on filters or assets from aspect.
List<String> entitiesToQuery =
assetUrns.stream().map(Urn::getEntityType).distinct().collect(Collectors.toList());
final List<EntityType> inputEntityTypes =
(input.getTypes() == null || input.getTypes().isEmpty())
? ImmutableList.of()
: input.getTypes();
final List<String> inputEntityNames =
inputEntityTypes.stream()
.map(EntityTypeMapper::getName)
.distinct()
.collect(Collectors.toList());
final List<String> finalEntityNames =
inputEntityNames.size() > 0 ? inputEntityNames : entitiesToQuery;
// escape forward slash since it is a reserved character in Elasticsearch
final String sanitizedQuery = ResolverUtils.escapeForwardSlash(input.getQuery());
final int start = input.getStart() != null ? input.getStart() : DEFAULT_START;
final int count = input.getCount() != null ? input.getCount() : DEFAULT_COUNT;
return GraphQLConcurrencyUtils.supplyAsync(
() -> {
// if no assets in application properties, exit early before search and return empty
// results
if (assetUrns.size() == 0) {
SearchResults results = new SearchResults();
results.setStart(start);
results.setCount(count);
results.setTotal(0);
results.setSearchResults(ImmutableList.of());
return results;
}
List<FacetFilterInput> filters = input.getFilters();
final List<Urn> urnsToFilterOn = getUrnsToFilterOn(assetUrns, filters);
// add urns from the aspect to our filters
final Filter baseFilter = ResolverUtils.buildFilter(filters, input.getOrFilters());
final Filter finalFilter =
buildFilterWithUrns(
context.getDataHubAppConfig(), new HashSet<>(urnsToFilterOn), baseFilter);
final SearchFlags searchFlags;
com.linkedin.datahub.graphql.generated.SearchFlags inputFlags = input.getSearchFlags();
if (inputFlags != null) {
searchFlags = SearchFlagsInputMapper.INSTANCE.apply(context, inputFlags);
} else {
searchFlags = null;
}
try {
log.debug(
"Executing search for application assets: entity types {}, query {}, filters: {}, start: {}, count: {}",
input.getTypes(),
input.getQuery(),
input.getOrFilters(),
start,
count);
SearchResults results =
UrnSearchResultsMapper.map(
context,
_entityClient.searchAcrossEntities(
context
.getOperationContext()
.withSearchFlags(flags -> searchFlags != null ? searchFlags : flags),
finalEntityNames,
sanitizedQuery,
finalFilter,
start,
count,
null));
return results;
} catch (Exception e) {
log.error(
"Failed to execute search for application assets: entity types {}, query {}, filters: {}, start: {}, count: {}",
input.getTypes(),
input.getQuery(),
input.getOrFilters(),
start,
count);
throw new RuntimeException(
"Failed to execute search: "
+ String.format(
"entity types %s, query %s, filters: %s, start: %s, count: %s",
input.getTypes(), input.getQuery(), input.getOrFilters(), start, count),
e);
}
},
this.getClass().getSimpleName(),
"get");
}
/**
* Check to see if our filters list has a hardcoded filter for output ports. If so, let this
* filter determine which urns we filter search results on. Otherwise, if no output port filter is
* found, return all asset urns as per usual.
*/
@Nonnull
private List<Urn> getUrnsToFilterOn(
@Nonnull final List<Urn> assetUrns, @Nullable final List<FacetFilterInput> filters) {
// All logic related to output ports is removed.
// We simply return the original assetUrns list.
return assetUrns;
}
}

View File

@ -0,0 +1,94 @@
package com.linkedin.datahub.graphql.resolvers.application;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import com.datahub.authentication.Authentication;
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.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.Application;
import com.linkedin.datahub.graphql.generated.UpdateApplicationInput;
import com.linkedin.datahub.graphql.types.application.mappers.ApplicationMapper;
import com.linkedin.domain.Domains;
import com.linkedin.entity.EntityResponse;
import com.linkedin.metadata.authorization.PoliciesConfig;
import com.linkedin.metadata.service.ApplicationService;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class UpdateApplicationResolver implements DataFetcher<CompletableFuture<Application>> {
private final ApplicationService _applicationService;
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
new ConjunctivePrivilegeGroup(
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
@Override
public CompletableFuture<Application> get(final DataFetchingEnvironment environment)
throws Exception {
final QueryContext context = environment.getContext();
final UpdateApplicationInput input =
bindArgument(environment.getArgument("input"), UpdateApplicationInput.class);
final Urn applicationUrn = UrnUtils.getUrn(environment.getArgument("urn"));
final Authentication authentication = context.getAuthentication();
return GraphQLConcurrencyUtils.supplyAsync(
() -> {
if (!_applicationService.verifyEntityExists(
context.getOperationContext(), applicationUrn)) {
throw new IllegalArgumentException("The Application provided dos not exist");
}
Domains domains =
_applicationService.getApplicationDomains(
context.getOperationContext(), applicationUrn);
if (domains != null && domains.hasDomains() && domains.getDomains().size() > 0) {
// get first domain since we only allow one domain right now
Urn domainUrn = UrnUtils.getUrn(domains.getDomains().get(0).toString());
final DisjunctivePrivilegeGroup orPrivilegeGroup =
new DisjunctivePrivilegeGroup(ImmutableList.of(ALL_PRIVILEGES_GROUP));
if (!AuthorizationUtils.isAuthorized(
context, domainUrn.getEntityType(), domainUrn.toString(), orPrivilegeGroup)) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}
}
try {
final Urn urn =
_applicationService.updateApplication(
context.getOperationContext(),
applicationUrn,
input.getName(),
input.getDescription());
EntityResponse response =
_applicationService.getApplicationEntityResponse(
context.getOperationContext(), urn);
if (response != null) {
return ApplicationMapper.map(context, response);
}
// should never happen
log.error(String.format("Unable to find application with urn %s", applicationUrn));
return null;
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to update Application with urn %s", applicationUrn), e);
}
},
this.getClass().getSimpleName(),
"get");
}
}

View File

@ -5,6 +5,7 @@ import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;
import com.datahub.authorization.ConjunctivePrivilegeGroup;
import com.datahub.authorization.DisjunctivePrivilegeGroup;
import com.google.common.collect.ImmutableList;
import com.linkedin.application.ApplicationProperties;
import com.linkedin.businessattribute.BusinessAttributeInfo;
import com.linkedin.common.urn.Urn;
import com.linkedin.container.EditableContainerProperties;
@ -534,6 +535,32 @@ public class DescriptionUtils {
entityService);
}
public static void updateApplicationDescription(
@Nonnull OperationContext opContext,
String newDescription,
Urn resourceUrn,
Urn actor,
EntityService<?> entityService) {
ApplicationProperties applicationProperties =
(ApplicationProperties)
EntityUtils.getAspectFromEntity(
opContext,
resourceUrn.toString(),
Constants.APPLICATION_PROPERTIES_ASPECT_NAME,
entityService,
new ApplicationProperties());
if (applicationProperties != null) {
applicationProperties.setDescription(newDescription);
}
persistAspect(
opContext,
resourceUrn,
Constants.APPLICATION_PROPERTIES_ASPECT_NAME,
applicationProperties,
actor,
entityService);
}
public static void updateBusinessAttributeDescription(
@Nonnull OperationContext opContext,
String newDescription,

View File

@ -66,6 +66,8 @@ public class UpdateDescriptionResolver implements DataFetcher<CompletableFuture<
return updateDataProductDescription(targetUrn, input, environment.getContext());
case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME:
return updateBusinessAttributeDescription(targetUrn, input, environment.getContext());
case Constants.APPLICATION_ENTITY_NAME:
return updateApplicationDescription(targetUrn, input, environment.getContext());
default:
throw new RuntimeException(
String.format(
@ -552,6 +554,37 @@ public class UpdateDescriptionResolver implements DataFetcher<CompletableFuture<
"updateDataProductDescription");
}
private CompletableFuture<Boolean> updateApplicationDescription(
Urn targetUrn, DescriptionUpdateInput input, QueryContext context) {
return GraphQLConcurrencyUtils.supplyAsync(
() -> {
if (!DescriptionUtils.isAuthorizedToUpdateDescription(context, targetUrn)) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}
DescriptionUtils.validateLabelInput(
context.getOperationContext(), targetUrn, _entityService);
try {
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
DescriptionUtils.updateApplicationDescription(
context.getOperationContext(),
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);
}
},
this.getClass().getSimpleName(),
"updateApplicationDescription");
}
private CompletableFuture<Boolean> updateBusinessAttributeDescription(
Urn targetUrn, DescriptionUpdateInput input, QueryContext context) {
return GraphQLConcurrencyUtils.supplyAsync(

View File

@ -91,7 +91,8 @@ public class SearchUtils {
EntityType.DATA_PRODUCT,
EntityType.NOTEBOOK,
EntityType.BUSINESS_ATTRIBUTE,
EntityType.SCHEMA_FIELD);
EntityType.SCHEMA_FIELD,
EntityType.APPLICATION);
/** Entities that are part of autocomplete by default in Auto Complete Across Entities */
public static final List<EntityType> AUTO_COMPLETE_ENTITY_TYPES =
@ -112,7 +113,8 @@ public class SearchUtils {
EntityType.NOTEBOOK,
EntityType.DATA_PRODUCT,
EntityType.DOMAIN,
EntityType.BUSINESS_ATTRIBUTE);
EntityType.BUSINESS_ATTRIBUTE,
EntityType.APPLICATION);
/** Entities that are part of browse by default */
public static final List<EntityType> BROWSE_ENTITY_TYPES =

View File

@ -0,0 +1,131 @@
package com.linkedin.datahub.graphql.types.application;
import static com.linkedin.metadata.Constants.APPLICATION_ENTITY_NAME;
import static com.linkedin.metadata.Constants.APPLICATION_PROPERTIES_ASPECT_NAME;
import static com.linkedin.metadata.Constants.DOMAINS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.FORMS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.GLOBAL_TAGS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.GLOSSARY_TERMS_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.STRUCTURED_PROPERTIES_ASPECT_NAME;
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.Application;
import com.linkedin.datahub.graphql.generated.AutoCompleteResults;
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.types.SearchableEntityType;
import com.linkedin.datahub.graphql.types.application.mappers.ApplicationMapper;
import com.linkedin.datahub.graphql.types.mappers.AutoCompleteResultsMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.query.AutoCompleteResult;
import com.linkedin.metadata.query.filter.Filter;
import graphql.execution.DataFetcherResult;
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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.NotImplementedException;
@RequiredArgsConstructor
public class ApplicationType
implements SearchableEntityType<Application, String>,
com.linkedin.datahub.graphql.types.EntityType<Application, String> {
public static final Set<String> ASPECTS_TO_FETCH =
ImmutableSet.of(
APPLICATION_PROPERTIES_ASPECT_NAME,
OWNERSHIP_ASPECT_NAME,
GLOBAL_TAGS_ASPECT_NAME,
GLOSSARY_TERMS_ASPECT_NAME,
DOMAINS_ASPECT_NAME,
INSTITUTIONAL_MEMORY_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME);
private final EntityClient _entityClient;
@Override
public EntityType type() {
return EntityType.APPLICATION;
}
@Override
public Function<Entity, String> getKeyProvider() {
return Entity::getUrn;
}
@Override
public Class<Application> objectClass() {
return Application.class;
}
@Override
public List<DataFetcherResult<Application>> batchLoad(
@Nonnull List<String> urns, @Nonnull QueryContext context) throws Exception {
final List<Urn> applicationUrns =
urns.stream().map(UrnUtils::getUrn).collect(Collectors.toList());
try {
final Map<Urn, EntityResponse> entities =
_entityClient.batchGetV2(
context.getOperationContext(),
APPLICATION_ENTITY_NAME,
new HashSet<>(applicationUrns),
ASPECTS_TO_FETCH);
final List<EntityResponse> gmsResults = new ArrayList<>(urns.size());
for (Urn urn : applicationUrns) {
gmsResults.add(entities.getOrDefault(urn, null));
}
return gmsResults.stream()
.map(
gmsResult ->
gmsResult == null
? null
: DataFetcherResult.<Application>newResult()
.data(ApplicationMapper.map(context, gmsResult))
.build())
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException("Failed to batch load Applications", e);
}
}
@Override
public AutoCompleteResults autoComplete(
@Nonnull String query,
@Nullable String field,
@Nullable Filter filters,
int limit,
@Nonnull final QueryContext context)
throws Exception {
final AutoCompleteResult result =
_entityClient.autoComplete(
context.getOperationContext(), APPLICATION_ENTITY_NAME, query, filters, limit);
return AutoCompleteResultsMapper.map(context, result);
}
@Override
public SearchResults search(
@Nonnull String query,
@Nullable List<FacetFilterInput> filters,
int start,
int count,
@Nonnull final QueryContext context)
throws Exception {
throw new NotImplementedException(
"Searchable type (deprecated) not implemented on Application entity type");
}
}

View File

@ -0,0 +1,133 @@
package com.linkedin.datahub.graphql.types.application.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.APPLICATION_PROPERTIES_ASPECT_NAME;
import static com.linkedin.metadata.Constants.DOMAINS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.FORMS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.GLOBAL_TAGS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.GLOSSARY_TERMS_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.STRUCTURED_PROPERTIES_ASPECT_NAME;
import com.linkedin.application.ApplicationProperties;
import com.linkedin.common.Forms;
import com.linkedin.common.GlobalTags;
import com.linkedin.common.GlossaryTerms;
import com.linkedin.common.InstitutionalMemory;
import com.linkedin.common.Ownership;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.data.DataMap;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.generated.Application;
import com.linkedin.datahub.graphql.generated.EntityType;
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.domain.DomainAssociationMapper;
import com.linkedin.datahub.graphql.types.form.FormsMapper;
import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper;
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertiesMapper;
import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper;
import com.linkedin.domain.Domains;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.structured.StructuredProperties;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class ApplicationMapper implements ModelMapper<EntityResponse, Application> {
public static final ApplicationMapper INSTANCE = new ApplicationMapper();
public static Application map(
@Nullable final QueryContext context, @Nonnull final EntityResponse entityResponse) {
return INSTANCE.apply(context, entityResponse);
}
@Override
public Application apply(
@Nullable final QueryContext context, @Nonnull final EntityResponse entityResponse) {
final Application result = new Application();
Urn entityUrn = entityResponse.getUrn();
result.setUrn(entityResponse.getUrn().toString());
result.setType(EntityType.APPLICATION);
EnvelopedAspectMap aspectMap = entityResponse.getAspects();
MappingHelper<Application> mappingHelper = new MappingHelper<>(aspectMap, result);
mappingHelper.mapToResult(
APPLICATION_PROPERTIES_ASPECT_NAME,
(application, dataMap) -> mapApplicationProperties(application, dataMap, entityUrn));
mappingHelper.mapToResult(
GLOBAL_TAGS_ASPECT_NAME,
(application, dataMap) ->
application.setTags(GlobalTagsMapper.map(context, new GlobalTags(dataMap), entityUrn)));
mappingHelper.mapToResult(
GLOSSARY_TERMS_ASPECT_NAME,
(application, dataMap) ->
application.setGlossaryTerms(
GlossaryTermsMapper.map(context, new GlossaryTerms(dataMap), entityUrn)));
mappingHelper.mapToResult(
DOMAINS_ASPECT_NAME,
(application, dataMap) ->
application.setDomain(
DomainAssociationMapper.map(context, new Domains(dataMap), application.getUrn())));
mappingHelper.mapToResult(
OWNERSHIP_ASPECT_NAME,
(application, dataMap) ->
application.setOwnership(
OwnershipMapper.map(context, new Ownership(dataMap), entityUrn)));
mappingHelper.mapToResult(
INSTITUTIONAL_MEMORY_ASPECT_NAME,
(application, dataMap) ->
application.setInstitutionalMemory(
InstitutionalMemoryMapper.map(
context, new InstitutionalMemory(dataMap), entityUrn)));
mappingHelper.mapToResult(
STRUCTURED_PROPERTIES_ASPECT_NAME,
((entity, dataMap) ->
entity.setStructuredProperties(
StructuredPropertiesMapper.map(
context, new StructuredProperties(dataMap), entityUrn))));
mappingHelper.mapToResult(
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(result, Application.class);
} else {
return result;
}
}
private void mapApplicationProperties(
@Nonnull Application application, @Nonnull DataMap dataMap, @Nonnull Urn urn) {
ApplicationProperties applicationProperties = new ApplicationProperties(dataMap);
com.linkedin.datahub.graphql.generated.ApplicationProperties properties =
new com.linkedin.datahub.graphql.generated.ApplicationProperties();
final String name =
applicationProperties.hasName() ? applicationProperties.getName() : urn.getId();
properties.setName(name);
properties.setDescription(applicationProperties.getDescription());
if (applicationProperties.hasExternalUrl()) {
properties.setExternalUrl(applicationProperties.getExternalUrl().toString());
}
if (applicationProperties.hasAssets()) {
properties.setNumAssets(applicationProperties.getAssets().size());
} else {
properties.setNumAssets(0);
}
properties.setCustomProperties(
CustomPropertiesMapper.map(
applicationProperties.getCustomProperties(), UrnUtils.getUrn(application.getUrn())));
application.setProperties(properties);
}
}

View File

@ -4,6 +4,7 @@ import static com.linkedin.metadata.Constants.*;
import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.Application;
import com.linkedin.datahub.graphql.generated.Assertion;
import com.linkedin.datahub.graphql.generated.BusinessAttribute;
import com.linkedin.datahub.graphql.generated.Chart;
@ -249,6 +250,11 @@ public class UrnToEntityMapper implements ModelMapper<com.linkedin.common.urn.Ur
((VersionSet) partialEntity).setUrn(input.toString());
((VersionSet) partialEntity).setType(EntityType.VERSION_SET);
}
if (input.getEntityType().equals(APPLICATION_ENTITY_NAME)) {
partialEntity = new Application();
((Application) partialEntity).setUrn(input.toString());
((Application) partialEntity).setType(EntityType.APPLICATION);
}
return partialEntity;
}
}

View File

@ -99,7 +99,7 @@ public class DataProductType
.build())
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException("Failed to batch load Queries", e);
throw new RuntimeException("Failed to batch load Data Products", e);
}
}

View File

@ -59,6 +59,7 @@ public class EntityTypeMapper {
.put(EntityType.RESTRICTED, Constants.RESTRICTED_ENTITY_NAME)
.put(EntityType.BUSINESS_ATTRIBUTE, Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME)
.put(EntityType.DATA_CONTRACT, Constants.DATA_CONTRACT_ENTITY_NAME)
.put(EntityType.APPLICATION, Constants.APPLICATION_ENTITY_NAME)
.build();
private static final Map<String, EntityType> ENTITY_NAME_TO_TYPE =

View File

@ -78,6 +78,7 @@ public class EntityTypeUrnMapper {
.put(
Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME,
"urn:li:entityType:datahub.businessAttribute")
.put(Constants.APPLICATION_ENTITY_NAME, "urn:li:entityType:datahub.application")
.build();
private static final Map<String, String> ENTITY_TYPE_URN_TO_NAME =

View File

@ -275,6 +275,11 @@ type Query {
Fetch a Data Process Instance by primary key (urn)
"""
dataProcessInstance(urn: String!): DataProcessInstance
"""
Fetch an Application by primary key (urn)
"""
application(urn: String!): Application
}
"""
@ -885,6 +890,37 @@ type Mutation {
input: BatchSetDataProductInput!
): Boolean
"""
Create a new Application
"""
createApplication(
"Inputs required to create a new Application."
input: CreateApplicationInput!
): Application
"""
Update a Application
"""
updateApplication(
"The urn identifier for the Application to update."
urn: String!
"Inputs required to create a new Application."
input: UpdateApplicationInput!
): Application
"""
Delete a Application by urn.
"""
deleteApplication("Urn of the application to remove." urn: String!): Boolean
"""
Batch set or unset a Application to a list of entities
"""
batchSetApplication(
"Input for batch setting application"
input: BatchSetApplicationInput!
): Boolean
"""
Create a Custom Ownership Type. This requires the 'Manage Ownership Types' Metadata Privilege.
"""
@ -1256,6 +1292,11 @@ enum EntityType {
A set of versioned entities, representing a single source / logical entity over time
"""
VERSION_SET
"""
An application
"""
APPLICATION
}
"""
@ -12745,6 +12786,204 @@ input BatchSetDataProductInput {
resourceUrns: [String!]!
}
"""
An Application, or a grouping of Entities for a single business purpose. Compared with Data Products, Applications represent a grouping of tables that exist to serve a specific
purpose. However, unlike Data Products, they don't represent groups that are tailored to be consumed for any particular purpose. Often, the assets in Applications power specific
outcomes, for example a Pricing Application.
"""
type Application implements Entity {
"""
The primary key of the Application
"""
urn: String!
"""
A standard Entity Type
"""
type: EntityType!
"""
Properties about an Application
"""
properties: ApplicationProperties
"""
Ownership metadata of the Application
"""
ownership: Ownership
"""
References to internal resources related to the Application
"""
institutionalMemory: InstitutionalMemory
"""
Edges extending from this entity
"""
relationships(input: RelationshipsInput!): EntityRelationshipsResult
"""
Children entities inside of the Application
"""
entities(input: SearchAcrossEntitiesInput): SearchResults
"""
The structured glossary terms associated with the Application
"""
glossaryTerms: GlossaryTerms
"""
The Domain associated with the Application
"""
domain: DomainAssociation
"""
Tags used for searching Application
"""
tags: GlobalTags
"""
Experimental API.
For fetching extra entities that do not have custom UI code yet
"""
aspects(input: AspectParams): [RawAspect!]
"""
Structured properties about this asset
"""
structuredProperties: StructuredProperties
"""
The forms associated with the Application
"""
forms: Forms
"""
Privileges given to a user relevant to this entity
"""
privileges: EntityPrivileges
}
"""
Properties about an Application
"""
type ApplicationProperties {
"""
Display name of the Application
"""
name: String!
"""
Description of the Application
"""
description: String
"""
External URL for the Appliation (most likely GitHub repo where Application may be managed as code)
"""
externalUrl: String
"""
Number of children entities inside of the Application. This number includes soft deleted entities.
"""
numAssets: Int
"""
Custom properties of the Application
"""
customProperties: [CustomPropertiesEntry!]
}
"""
Input required to fetch the entities inside of a Application.
"""
input ApplicationEntitiesInput {
"""
Optional query filter for particular entities inside the Application
"""
query: String
"""
The offset of the result set
"""
start: Int
"""
The number of entities to include in result set
"""
count: Int
"""
Optional Facet filters to apply to the result set
"""
filters: [FacetFilterInput!]
}
"""
Input required for creating a Application.
"""
input CreateApplicationInput {
"""
Properties about the Application
"""
properties: CreateApplicationPropertiesInput!
"""
The primary key of the Domain
"""
domainUrn: String!
"""
An optional id for the new application
"""
id: String
}
"""
Input properties required for creating a Application
"""
input CreateApplicationPropertiesInput {
"""
A display name for the Application
"""
name: String!
"""
An optional description for the Application
"""
description: String
}
"""
Input properties required for update a Application
"""
input UpdateApplicationInput {
"""
A display name for the Application
"""
name: String
"""
An optional description for the Application
"""
description: String
}
"""
Input properties required for batch setting a Application on other entities
"""
input BatchSetApplicationInput {
"""
The urn of the application you are setting on a group of resources.
If this is null, the Application will be unset for the given resources.
"""
applicationUrn: String
"""
The urns of the entities the given application should be set on
"""
resourceUrns: [String!]!
}
"""
Properties about an individual Custom Ownership Type.
"""

View File

@ -66,6 +66,14 @@ extend type Query {
input: SearchAcrossEntitiesInput!
): SearchResults
"""
List Application assets for a given urn
"""
listApplicationAssets(
urn: String!
input: SearchAcrossEntitiesInput!
): SearchResults
"""
Browse for different entities by getting organizational groups and their
aggregated counts + content. Uses browsePathsV2 aspect and replaces our old

View File

@ -98,6 +98,7 @@ public class Constants {
public static final String DATAHUB_VIEW_ENTITY_NAME = "dataHubView";
public static final String QUERY_ENTITY_NAME = "query";
public static final String DATA_PRODUCT_ENTITY_NAME = "dataProduct";
public static final String APPLICATION_ENTITY_NAME = "application";
public static final String OWNERSHIP_TYPE_ENTITY_NAME = "ownershipType";
public static final Urn DEFAULT_OWNERSHIP_TYPE_URN =
UrnUtils.getUrn("urn:li:ownershipType:__system__none");
@ -370,6 +371,9 @@ public class Constants {
public static final String DATA_PRODUCT_PROPERTIES_ASPECT_NAME = "dataProductProperties";
public static final String DATA_PRODUCTS_ASPECT_NAME = "dataProducts";
// Application
public static final String APPLICATION_PROPERTIES_ASPECT_NAME = "applicationProperties";
// Ownership Types
public static final String OWNERSHIP_TYPE_KEY_ASPECT_NAME = "ownershipTypeKey";
public static final String OWNERSHIP_TYPE_INFO_ASPECT_NAME = "ownershipTypeInfo";

View File

@ -0,0 +1,8 @@
namespace com.linkedin.application
import com.linkedin.common.Edge
/**
* Represents an association of assets to a Application.
**/
record ApplicationAssociation includes Edge {}

View File

@ -0,0 +1,14 @@
namespace com.linkedin.application
/**
* Key for a Query
*/
@Aspect = {
"name": "applicationKey"
}
record ApplicationKey {
/**
* A unique id for the Application.
*/
id: string
}

View File

@ -0,0 +1,42 @@
namespace com.linkedin.application
import com.linkedin.common.CustomProperties
import com.linkedin.common.ExternalReference
/**
* The main properties of an Application
*/
@Aspect = {
"name": "applicationProperties"
}
record ApplicationProperties includes CustomProperties, ExternalReference {
/**
* Display name of the Application
*/
@Searchable = {
"fieldType": "WORD_GRAM",
"enableAutocomplete": true,
"boostScore": 10.0,
"fieldNameAliases": [ "_entityName" ]
}
name: optional string
/**
* Documentation of the application
*/
@Searchable = {
"fieldType": "TEXT",
"hasValuesFieldName": "hasDescription"
}
description: optional string
/**
* A list of assets that are part of this Application
*/
@Relationship = {
"/*/destinationUrn": {
"name": "ApplicationContains",
"entityTypes": [ "dataset", "dataJob", "dataFlow", "chart", "dashboard", "notebook", "container", "mlModel", "mlModelGroup", "mlFeatureTable", "mlFeature", "mlPrimaryKey" ],
}
}
assets: optional array[ApplicationAssociation]
}

View File

@ -21,7 +21,7 @@ record DataProductProperties includes CustomProperties, ExternalReference {
name: optional string
/**
* Documentation of the dataset
* Documentation of the data product
*/
@Searchable = {
"fieldType": "TEXT",

View File

@ -586,6 +586,21 @@ entities:
- forms
- testResults
- subTypes
- name: application
category: core
keyAspect: applicationKey
aspects:
- applicationProperties
- ownership
- glossaryTerms
- globalTags
- domains
- institutionalMemory
- status
- structuredProperties
- forms
- testResults
- subTypes
- name: ownershipType
doc: Ownership Type represents a user-created ownership category for a person or group who is responsible for an asset.
category: core

View File

@ -0,0 +1,27 @@
package com.linkedin.gms.factory.application;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.graph.GraphClient;
import com.linkedin.metadata.service.ApplicationService;
import javax.annotation.Nonnull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class ApplicationServiceFactory {
@Autowired
@Qualifier("graphClient")
private GraphClient _graphClient;
@Bean(name = "applicationService")
@Scope("singleton")
@Nonnull
protected ApplicationService getInstance(
@Qualifier("entityClient") final EntityClient entityClient) throws Exception {
return new ApplicationService(entityClient, _graphClient);
}
}

View File

@ -33,6 +33,7 @@ import com.linkedin.metadata.graph.GraphService;
import com.linkedin.metadata.graph.SiblingGraphService;
import com.linkedin.metadata.models.registry.EntityRegistry;
import com.linkedin.metadata.recommendation.RecommendationsService;
import com.linkedin.metadata.service.ApplicationService;
import com.linkedin.metadata.service.AssertionService;
import com.linkedin.metadata.service.BusinessAttributeService;
import com.linkedin.metadata.service.DataProductService;
@ -179,6 +180,10 @@ public class GraphQLEngineFactory {
@Qualifier("dataProductService")
private DataProductService dataProductService;
@Autowired
@Qualifier("applicationService")
private ApplicationService applicationService;
@Autowired
@Qualifier("formService")
private FormService formService;
@ -253,6 +258,7 @@ public class GraphQLEngineFactory {
args.setFormService(formService);
args.setRestrictedService(restrictedService);
args.setDataProductService(dataProductService);
args.setApplicationService(applicationService);
args.setGraphQLQueryComplexityLimit(
configProvider.getGraphQL().getQuery().getComplexityLimit());
args.setGraphQLQueryIntrospectionEnabled(

View File

@ -0,0 +1,520 @@
package com.linkedin.metadata.service;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.linkedin.application.ApplicationAssociation;
import com.linkedin.application.ApplicationAssociationArray;
import com.linkedin.application.ApplicationKey;
import com.linkedin.application.ApplicationProperties;
import com.linkedin.common.EntityRelationships;
import com.linkedin.common.UrnArray;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.data.DataMap;
import com.linkedin.data.template.SetMode;
import com.linkedin.domain.Domains;
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.graph.GraphClient;
import com.linkedin.metadata.query.filter.RelationshipDirection;
import com.linkedin.metadata.utils.EntityKeyUtils;
import com.linkedin.r2.RemoteInvocationException;
import io.datahubproject.metadata.context.OperationContext;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
/**
* This class is used to permit easy CRUD operations on an Application
*
* <p>Note that no Authorization is performed within the service. The expectation is that the caller
* has already verified the permissions of the active Actor.
*/
@Slf4j
public class ApplicationService {
private final EntityClient _entityClient;
private final GraphClient
_graphClient; // Keep for now, though direct relationship manipulation might change.
public ApplicationService(@Nonnull EntityClient entityClient, @Nonnull GraphClient graphClient) {
_entityClient = entityClient;
_graphClient = graphClient;
}
/**
* Creates a new Application.
*
* <p>Note that this method does not do authorization validation. It is assumed that users of this
* class have already authorized the operation.
*
* @param name optional name of the Application
* @param description optional description of the Application
* @return the urn of the newly created Application
*/
public Urn createApplication(
@Nonnull OperationContext opContext,
@Nullable String id,
@Nullable String name,
@Nullable String description) {
final ApplicationKey key = new ApplicationKey();
if (id != null && !id.isBlank()) {
key.setId(id);
} else {
key.setId(UUID.randomUUID().toString());
}
try {
if (_entityClient.exists(
opContext,
EntityKeyUtils.convertEntityKeyToUrn(key, Constants.APPLICATION_ENTITY_NAME))) {
throw new IllegalArgumentException("This Application already exists!");
}
} catch (RemoteInvocationException e) {
throw new RuntimeException("Unable to check for existence of Application!", e);
}
final ApplicationProperties properties = new ApplicationProperties();
properties.setName(name, SetMode.IGNORE_NULL);
properties.setDescription(description, SetMode.IGNORE_NULL);
// Initialize assets array as per PDL: assets: optional array[ApplicationAssociation]
properties.setAssets(new ApplicationAssociationArray());
try {
final Urn entityUrn =
EntityKeyUtils.convertEntityKeyToUrn(key, Constants.APPLICATION_ENTITY_NAME);
return UrnUtils.getUrn(
_entityClient.ingestProposal(
opContext,
AspectUtils.buildMetadataChangeProposal(
entityUrn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
false));
} catch (Exception e) {
throw new RuntimeException("Failed to create Application", e);
}
}
/**
* Updates an existing Application. If a provided field is null, the previous value will be kept.
*
* <p>Note that this method does not do authorization validation. It is assumed that users of this
* class have already authorized the operation.
*
* @param urn the urn of the Application
* @param name optional name of the Application
* @param description optional description of the Application
*/
public Urn updateApplication(
@Nonnull OperationContext opContext,
@Nonnull Urn urn,
@Nullable String name,
@Nullable String description) {
Objects.requireNonNull(urn, "urn must not be null");
Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
ApplicationProperties properties = getApplicationProperties(opContext, urn);
if (properties == null) {
throw new IllegalArgumentException(
String.format(
"Failed to update Application. Application with urn %s does not exist.", urn));
}
boolean changed = false;
if (name != null && !name.equals(properties.getName())) { // check if actually different
properties.setName(name);
changed = true;
}
if (description != null
&& !description.equals(properties.getDescription())) { // check if actually different
properties.setDescription(description);
changed = true;
}
if (!changed) {
return urn;
}
try {
return UrnUtils.getUrn(
_entityClient.ingestProposal(
opContext,
AspectUtils.buildMetadataChangeProposal(
urn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
false));
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to update Application with urn %s", urn), e);
}
}
/**
* @param applicationUrn the urn of the Application
* @return an instance of {@link ApplicationProperties} for the Application, null if it does not
* exist.
*/
@Nullable
public ApplicationProperties getApplicationProperties(
@Nonnull OperationContext opContext, @Nonnull final Urn applicationUrn) {
Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
final EntityResponse response;
try {
response =
_entityClient.getV2(
opContext,
Constants.APPLICATION_ENTITY_NAME,
applicationUrn,
ImmutableSet.of(Constants.APPLICATION_PROPERTIES_ASPECT_NAME));
} catch (Exception e) {
log.error(
String.format("Failed to retrieve ApplicationProperties for urn %s", applicationUrn), e);
return null;
}
if (response != null
&& response.getAspects().containsKey(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)) {
return new ApplicationProperties(
response
.getAspects()
.get(Constants.APPLICATION_PROPERTIES_ASPECT_NAME)
.getValue()
.data());
}
return null;
}
/**
* @param applicationUrn the urn of the Application
* @return an instance of {@link ApplicationProperties} for the Application, null if it does not
* exist.
*/
@Nullable
public Domains getApplicationDomains(
@Nonnull OperationContext opContext, @Nonnull final Urn applicationUrn) {
Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
try {
final EntityResponse response =
_entityClient.getV2(
opContext,
Constants.APPLICATION_ENTITY_NAME,
applicationUrn,
ImmutableSet.of(Constants.DOMAINS_ASPECT_NAME));
if (response != null && response.getAspects().containsKey(Constants.DOMAINS_ASPECT_NAME)) {
return new Domains(
response.getAspects().get(Constants.DOMAINS_ASPECT_NAME).getValue().data());
}
} catch (Exception e) {
log.error(String.format("Failed to get domains for application %s", applicationUrn), e);
}
return null;
}
@Nullable
public EntityResponse getApplicationEntityResponse(
@Nonnull OperationContext opContext, @Nonnull final Urn applicationUrn) {
Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
try {
return _entityClient.getV2(
opContext,
Constants.APPLICATION_ENTITY_NAME,
applicationUrn,
ImmutableSet.of(Constants.APPLICATION_PROPERTIES_ASPECT_NAME));
} catch (Exception e) {
log.error(String.format("Failed to fetch Application with urn %s", applicationUrn), e);
}
return null;
}
public void setDomain(
@Nonnull OperationContext opContext,
@Nonnull final Urn applicationUrn,
@Nonnull final Urn domainUrn) {
Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
Objects.requireNonNull(domainUrn, "domainUrn must not be null");
Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
Domains domains = getApplicationDomains(opContext, applicationUrn);
if (domains == null) {
domains = new Domains(new DataMap());
domains.setDomains(new UrnArray());
}
UrnArray domainList = domains.getDomains();
if (domainList.stream().anyMatch(urn -> urn.equals(domainUrn))) {
log.info(
String.format(
"Domain %s already exists for Application %s. Skipping add.",
domainUrn, applicationUrn));
return;
}
domains.getDomains().add(domainUrn);
try {
_entityClient.ingestProposal(
opContext,
AspectUtils.buildMetadataChangeProposal(
applicationUrn, Constants.DOMAINS_ASPECT_NAME, domains),
false);
} catch (Exception e) {
throw new RuntimeException(
String.format(
"Failed to set domain for Application with urn %s domain %s",
applicationUrn, domainUrn),
e);
}
}
public void deleteApplication(@Nonnull OperationContext opContext, @Nonnull Urn applicationUrn) {
Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
// 1. Check properties-based assets
ApplicationProperties properties = getApplicationProperties(opContext, applicationUrn);
if (properties != null && properties.hasAssets() && !properties.getAssets().isEmpty()) {
throw new RuntimeException(
String.format(
"Cannot delete application %s, it still contains assets (found in properties). Please remove assets before deleting.",
applicationUrn));
}
// 2. Check graph-based relationships for assets
// Based on PDL: @Relationship = { "/*/destinationUrn": { "name": "ApplicationContains", ... } }
// This means an outgoing relationship from Application to Asset.
try {
EntityRelationships graphRelationships =
_graphClient.getRelatedEntities(
applicationUrn.toString(),
ImmutableSet.of(
"ApplicationContains"), // Relationship type - corrected to ImmutableSet
RelationshipDirection.OUTGOING,
0, // start
1, // count (we only need to know if at least one exists)
opContext.getAuthentication().getActor().toUrnStr());
if (graphRelationships != null
&& graphRelationships.getRelationships() != null
&& !graphRelationships.getRelationships().isEmpty()) {
throw new RuntimeException(
String.format(
"Cannot delete application %s, it still contains assets (found via graph relationships). Please remove assets before deleting.",
applicationUrn));
}
} catch (Exception e) {
// Log the exception, as failure to query graph shouldn't necessarily block deletion if
// properties are clear,
// but it's an indication of potential inconsistency or error.
// Depending on strictness, this could also be a hard failure.
log.error(
String.format(
"Error checking graph relationships for application %s during delete: %s",
applicationUrn, e.getMessage()),
e);
// For now, let's make it a hard failure to ensure consistency, similar to
// DataProductService's implicit check.
throw new RuntimeException(
String.format(
"Failed to verify graph relationships for application %s during delete.",
applicationUrn),
e);
}
// 3. Proceed with deletion if both checks pass
try {
_entityClient.deleteEntity(opContext, applicationUrn);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to delete Application with urn %s", applicationUrn), e);
}
}
public void batchSetApplicationAssets(
@Nonnull OperationContext opContext,
@Nonnull Urn applicationUrn,
@Nonnull List<Urn> resourceUrns,
@Nonnull Urn actorUrn) { // actorUrn for audit stamp if not in opContext
Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
Objects.requireNonNull(resourceUrns, "resourceUrns must not be null");
ApplicationProperties properties = getApplicationProperties(opContext, applicationUrn);
if (properties == null) {
log.error(
"Application properties not found for {}, cannot add assets. Create the application first.",
applicationUrn);
throw new IllegalArgumentException(
"Application properties not found for "
+ applicationUrn
+ ". Create the application first.");
}
ApplicationAssociationArray currentAssets =
properties.hasAssets() ? properties.getAssets() : new ApplicationAssociationArray();
boolean changed = false;
for (Urn resourceUrn : resourceUrns) {
// TODO: Revisit ApplicationAssociation structure once PDL is finalized and classes generated.
// Assuming ApplicationAssociation PDL will define how a resource URN is stored.
// For now, we'll create a new association and add it. The exact mechanism
// for checking existence and setting the resource URN might change based on PDL.
boolean exists = false;
for (ApplicationAssociation existing : currentAssets) {
// TODO: Replace with actual check once ApplicationAssociation.getResource() is
// confirmed/available
// if (existing.getResource().equals(resourceUrn)) {
// exists = true;
// break;
// }
}
if (!exists) {
ApplicationAssociation newAssociation = new ApplicationAssociation();
// TODO: newAssociation.setResource(resourceUrn); // This line caused errors, dependent on
// PDL
currentAssets.add(newAssociation);
changed = true;
}
}
if (changed) {
properties.setAssets(currentAssets);
// AuditStamp handling can be added here if ApplicationProperties has an audit field
// Or if it's handled by the MCP interceptor automatically
// final AuditStamp audit = new
// AuditStamp().setActor(actorUrn).setTime(System.currentTimeMillis());
// properties.setLastModified(audit); // Example if such a field exists
try {
_entityClient.ingestProposal(
opContext,
AspectUtils.buildMetadataChangeProposal(
applicationUrn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
false);
} catch (Exception e) {
throw new RuntimeException(
String.format(
"Failed to batch set application assets for %s with resources %s!",
applicationUrn, resourceUrns),
e);
}
}
}
public void batchRemoveApplicationAssets(
@Nonnull OperationContext opContext,
@Nonnull Urn applicationUrn,
@Nonnull List<Urn> resourceUrnsToRemove,
@Nonnull Urn actorUrn) { // actorUrn for audit stamp
Objects.requireNonNull(applicationUrn, "applicationUrn must not be null");
Objects.requireNonNull(resourceUrnsToRemove, "resourceUrnsToRemove must not be null");
ApplicationProperties properties = getApplicationProperties(opContext, applicationUrn);
if (properties == null || !properties.hasAssets() || properties.getAssets().isEmpty()) {
log.info(
"Application {} has no assets or properties not found, nothing to remove.",
applicationUrn);
return; // No assets to remove
}
ApplicationAssociationArray currentAssets = properties.getAssets();
List<ApplicationAssociation> associationsToRemove = new ArrayList<>();
for (ApplicationAssociation asset : currentAssets) {
// TODO: Revisit ApplicationAssociation structure once PDL is finalized and classes generated.
// Replace with actual check once ApplicationAssociation.getResource() is confirmed/available
// if (resourceUrnsToRemove.contains(asset.getResource())) {
// associationsToRemove.add(asset);
// }
}
if (!associationsToRemove.isEmpty()) {
for (ApplicationAssociation toRemove : associationsToRemove) {
currentAssets.remove(
toRemove); // Relies on proper .equals() in ApplicationAssociation or object identity
}
properties.setAssets(currentAssets);
// AuditStamp handling (similar to batchSetApplicationAssets)
try {
_entityClient.ingestProposal(
opContext,
AspectUtils.buildMetadataChangeProposal(
applicationUrn, Constants.APPLICATION_PROPERTIES_ASPECT_NAME, properties),
false);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to batch remove assets from application %s", applicationUrn), e);
}
}
}
public void unsetApplication(
@Nonnull OperationContext opContext, @Nonnull Urn resourceUrn, @Nonnull Urn actorUrn) {
Objects.requireNonNull(resourceUrn, "resourceUrn must not be null");
Objects.requireNonNull(actorUrn, "actorUrn must not be null");
Objects.requireNonNull(opContext.getSessionAuthentication(), "authentication must not be null");
// Find the parent Application URN from the resourceUrn by querying the graph
// We are looking for an INCOMING "ApplicationContains" relationship to the resourceUrn.
// The source of this relationship will be the Application.
Urn applicationUrn = null;
try {
final EntityRelationships relationships =
_graphClient.getRelatedEntities(
resourceUrn.toString(),
ImmutableSet.of("ApplicationContains"), // Relationship type
RelationshipDirection
.INCOMING, // Direction: From Application TO Resource, so INCOMING for Resource
0, // start
1, // count (expecting only one Application to contain a given resource)
opContext.getAuthentication().getActor().toUrnStr());
if (relationships != null
&& relationships.getRelationships() != null
&& !relationships.getRelationships().isEmpty()) {
if (relationships.getRelationships().size() > 1) {
log.warn(
"Resource {} is part of multiple applications via 'ApplicationContains' relationship. Proceeding with the first one found: {}. This might indicate a data modeling issue.",
resourceUrn,
relationships.getRelationships().get(0).getEntity());
}
applicationUrn = relationships.getRelationships().get(0).getEntity();
} else {
log.info(
"Resource {} is not part of any application via 'ApplicationContains' relationship. Nothing to unset.",
resourceUrn);
return; // Nothing to do if the resource is not part of any application
}
} catch (Exception e) {
log.error(
String.format(
"Failed to find parent application for resource %s. Error: %s",
resourceUrn, e.getMessage()),
e);
throw new RuntimeException(
String.format("Failed to find parent application for resource %s", resourceUrn), e);
}
if (applicationUrn != null) {
// Now that we have the application URN, call batchRemoveApplicationAssets
batchRemoveApplicationAssets(
opContext, applicationUrn, ImmutableList.of(resourceUrn), actorUrn);
log.info(
"Successfully unset application for resource {} from application {}",
resourceUrn,
applicationUrn);
} else {
// This case should ideally be caught by the graph query result, but as a safeguard:
log.warn(
"Could not determine application to unset for resource {}. No action taken.",
resourceUrn);
}
}
public boolean verifyEntityExists(@Nonnull OperationContext opContext, @Nonnull Urn entityUrn) {
try {
return _entityClient.exists(opContext, entityUrn);
} catch (RemoteInvocationException e) {
throw new RuntimeException("Failed to check entity existence: " + e.getMessage(), e);
}
}
}

View File

@ -722,6 +722,26 @@ public class PoliciesConfig {
CREATE_ENTITY_PRIVILEGE,
EXISTS_ENTITY_PRIVILEGE));
// Application Privileges
public static final ResourcePrivileges APPLICATION_PRIVILEGES =
ResourcePrivileges.of(
"application",
"Applications",
"Applications created on DataHub",
ImmutableList.of(
VIEW_ENTITY_PAGE_PRIVILEGE,
EDIT_ENTITY_OWNERS_PRIVILEGE,
EDIT_ENTITY_DOCS_PRIVILEGE,
EDIT_ENTITY_DOC_LINKS_PRIVILEGE,
EDIT_ENTITY_PRIVILEGE,
DELETE_ENTITY_PRIVILEGE,
EDIT_ENTITY_TAGS_PRIVILEGE,
EDIT_ENTITY_GLOSSARY_TERMS_PRIVILEGE,
EDIT_ENTITY_DOMAINS_PRIVILEGE,
EDIT_ENTITY_PROPERTIES_PRIVILEGE,
CREATE_ENTITY_PRIVILEGE,
EXISTS_ENTITY_PRIVILEGE));
// Glossary Term Privileges
public static final ResourcePrivileges GLOSSARY_TERM_PRIVILEGES =
ResourcePrivileges.of(
@ -863,7 +883,8 @@ public class PoliciesConfig {
BUSINESS_ATTRIBUTE_PRIVILEGES,
STRUCTURED_PROPERTIES_PRIVILEGES,
VERSION_SET_PRIVILEGES,
PLATFORM_INSTANCE_PRIVILEGES);
PLATFORM_INSTANCE_PRIVILEGES,
APPLICATION_PRIVILEGES);
// Merge all entity specific resource privileges to create a superset of all resource privileges
public static final ResourcePrivileges ALL_RESOURCE_PRIVILEGES =