-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add updateStructuredProperty graphql endpoint
- Loading branch information
1 parent
7569510
commit 992f442
Showing
4 changed files
with
320 additions
and
0 deletions.
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
129 changes: 129 additions & 0 deletions
129
...edin/datahub/graphql/resolvers/structuredproperties/UpdateStructuredPropertyResolver.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,129 @@ | ||
package com.linkedin.datahub.graphql.resolvers.structuredproperties; | ||
|
||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; | ||
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_ENTITY_NAME; | ||
|
||
import com.linkedin.common.urn.Urn; | ||
import com.linkedin.common.urn.UrnUtils; | ||
import com.linkedin.data.template.SetMode; | ||
import com.linkedin.data.template.StringArray; | ||
import com.linkedin.data.template.StringArrayMap; | ||
import com.linkedin.datahub.graphql.QueryContext; | ||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; | ||
import com.linkedin.datahub.graphql.exception.AuthorizationException; | ||
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity; | ||
import com.linkedin.datahub.graphql.generated.UpdateStructuredPropertyInput; | ||
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertyMapper; | ||
import com.linkedin.entity.EntityResponse; | ||
import com.linkedin.entity.client.EntityClient; | ||
import com.linkedin.metadata.aspect.patch.builder.StructuredPropertyDefinitionPatchBuilder; | ||
import com.linkedin.mxe.MetadataChangeProposal; | ||
import com.linkedin.structured.PrimitivePropertyValue; | ||
import com.linkedin.structured.PropertyCardinality; | ||
import com.linkedin.structured.PropertyValue; | ||
import graphql.schema.DataFetcher; | ||
import graphql.schema.DataFetchingEnvironment; | ||
import java.util.Objects; | ||
import java.util.concurrent.CompletableFuture; | ||
import javax.annotation.Nonnull; | ||
|
||
public class UpdateStructuredPropertyResolver | ||
implements DataFetcher<CompletableFuture<StructuredPropertyEntity>> { | ||
|
||
private final EntityClient _entityClient; | ||
|
||
public UpdateStructuredPropertyResolver(@Nonnull final EntityClient entityClient) { | ||
_entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null"); | ||
} | ||
|
||
@Override | ||
public CompletableFuture<StructuredPropertyEntity> get(final DataFetchingEnvironment environment) | ||
throws Exception { | ||
final QueryContext context = environment.getContext(); | ||
|
||
final UpdateStructuredPropertyInput input = | ||
bindArgument(environment.getArgument("input"), UpdateStructuredPropertyInput.class); | ||
|
||
return CompletableFuture.supplyAsync( | ||
() -> { | ||
try { | ||
if (!AuthorizationUtils.canManageStructuredProperties(context)) { | ||
throw new AuthorizationException( | ||
"Unable to update structured property. Please contact your admin."); | ||
} | ||
final Urn propertyUrn = UrnUtils.getUrn(input.getUrn()); | ||
StructuredPropertyDefinitionPatchBuilder builder = | ||
new StructuredPropertyDefinitionPatchBuilder().urn(propertyUrn); | ||
|
||
if (input.getDisplayName() != null) { | ||
builder.setDisplayName(input.getDisplayName()); | ||
} | ||
if (input.getDescription() != null) { | ||
builder.setDescription(input.getDescription()); | ||
} | ||
if (input.getImmutable() != null) { | ||
builder.setImmutable(input.getImmutable()); | ||
} | ||
if (input.getTypeQualifier() != null) { | ||
buildTypeQualifier(input, builder); | ||
} | ||
if (input.getNewAllowedValues() != null) { | ||
buildAllowedValues(input, builder); | ||
} | ||
if (input.getSetCardinalityAsMultiple() != null) { | ||
builder.setCardinality(PropertyCardinality.MULTIPLE); | ||
} | ||
if (input.getNewEntityTypes() != null) { | ||
input.getNewEntityTypes().forEach(builder::addEntityType); | ||
} | ||
|
||
MetadataChangeProposal mcp = builder.build(); | ||
_entityClient.ingestProposal(context.getOperationContext(), mcp, false); | ||
|
||
EntityResponse response = | ||
_entityClient.getV2( | ||
context.getOperationContext(), | ||
STRUCTURED_PROPERTY_ENTITY_NAME, | ||
propertyUrn, | ||
null); | ||
return StructuredPropertyMapper.map(context, response); | ||
} catch (Exception e) { | ||
throw new RuntimeException( | ||
String.format("Failed to perform update against input %s", input), e); | ||
} | ||
}); | ||
} | ||
|
||
private void buildTypeQualifier( | ||
@Nonnull final UpdateStructuredPropertyInput input, | ||
@Nonnull final StructuredPropertyDefinitionPatchBuilder builder) { | ||
if (input.getTypeQualifier().getNewAllowedTypes() != null) { | ||
final StringArrayMap typeQualifier = new StringArrayMap(); | ||
StringArray allowedTypes = new StringArray(); | ||
allowedTypes.addAll(input.getTypeQualifier().getNewAllowedTypes()); | ||
typeQualifier.put("allowedTypes", allowedTypes); | ||
builder.setTypeQualifier(typeQualifier); | ||
} | ||
} | ||
|
||
private void buildAllowedValues( | ||
@Nonnull final UpdateStructuredPropertyInput input, | ||
@Nonnull final StructuredPropertyDefinitionPatchBuilder builder) { | ||
input | ||
.getNewAllowedValues() | ||
.forEach( | ||
allowedValueInput -> { | ||
PropertyValue value = new PropertyValue(); | ||
PrimitivePropertyValue primitiveValue = new PrimitivePropertyValue(); | ||
if (allowedValueInput.getStringValue() != null) { | ||
primitiveValue.setString(allowedValueInput.getStringValue()); | ||
} | ||
if (allowedValueInput.getNumberValue() != null) { | ||
primitiveValue.setDouble(allowedValueInput.getNumberValue().doubleValue()); | ||
} | ||
value.setValue(primitiveValue); | ||
value.setDescription(allowedValueInput.getDescription(), SetMode.IGNORE_NULL); | ||
builder.addAllowedValue(value); | ||
}); | ||
} | ||
} |
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
123 changes: 123 additions & 0 deletions
123
.../datahub/graphql/resolvers/structuredproperties/UpdateStructuredPropertyResolverTest.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,123 @@ | ||
package com.linkedin.datahub.graphql.resolvers.structuredproperties; | ||
|
||
import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; | ||
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.testng.Assert.assertEquals; | ||
import static org.testng.Assert.assertThrows; | ||
|
||
import com.linkedin.common.urn.UrnUtils; | ||
import com.linkedin.datahub.graphql.QueryContext; | ||
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity; | ||
import com.linkedin.datahub.graphql.generated.UpdateStructuredPropertyInput; | ||
import com.linkedin.entity.EntityResponse; | ||
import com.linkedin.entity.EnvelopedAspectMap; | ||
import com.linkedin.entity.client.EntityClient; | ||
import com.linkedin.metadata.Constants; | ||
import com.linkedin.mxe.MetadataChangeProposal; | ||
import com.linkedin.r2.RemoteInvocationException; | ||
import graphql.schema.DataFetchingEnvironment; | ||
import java.util.concurrent.CompletionException; | ||
import org.mockito.Mockito; | ||
import org.testng.annotations.Test; | ||
|
||
public class UpdateStructuredPropertyResolverTest { | ||
private static final String TEST_STRUCTURED_PROPERTY_URN = "urn:li:structuredProperty:1"; | ||
|
||
private static final UpdateStructuredPropertyInput TEST_INPUT = | ||
new UpdateStructuredPropertyInput( | ||
TEST_STRUCTURED_PROPERTY_URN, | ||
"New Display Name", | ||
"new description", | ||
true, | ||
null, | ||
null, | ||
null, | ||
null); | ||
|
||
@Test | ||
public void testGetSuccess() throws Exception { | ||
EntityClient mockEntityClient = initMockEntityClient(true); | ||
UpdateStructuredPropertyResolver resolver = | ||
new UpdateStructuredPropertyResolver(mockEntityClient); | ||
|
||
// Execute resolver | ||
QueryContext mockContext = getMockAllowContext(); | ||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); | ||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); | ||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext); | ||
|
||
StructuredPropertyEntity prop = resolver.get(mockEnv).get(); | ||
|
||
assertEquals(prop.getUrn(), TEST_STRUCTURED_PROPERTY_URN); | ||
|
||
// Validate that we called ingest | ||
Mockito.verify(mockEntityClient, Mockito.times(1)) | ||
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false)); | ||
} | ||
|
||
@Test | ||
public void testGetUnauthorized() throws Exception { | ||
EntityClient mockEntityClient = initMockEntityClient(true); | ||
UpdateStructuredPropertyResolver resolver = | ||
new UpdateStructuredPropertyResolver(mockEntityClient); | ||
|
||
// Execute resolver | ||
QueryContext mockContext = getMockDenyContext(); | ||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); | ||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); | ||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext); | ||
|
||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); | ||
|
||
// Validate that we did NOT call ingest | ||
Mockito.verify(mockEntityClient, Mockito.times(0)) | ||
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false)); | ||
} | ||
|
||
@Test | ||
public void testGetFailure() throws Exception { | ||
EntityClient mockEntityClient = initMockEntityClient(false); | ||
UpdateStructuredPropertyResolver resolver = | ||
new UpdateStructuredPropertyResolver(mockEntityClient); | ||
|
||
// Execute resolver | ||
QueryContext mockContext = getMockAllowContext(); | ||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); | ||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT); | ||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext); | ||
|
||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); | ||
|
||
// Validate that ingest was called, but that caused a failure | ||
Mockito.verify(mockEntityClient, Mockito.times(1)) | ||
.ingestProposal(any(), any(MetadataChangeProposal.class), Mockito.eq(false)); | ||
} | ||
|
||
private EntityClient initMockEntityClient(boolean shouldSucceed) throws Exception { | ||
EntityClient client = Mockito.mock(EntityClient.class); | ||
EntityResponse response = new EntityResponse(); | ||
response.setEntityName(Constants.STRUCTURED_PROPERTY_ENTITY_NAME); | ||
response.setUrn(UrnUtils.getUrn(TEST_STRUCTURED_PROPERTY_URN)); | ||
response.setAspects(new EnvelopedAspectMap()); | ||
if (shouldSucceed) { | ||
Mockito.when( | ||
client.getV2( | ||
any(), | ||
Mockito.eq(Constants.STRUCTURED_PROPERTY_ENTITY_NAME), | ||
any(), | ||
Mockito.eq(null))) | ||
.thenReturn(response); | ||
} else { | ||
Mockito.when( | ||
client.getV2( | ||
any(), | ||
Mockito.eq(Constants.STRUCTURED_PROPERTY_ENTITY_NAME), | ||
any(), | ||
Mockito.eq(null))) | ||
.thenThrow(new RemoteInvocationException()); | ||
} | ||
|
||
return client; | ||
} | ||
} |