Skip to content

Commit

Permalink
feat(forms) Add CRUD endpoints to GraphQL for Form entities (datahub-…
Browse files Browse the repository at this point in the history
  • Loading branch information
chriscollins3456 authored and aviv-julienjehannet committed Jul 17, 2024
1 parent cf78d4d commit 89fb331
Show file tree
Hide file tree
Showing 13 changed files with 921 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,11 @@
import com.linkedin.datahub.graphql.resolvers.form.BatchAssignFormResolver;
import com.linkedin.datahub.graphql.resolvers.form.BatchRemoveFormResolver;
import com.linkedin.datahub.graphql.resolvers.form.CreateDynamicFormAssignmentResolver;
import com.linkedin.datahub.graphql.resolvers.form.CreateFormResolver;
import com.linkedin.datahub.graphql.resolvers.form.DeleteFormResolver;
import com.linkedin.datahub.graphql.resolvers.form.IsFormAssignedToMeResolver;
import com.linkedin.datahub.graphql.resolvers.form.SubmitFormPromptResolver;
import com.linkedin.datahub.graphql.resolvers.form.UpdateFormResolver;
import com.linkedin.datahub.graphql.resolvers.form.VerifyFormResolver;
import com.linkedin.datahub.graphql.resolvers.glossary.AddRelatedTermsResolver;
import com.linkedin.datahub.graphql.resolvers.glossary.CreateGlossaryNodeResolver;
Expand Down Expand Up @@ -1319,7 +1322,11 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("raiseIncident", new RaiseIncidentResolver(this.entityClient))
.dataFetcher(
"updateIncidentStatus",
new UpdateIncidentStatusResolver(this.entityClient, this.entityService));
new UpdateIncidentStatusResolver(this.entityClient, this.entityService))
.dataFetcher(
"createForm", new CreateFormResolver(this.entityClient, this.formService))
.dataFetcher("deleteForm", new DeleteFormResolver(this.entityClient))
.dataFetcher("updateForm", new UpdateFormResolver(this.entityClient));
if (featureFlags.isBusinessAttributeEntityEnabled()) {
typeWiring
.dataFetcher(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,13 @@ public static <T> T restrictEntity(@Nonnull Object entity, Class<T> clazz) {
}
}

public static boolean canManageForms(@Nonnull QueryContext context) {
return AuthUtil.isAuthorized(
context.getAuthorizer(),
context.getActorUrn(),
PoliciesConfig.MANAGE_DOCUMENTATION_FORMS_PRIVILEGE);
}

public static boolean isAuthorized(
@Nonnull Authorizer authorizer,
@Nonnull String actor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.linkedin.datahub.graphql.resolvers.form;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;

import com.linkedin.common.urn.Urn;
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.CreateFormInput;
import com.linkedin.datahub.graphql.generated.CreatePromptInput;
import com.linkedin.datahub.graphql.generated.Form;
import com.linkedin.datahub.graphql.generated.FormPromptType;
import com.linkedin.datahub.graphql.resolvers.mutate.util.FormUtils;
import com.linkedin.datahub.graphql.types.form.FormMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.form.FormInfo;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.service.FormService;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class CreateFormResolver implements DataFetcher<CompletableFuture<Form>> {

private final EntityClient _entityClient;
private final FormService _formService;

public CreateFormResolver(
@Nonnull final EntityClient entityClient, @Nonnull final FormService formService) {
_entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null");
_formService = Objects.requireNonNull(formService, "formService must not be null");
}

@Override
public CompletableFuture<Form> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();

final CreateFormInput input =
bindArgument(environment.getArgument("input"), CreateFormInput.class);
final FormInfo formInfo = FormUtils.mapFormInfo(input);

return CompletableFuture.supplyAsync(
() -> {
try {
if (!AuthorizationUtils.canManageForms(context)) {
throw new AuthorizationException("Unable to create form. Please contact your admin.");
}
validatePrompts(input.getPrompts());

Urn formUrn =
_formService.createForm(context.getOperationContext(), formInfo, input.getId());
EntityResponse response =
_entityClient.getV2(
context.getOperationContext(), Constants.FORM_ENTITY_NAME, formUrn, null);
return FormMapper.map(context, response);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e);
}
});
}

private void validatePrompts(@Nullable List<CreatePromptInput> prompts) {
if (prompts == null) {
return;
}
prompts.forEach(
prompt -> {
if (prompt.getType().equals(FormPromptType.STRUCTURED_PROPERTY)
|| prompt.getType().equals(FormPromptType.FIELDS_STRUCTURED_PROPERTY)) {
if (prompt.getStructuredPropertyParams() == null) {
throw new IllegalArgumentException(
"Provided prompt with type STRUCTURED_PROPERTY or FIELDS_STRUCTURED_PROPERTY and no structured property params");
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.linkedin.datahub.graphql.resolvers.form;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;

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.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.DeleteFormInput;
import com.linkedin.entity.client.EntityClient;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DeleteFormResolver implements DataFetcher<CompletableFuture<Boolean>> {

private final EntityClient _entityClient;

public DeleteFormResolver(@Nonnull final EntityClient entityClient) {
_entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null");
}

@Override
public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment)
throws Exception {
final QueryContext context = environment.getContext();

final DeleteFormInput input =
bindArgument(environment.getArgument("input"), DeleteFormInput.class);
final Urn formUrn = UrnUtils.getUrn(input.getUrn());

return CompletableFuture.supplyAsync(
() -> {
try {
if (!AuthorizationUtils.canManageForms(context)) {
throw new AuthorizationException("Unable to delete form. Please contact your admin.");
}
_entityClient.deleteEntity(context.getOperationContext(), formUrn);
// Asynchronously Delete all references to the entity (to return quickly)
CompletableFuture.runAsync(
() -> {
try {
_entityClient.deleteEntityReferences(context.getOperationContext(), formUrn);
} catch (Exception e) {
log.error(
String.format(
"Caught exception while attempting to clear all entity references for Form with urn %s",
formUrn),
e);
}
});

return true;
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.linkedin.datahub.graphql.resolvers.form;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;

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.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.Form;
import com.linkedin.datahub.graphql.generated.UpdateFormInput;
import com.linkedin.datahub.graphql.resolvers.mutate.util.FormUtils;
import com.linkedin.datahub.graphql.types.form.FormMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.form.FormType;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.aspect.patch.builder.FormInfoPatchBuilder;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;

public class UpdateFormResolver implements DataFetcher<CompletableFuture<Form>> {

private final EntityClient _entityClient;

public UpdateFormResolver(@Nonnull final EntityClient entityClient) {
_entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null");
}

@Override
public CompletableFuture<Form> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();

final UpdateFormInput input =
bindArgument(environment.getArgument("input"), UpdateFormInput.class);
final Urn formUrn = UrnUtils.getUrn(input.getUrn());

return CompletableFuture.supplyAsync(
() -> {
try {
if (!AuthorizationUtils.canManageForms(context)) {
throw new AuthorizationException("Unable to update form. Please contact your admin.");
}
if (!_entityClient.exists(context.getOperationContext(), formUrn)) {
throw new IllegalArgumentException(
String.format("Form with urn %s does not exist", formUrn));
}

FormInfoPatchBuilder patchBuilder = new FormInfoPatchBuilder().urn(formUrn);
if (input.getName() != null) {
patchBuilder.setName(input.getName());
}
if (input.getDescription() != null) {
patchBuilder.setDescription(input.getDescription());
}
if (input.getType() != null) {
patchBuilder.setType(FormType.valueOf(input.getType().toString()));
}
if (input.getPromptsToAdd() != null) {
patchBuilder.addPrompts(FormUtils.mapPromptsToAdd(input.getPromptsToAdd()));
}
if (input.getPromptsToRemove() != null) {
patchBuilder.removePrompts(input.getPromptsToRemove());
}
if (input.getActors() != null) {
if (input.getActors().getOwners() != null) {
patchBuilder.setOwnershipForm(input.getActors().getOwners());
}
if (input.getActors().getUsersToAdd() != null) {
input.getActors().getUsersToAdd().forEach(patchBuilder::addAssignedUser);
}
if (input.getActors().getUsersToRemove() != null) {
input.getActors().getUsersToRemove().forEach(patchBuilder::removeAssignedUser);
}
if (input.getActors().getGroupsToAdd() != null) {
input.getActors().getGroupsToAdd().forEach(patchBuilder::addAssignedGroup);
}
if (input.getActors().getGroupsToRemove() != null) {
input.getActors().getGroupsToRemove().forEach(patchBuilder::removeAssignedGroup);
}
}
_entityClient.ingestProposal(
context.getOperationContext(), patchBuilder.build(), false);

EntityResponse response =
_entityClient.getV2(
context.getOperationContext(), Constants.FORM_ENTITY_NAME, formUrn, null);
return FormMapper.map(context, response);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to perform update against input %s", input), e);
}
});
}
}
Loading

0 comments on commit 89fb331

Please sign in to comment.