Skip to content

Commit e080339

Browse files
feat(properties) Add upsertStructuredProperties graphql endpoint for assets
1 parent ef3a814 commit e080339

File tree

9 files changed

+518
-19
lines changed

9 files changed

+518
-19
lines changed

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java

+4
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@
256256
import com.linkedin.datahub.graphql.resolvers.settings.view.UpdateGlobalViewsSettingsResolver;
257257
import com.linkedin.datahub.graphql.resolvers.step.BatchGetStepStatesResolver;
258258
import com.linkedin.datahub.graphql.resolvers.step.BatchUpdateStepStatesResolver;
259+
import com.linkedin.datahub.graphql.resolvers.structuredproperties.UpsertStructuredPropertiesResolver;
259260
import com.linkedin.datahub.graphql.resolvers.tag.CreateTagResolver;
260261
import com.linkedin.datahub.graphql.resolvers.tag.DeleteTagResolver;
261262
import com.linkedin.datahub.graphql.resolvers.tag.SetTagColorResolver;
@@ -1216,6 +1217,9 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
12161217
.dataFetcher(
12171218
"verifyForm", new VerifyFormResolver(this.formService, this.groupService))
12181219
.dataFetcher("batchRemoveForm", new BatchRemoveFormResolver(this.formService))
1220+
.dataFetcher(
1221+
"upsertStructuredProperties",
1222+
new UpsertStructuredPropertiesResolver(this.entityClient))
12191223
.dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient))
12201224
.dataFetcher(
12211225
"updateIncidentStatus",

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/FormUtils.java

+1-9
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import com.linkedin.metadata.query.filter.Criterion;
1313
import com.linkedin.metadata.query.filter.CriterionArray;
1414
import com.linkedin.metadata.query.filter.Filter;
15-
import com.linkedin.structured.PrimitivePropertyValue;
1615
import com.linkedin.structured.PrimitivePropertyValueArray;
1716
import java.util.Objects;
1817
import javax.annotation.Nonnull;
@@ -37,14 +36,7 @@ public static PrimitivePropertyValueArray getStructuredPropertyValuesFromInput(
3736
input
3837
.getStructuredPropertyParams()
3938
.getValues()
40-
.forEach(
41-
value -> {
42-
if (value.getStringValue() != null) {
43-
values.add(PrimitivePropertyValue.create(value.getStringValue()));
44-
} else if (value.getNumberValue() != null) {
45-
values.add(PrimitivePropertyValue.create(value.getNumberValue().doubleValue()));
46-
}
47-
});
39+
.forEach(value -> values.add(StructuredPropertyUtils.mapPropertyValueInput(value)));
4840

4941
return values;
5042
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.linkedin.datahub.graphql.resolvers.mutate.util;
2+
3+
import com.datahub.authorization.ConjunctivePrivilegeGroup;
4+
import com.datahub.authorization.DisjunctivePrivilegeGroup;
5+
import com.google.common.collect.ImmutableList;
6+
import com.linkedin.common.urn.Urn;
7+
import com.linkedin.datahub.graphql.QueryContext;
8+
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
9+
import com.linkedin.datahub.graphql.generated.PropertyValueInput;
10+
import com.linkedin.metadata.authorization.PoliciesConfig;
11+
import com.linkedin.structured.PrimitivePropertyValue;
12+
import javax.annotation.Nonnull;
13+
import javax.annotation.Nullable;
14+
15+
public class StructuredPropertyUtils {
16+
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP =
17+
new ConjunctivePrivilegeGroup(
18+
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()));
19+
20+
private StructuredPropertyUtils() {}
21+
22+
@Nullable
23+
public static PrimitivePropertyValue mapPropertyValueInput(
24+
@Nonnull final PropertyValueInput valueInput) {
25+
if (valueInput.getStringValue() != null) {
26+
return PrimitivePropertyValue.create(valueInput.getStringValue());
27+
} else if (valueInput.getNumberValue() != null) {
28+
return PrimitivePropertyValue.create(valueInput.getNumberValue().doubleValue());
29+
}
30+
return null;
31+
}
32+
33+
public static boolean isAuthorizedToUpdateProperties(
34+
@Nonnull QueryContext context, @Nonnull Urn targetUrn) {
35+
// If you either have all entity privileges, or have the specific privileges required, you are
36+
// authorized.
37+
final DisjunctivePrivilegeGroup orPrivilegeGroups =
38+
new DisjunctivePrivilegeGroup(
39+
ImmutableList.of(
40+
ALL_PRIVILEGES_GROUP,
41+
new ConjunctivePrivilegeGroup(
42+
ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PROPERTIES_PRIVILEGE.getType()))));
43+
44+
return AuthorizationUtils.isAuthorized(
45+
context.getAuthorizer(),
46+
context.getActorUrn(),
47+
targetUrn.getEntityType(),
48+
targetUrn.toString(),
49+
orPrivilegeGroups);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package com.linkedin.datahub.graphql.resolvers.structuredproperties;
2+
3+
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
4+
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTIES_ASPECT_NAME;
5+
6+
import com.datahub.authentication.Authentication;
7+
import com.linkedin.common.AuditStamp;
8+
import com.linkedin.common.urn.Urn;
9+
import com.linkedin.common.urn.UrnUtils;
10+
import com.linkedin.datahub.graphql.QueryContext;
11+
import com.linkedin.datahub.graphql.exception.AuthorizationException;
12+
import com.linkedin.datahub.graphql.generated.PropertyValueInput;
13+
import com.linkedin.datahub.graphql.generated.UpsertStructuredPropertiesInput;
14+
import com.linkedin.datahub.graphql.resolvers.mutate.util.StructuredPropertyUtils;
15+
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertiesMapper;
16+
import com.linkedin.entity.EntityResponse;
17+
import com.linkedin.entity.client.EntityClient;
18+
import com.linkedin.metadata.entity.AspectUtils;
19+
import com.linkedin.metadata.utils.AuditStampUtils;
20+
import com.linkedin.mxe.MetadataChangeProposal;
21+
import com.linkedin.structured.PrimitivePropertyValueArray;
22+
import com.linkedin.structured.StructuredProperties;
23+
import com.linkedin.structured.StructuredPropertyValueAssignment;
24+
import com.linkedin.structured.StructuredPropertyValueAssignmentArray;
25+
import graphql.com.google.common.collect.ImmutableSet;
26+
import graphql.schema.DataFetcher;
27+
import graphql.schema.DataFetchingEnvironment;
28+
import java.util.HashMap;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.Objects;
32+
import java.util.concurrent.CompletableFuture;
33+
import java.util.stream.Collectors;
34+
import javax.annotation.Nonnull;
35+
36+
public class UpsertStructuredPropertiesResolver
37+
implements DataFetcher<
38+
CompletableFuture<com.linkedin.datahub.graphql.generated.StructuredProperties>> {
39+
40+
private final EntityClient _entityClient;
41+
42+
public UpsertStructuredPropertiesResolver(@Nonnull final EntityClient entityClient) {
43+
_entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null");
44+
}
45+
46+
@Override
47+
public CompletableFuture<com.linkedin.datahub.graphql.generated.StructuredProperties> get(
48+
final DataFetchingEnvironment environment) throws Exception {
49+
final QueryContext context = environment.getContext();
50+
final Authentication authentication = context.getAuthentication();
51+
52+
final UpsertStructuredPropertiesInput input =
53+
bindArgument(environment.getArgument("input"), UpsertStructuredPropertiesInput.class);
54+
final Urn assetUrn = UrnUtils.getUrn(input.getAssetUrn());
55+
Map<String, List<PropertyValueInput>> updateMap = new HashMap<>();
56+
// create a map of updates from our input
57+
input
58+
.getStructuredPropertyInputParams()
59+
.forEach(param -> updateMap.put(param.getStructuredPropertyUrn(), param.getValues()));
60+
61+
return CompletableFuture.supplyAsync(
62+
() -> {
63+
try {
64+
// check authorization first
65+
if (!StructuredPropertyUtils.isAuthorizedToUpdateProperties(context, assetUrn)) {
66+
throw new AuthorizationException(
67+
String.format(
68+
"Not authorized to update properties on the gives urn %s", assetUrn));
69+
}
70+
71+
final AuditStamp auditStamp =
72+
AuditStampUtils.createAuditStamp(authentication.getActor().toUrnStr());
73+
74+
if (!_entityClient.exists(assetUrn, authentication)) {
75+
throw new RuntimeException(
76+
String.format("Asset with provided urn %s does not exist", assetUrn));
77+
}
78+
79+
// get or default the structured properties aspect
80+
StructuredProperties structuredProperties =
81+
getStructuredProperties(assetUrn, authentication);
82+
83+
// update the existing properties based on new value
84+
StructuredPropertyValueAssignmentArray properties =
85+
updateExistingProperties(structuredProperties, updateMap, auditStamp);
86+
87+
// append any new properties from our input
88+
addNewProperties(properties, updateMap, auditStamp);
89+
90+
structuredProperties.setProperties(properties);
91+
92+
// ingest change proposal
93+
final MetadataChangeProposal structuredPropertiesProposal =
94+
AspectUtils.buildMetadataChangeProposal(
95+
assetUrn, STRUCTURED_PROPERTIES_ASPECT_NAME, structuredProperties);
96+
97+
_entityClient.ingestProposal(structuredPropertiesProposal, authentication, false);
98+
99+
return StructuredPropertiesMapper.map(structuredProperties);
100+
} catch (Exception e) {
101+
throw new RuntimeException(
102+
String.format("Failed to perform update against input %s", input), e);
103+
}
104+
});
105+
}
106+
107+
private StructuredProperties getStructuredProperties(Urn assetUrn, Authentication authentication)
108+
throws Exception {
109+
EntityResponse response =
110+
_entityClient.getV2(
111+
assetUrn.getEntityType(),
112+
assetUrn,
113+
ImmutableSet.of(STRUCTURED_PROPERTIES_ASPECT_NAME),
114+
authentication);
115+
StructuredProperties structuredProperties = new StructuredProperties();
116+
structuredProperties.setProperties(new StructuredPropertyValueAssignmentArray());
117+
if (response != null && response.getAspects().containsKey(STRUCTURED_PROPERTIES_ASPECT_NAME)) {
118+
structuredProperties =
119+
new StructuredProperties(
120+
response.getAspects().get(STRUCTURED_PROPERTIES_ASPECT_NAME).getValue().data());
121+
}
122+
return structuredProperties;
123+
}
124+
125+
private StructuredPropertyValueAssignmentArray updateExistingProperties(
126+
StructuredProperties structuredProperties,
127+
Map<String, List<PropertyValueInput>> updateMap,
128+
AuditStamp auditStamp) {
129+
return new StructuredPropertyValueAssignmentArray(
130+
structuredProperties.getProperties().stream()
131+
.map(
132+
propAssignment -> {
133+
String propUrnString = propAssignment.getPropertyUrn().toString();
134+
if (updateMap.containsKey(propUrnString)) {
135+
List<PropertyValueInput> valueList = updateMap.get(propUrnString);
136+
PrimitivePropertyValueArray values =
137+
new PrimitivePropertyValueArray(
138+
valueList.stream()
139+
.map(StructuredPropertyUtils::mapPropertyValueInput)
140+
.collect(Collectors.toList()));
141+
propAssignment.setValues(values);
142+
propAssignment.setLastModified(auditStamp);
143+
}
144+
return propAssignment;
145+
})
146+
.collect(Collectors.toList()));
147+
}
148+
149+
private void addNewProperties(
150+
StructuredPropertyValueAssignmentArray properties,
151+
Map<String, List<PropertyValueInput>> updateMap,
152+
AuditStamp auditStamp) {
153+
// first remove existing properties from updateMap so that we append only new properties
154+
properties.forEach(prop -> updateMap.remove(prop.getPropertyUrn().toString()));
155+
156+
updateMap.forEach(
157+
(structuredPropUrn, values) -> {
158+
StructuredPropertyValueAssignment valueAssignment =
159+
new StructuredPropertyValueAssignment();
160+
valueAssignment.setPropertyUrn(UrnUtils.getUrn(structuredPropUrn));
161+
valueAssignment.setValues(
162+
new PrimitivePropertyValueArray(
163+
values.stream()
164+
.map(StructuredPropertyUtils::mapPropertyValueInput)
165+
.collect(Collectors.toList())));
166+
valueAssignment.setCreated(auditStamp);
167+
valueAssignment.setLastModified(auditStamp);
168+
properties.add(valueAssignment);
169+
});
170+
}
171+
}

datahub-graphql-core/src/main/resources/properties.graphql

+22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
extend type Mutation {
2+
"""
3+
Upsert structured properties onto a given asset
4+
"""
5+
upsertStructuredProperties(input: UpsertStructuredPropertiesInput!): StructuredProperties!
6+
}
7+
18
"""
29
A structured property that can be shared between different entities
310
"""
@@ -157,6 +164,21 @@ type StructuredPropertiesEntry {
157164
valueEntities: [Entity]
158165
}
159166

167+
"""
168+
Input for upserting structured properties on a given asset
169+
"""
170+
input UpsertStructuredPropertiesInput {
171+
"""
172+
The urn of the asset that we are updating
173+
"""
174+
assetUrn: String!
175+
176+
"""
177+
The list of structured properties you want to upsert on this asset
178+
"""
179+
structuredPropertyInputParams: [StructuredPropertyInputParams!]!
180+
}
181+
160182
"""
161183
A data type registered in DataHub
162184
"""

0 commit comments

Comments
 (0)