-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(properties) Add upsertStructuredProperties graphql endpoint for assets #9906
Merged
chriscollins3456
merged 7 commits into
datahub-project:master
from
chriscollins3456:cc--prd-1056-be-upsert-structured-props
Mar 20, 2024
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
e080339
feat(properties) Add upsertStructuredProperties graphql endpoint for …
chriscollins3456 e7ea31a
Make structured properties editable in the UI, some refactoring to do
chriscollins3456 bb6bfcf
merge in master, resolve conflicts
chriscollins3456 264547d
only show edit if you have privileges, refactor structured prop input
chriscollins3456 2b6b427
fix CI
chriscollins3456 dbdb3da
remove placeholder description
chriscollins3456 3dfe524
Merge branch 'master' into cc--prd-1056-be-upsert-structured-props
chriscollins3456 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
...main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/StructuredPropertyUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package com.linkedin.datahub.graphql.resolvers.mutate.util; | ||
|
||
import com.datahub.authorization.ConjunctivePrivilegeGroup; | ||
import com.datahub.authorization.DisjunctivePrivilegeGroup; | ||
import com.google.common.collect.ImmutableList; | ||
import com.linkedin.common.urn.Urn; | ||
import com.linkedin.datahub.graphql.QueryContext; | ||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; | ||
import com.linkedin.datahub.graphql.generated.PropertyValueInput; | ||
import com.linkedin.metadata.authorization.PoliciesConfig; | ||
import com.linkedin.structured.PrimitivePropertyValue; | ||
import javax.annotation.Nonnull; | ||
import javax.annotation.Nullable; | ||
|
||
public class StructuredPropertyUtils { | ||
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP = | ||
new ConjunctivePrivilegeGroup( | ||
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType())); | ||
|
||
private StructuredPropertyUtils() {} | ||
|
||
@Nullable | ||
public static PrimitivePropertyValue mapPropertyValueInput( | ||
@Nonnull final PropertyValueInput valueInput) { | ||
if (valueInput.getStringValue() != null) { | ||
return PrimitivePropertyValue.create(valueInput.getStringValue()); | ||
} else if (valueInput.getNumberValue() != null) { | ||
return PrimitivePropertyValue.create(valueInput.getNumberValue().doubleValue()); | ||
} | ||
return null; | ||
} | ||
|
||
public static boolean isAuthorizedToUpdateProperties( | ||
@Nonnull QueryContext context, @Nonnull Urn targetUrn) { | ||
// If you either have all entity privileges, or have the specific privileges required, you are | ||
// authorized. | ||
final DisjunctivePrivilegeGroup orPrivilegeGroups = | ||
new DisjunctivePrivilegeGroup( | ||
ImmutableList.of( | ||
ALL_PRIVILEGES_GROUP, | ||
new ConjunctivePrivilegeGroup( | ||
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PROPERTIES_PRIVILEGE.getType())))); | ||
|
||
return AuthorizationUtils.isAuthorized( | ||
context.getAuthorizer(), | ||
context.getActorUrn(), | ||
targetUrn.getEntityType(), | ||
targetUrn.toString(), | ||
orPrivilegeGroups); | ||
} | ||
} |
171 changes: 171 additions & 0 deletions
171
...in/datahub/graphql/resolvers/structuredproperties/UpsertStructuredPropertiesResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package com.linkedin.datahub.graphql.resolvers.structuredproperties; | ||
|
||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; | ||
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTIES_ASPECT_NAME; | ||
|
||
import com.datahub.authentication.Authentication; | ||
import com.linkedin.common.AuditStamp; | ||
import com.linkedin.common.urn.Urn; | ||
import com.linkedin.common.urn.UrnUtils; | ||
import com.linkedin.datahub.graphql.QueryContext; | ||
import com.linkedin.datahub.graphql.exception.AuthorizationException; | ||
import com.linkedin.datahub.graphql.generated.PropertyValueInput; | ||
import com.linkedin.datahub.graphql.generated.UpsertStructuredPropertiesInput; | ||
import com.linkedin.datahub.graphql.resolvers.mutate.util.StructuredPropertyUtils; | ||
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertiesMapper; | ||
import com.linkedin.entity.EntityResponse; | ||
import com.linkedin.entity.client.EntityClient; | ||
import com.linkedin.metadata.entity.AspectUtils; | ||
import com.linkedin.metadata.utils.AuditStampUtils; | ||
import com.linkedin.mxe.MetadataChangeProposal; | ||
import com.linkedin.structured.PrimitivePropertyValueArray; | ||
import com.linkedin.structured.StructuredProperties; | ||
import com.linkedin.structured.StructuredPropertyValueAssignment; | ||
import com.linkedin.structured.StructuredPropertyValueAssignmentArray; | ||
import graphql.com.google.common.collect.ImmutableSet; | ||
import graphql.schema.DataFetcher; | ||
import graphql.schema.DataFetchingEnvironment; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.stream.Collectors; | ||
import javax.annotation.Nonnull; | ||
|
||
public class UpsertStructuredPropertiesResolver | ||
implements DataFetcher< | ||
CompletableFuture<com.linkedin.datahub.graphql.generated.StructuredProperties>> { | ||
|
||
private final EntityClient _entityClient; | ||
|
||
public UpsertStructuredPropertiesResolver(@Nonnull final EntityClient entityClient) { | ||
_entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null"); | ||
} | ||
|
||
@Override | ||
public CompletableFuture<com.linkedin.datahub.graphql.generated.StructuredProperties> get( | ||
final DataFetchingEnvironment environment) throws Exception { | ||
final QueryContext context = environment.getContext(); | ||
final Authentication authentication = context.getAuthentication(); | ||
|
||
final UpsertStructuredPropertiesInput input = | ||
bindArgument(environment.getArgument("input"), UpsertStructuredPropertiesInput.class); | ||
final Urn assetUrn = UrnUtils.getUrn(input.getAssetUrn()); | ||
Map<String, List<PropertyValueInput>> updateMap = new HashMap<>(); | ||
// create a map of updates from our input | ||
input | ||
.getStructuredPropertyInputParams() | ||
.forEach(param -> updateMap.put(param.getStructuredPropertyUrn(), param.getValues())); | ||
|
||
return CompletableFuture.supplyAsync( | ||
() -> { | ||
try { | ||
// check authorization first | ||
if (!StructuredPropertyUtils.isAuthorizedToUpdateProperties(context, assetUrn)) { | ||
throw new AuthorizationException( | ||
String.format( | ||
"Not authorized to update properties on the gives urn %s", assetUrn)); | ||
} | ||
|
||
final AuditStamp auditStamp = | ||
AuditStampUtils.createAuditStamp(authentication.getActor().toUrnStr()); | ||
|
||
if (!_entityClient.exists(assetUrn, authentication)) { | ||
throw new RuntimeException( | ||
String.format("Asset with provided urn %s does not exist", assetUrn)); | ||
} | ||
|
||
// get or default the structured properties aspect | ||
StructuredProperties structuredProperties = | ||
getStructuredProperties(assetUrn, authentication); | ||
|
||
// update the existing properties based on new value | ||
StructuredPropertyValueAssignmentArray properties = | ||
updateExistingProperties(structuredProperties, updateMap, auditStamp); | ||
|
||
// append any new properties from our input | ||
addNewProperties(properties, updateMap, auditStamp); | ||
|
||
structuredProperties.setProperties(properties); | ||
|
||
// ingest change proposal | ||
final MetadataChangeProposal structuredPropertiesProposal = | ||
AspectUtils.buildMetadataChangeProposal( | ||
assetUrn, STRUCTURED_PROPERTIES_ASPECT_NAME, structuredProperties); | ||
|
||
_entityClient.ingestProposal(structuredPropertiesProposal, authentication, false); | ||
|
||
return StructuredPropertiesMapper.map(structuredProperties); | ||
} catch (Exception e) { | ||
throw new RuntimeException( | ||
String.format("Failed to perform update against input %s", input), e); | ||
} | ||
}); | ||
} | ||
|
||
private StructuredProperties getStructuredProperties(Urn assetUrn, Authentication authentication) | ||
throws Exception { | ||
EntityResponse response = | ||
_entityClient.getV2( | ||
assetUrn.getEntityType(), | ||
assetUrn, | ||
ImmutableSet.of(STRUCTURED_PROPERTIES_ASPECT_NAME), | ||
authentication); | ||
StructuredProperties structuredProperties = new StructuredProperties(); | ||
structuredProperties.setProperties(new StructuredPropertyValueAssignmentArray()); | ||
if (response != null && response.getAspects().containsKey(STRUCTURED_PROPERTIES_ASPECT_NAME)) { | ||
structuredProperties = | ||
new StructuredProperties( | ||
response.getAspects().get(STRUCTURED_PROPERTIES_ASPECT_NAME).getValue().data()); | ||
} | ||
return structuredProperties; | ||
} | ||
|
||
private StructuredPropertyValueAssignmentArray updateExistingProperties( | ||
StructuredProperties structuredProperties, | ||
Map<String, List<PropertyValueInput>> updateMap, | ||
AuditStamp auditStamp) { | ||
return new StructuredPropertyValueAssignmentArray( | ||
structuredProperties.getProperties().stream() | ||
.map( | ||
propAssignment -> { | ||
String propUrnString = propAssignment.getPropertyUrn().toString(); | ||
if (updateMap.containsKey(propUrnString)) { | ||
List<PropertyValueInput> valueList = updateMap.get(propUrnString); | ||
PrimitivePropertyValueArray values = | ||
new PrimitivePropertyValueArray( | ||
valueList.stream() | ||
.map(StructuredPropertyUtils::mapPropertyValueInput) | ||
.collect(Collectors.toList())); | ||
propAssignment.setValues(values); | ||
propAssignment.setLastModified(auditStamp); | ||
} | ||
return propAssignment; | ||
}) | ||
.collect(Collectors.toList())); | ||
} | ||
|
||
private void addNewProperties( | ||
StructuredPropertyValueAssignmentArray properties, | ||
Map<String, List<PropertyValueInput>> updateMap, | ||
AuditStamp auditStamp) { | ||
// first remove existing properties from updateMap so that we append only new properties | ||
properties.forEach(prop -> updateMap.remove(prop.getPropertyUrn().toString())); | ||
|
||
updateMap.forEach( | ||
(structuredPropUrn, values) -> { | ||
StructuredPropertyValueAssignment valueAssignment = | ||
new StructuredPropertyValueAssignment(); | ||
valueAssignment.setPropertyUrn(UrnUtils.getUrn(structuredPropUrn)); | ||
valueAssignment.setValues( | ||
new PrimitivePropertyValueArray( | ||
values.stream() | ||
.map(StructuredPropertyUtils::mapPropertyValueInput) | ||
.collect(Collectors.toList()))); | ||
valueAssignment.setCreated(auditStamp); | ||
valueAssignment.setLastModified(auditStamp); | ||
properties.add(valueAssignment); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic in this resolver seems like duplicate work to support replacing a structured property. This might be better using the structured property patch builder for this work. Or the generic patch builder. The patch support for structured properties was built to add/remove/replace structured property values on entities. The read current value and mutate is inherent in the patch operation for many kinds of aspects.