mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-13 09:52:46 +00:00
graphql & backend + models
This commit is contained in:
parent
de75524ca5
commit
6a5b619e1d
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
package com.linkedin.datahub.graphql;
|
||||
|
||||
public class ListApplicationAssetsResolver {}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 {}
|
||||
@ -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
|
||||
}
|
||||
@ -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]
|
||||
}
|
||||
@ -21,7 +21,7 @@ record DataProductProperties includes CustomProperties, ExternalReference {
|
||||
name: optional string
|
||||
|
||||
/**
|
||||
* Documentation of the dataset
|
||||
* Documentation of the data product
|
||||
*/
|
||||
@Searchable = {
|
||||
"fieldType": "TEXT",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user