Fix #2866: Add support for PATCH to feed API to be able to resolve a thread (#3027)

This commit is contained in:
Vivek Ratnavel Subramanian 2022-03-01 11:15:31 -08:00 committed by GitHub
parent 58df9f110e
commit b41b7d8bfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 0 deletions

View File

@ -16,6 +16,7 @@ package org.openmetadata.catalog.jdbi3;
import static org.openmetadata.catalog.Entity.helper;
import static org.openmetadata.catalog.util.EntityUtil.toBoolean;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
@ -23,13 +24,18 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import javax.json.JsonPatch;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.feed.EntityLinkThreadCount;
import org.openmetadata.catalog.api.feed.ThreadCount;
import org.openmetadata.catalog.entity.feed.Thread;
import org.openmetadata.catalog.resources.feeds.FeedResource;
import org.openmetadata.catalog.resources.feeds.FeedUtil;
import org.openmetadata.catalog.resources.feeds.MessageParser;
import org.openmetadata.catalog.resources.feeds.MessageParser.EntityLink;
@ -39,6 +45,8 @@ import org.openmetadata.catalog.type.Post;
import org.openmetadata.catalog.type.Relationship;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.RestUtil;
import org.openmetadata.catalog.util.RestUtil.PatchResponse;
public class FeedRepository {
private final CollectionDAO dao;
@ -230,6 +238,47 @@ public class FeedRepository {
return limitPostsInThreads(threads, limitPosts);
}
@Transaction
public final PatchResponse<Thread> patch(UriInfo uriInfo, UUID id, String user, JsonPatch patch)
throws IOException, ParseException {
// Get all the fields in the original thread that can be updated during PATCH operation
Thread original = get(id.toString());
// Apply JSON patch to the original thread to get the updated thread
Thread updated = JsonUtils.applyPatch(original, patch, Thread.class);
// update the "updatedBy" and "updatedAt" fields
updated.withUpdatedAt(System.currentTimeMillis()).withUpdatedBy(user);
restorePatchAttributes(original, updated);
// Update the attributes
String change = patchUpdate(original, updated) ? RestUtil.ENTITY_UPDATED : RestUtil.ENTITY_NO_CHANGE;
Thread updatedHref = FeedResource.addHref(uriInfo, updated);
return new PatchResponse<>(Status.OK, updatedHref, change);
}
private void restorePatchAttributes(Thread original, Thread updated) {
// Patch can't make changes to following fields. Ignore the changes
updated.withId(original.getId()).withAbout(original.getAbout());
}
private boolean patchUpdate(Thread original, Thread updated) throws JsonProcessingException {
updated.setId(original.getId());
// store the updated thread
// if there is no change, there is no need to apply patch
if (fieldsChanged(original, updated)) {
dao.feedDAO().update(updated.getId().toString(), JsonUtils.pojoToJson(updated));
return true;
}
return false;
}
private boolean fieldsChanged(Thread original, Thread updated) {
// Patch supports only isResolved and message for now
return original.getResolved() != updated.getResolved() || !original.getMessage().equals(updated.getMessage());
}
private List<Thread> limitPostsInThreads(List<Thread> threads, int limitPosts) {
for (Thread t : threads) {
List<Post> posts = t.getPosts();

View File

@ -13,25 +13,33 @@
package org.openmetadata.catalog.resources.feeds;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.annotations.Api;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -51,6 +59,7 @@ import org.openmetadata.catalog.resources.Collection;
import org.openmetadata.catalog.security.Authorizer;
import org.openmetadata.catalog.type.Post;
import org.openmetadata.catalog.util.RestUtil;
import org.openmetadata.catalog.util.RestUtil.PatchResponse;
import org.openmetadata.catalog.util.ResultList;
@Path("/v1/feed")
@ -61,8 +70,15 @@ import org.openmetadata.catalog.util.ResultList;
public class FeedResource {
// TODO add /v1/feed?user=userid
public static final String COLLECTION_PATH = "/v1/feed/";
public static final List<String> ALLOWED_FIELDS = getAllowedFields();
private final FeedRepository dao;
private static List<String> getAllowedFields() {
JsonPropertyOrder propertyOrder = Thread.class.getAnnotation(JsonPropertyOrder.class);
return new ArrayList<>(Arrays.asList(propertyOrder.value()));
}
public static List<Thread> addHref(UriInfo uriInfo, List<Thread> threads) {
threads.forEach(t -> addHref(uriInfo, t));
return threads;
@ -148,6 +164,33 @@ public class FeedResource {
return addHref(uriInfo, dao.get(id));
}
@PATCH
@Path("/{id}")
@Operation(
summary = "Update a thread by `id`.",
tags = "feeds",
description = "Update an existing thread using JsonPatch.",
externalDocs = @ExternalDocumentation(description = "JsonPatch RFC", url = "https://tools.ietf.org/html/rfc6902"))
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
public Response updateThread(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@PathParam("id") String id,
@RequestBody(
description = "JsonPatch with array of operations",
content =
@Content(
mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
examples = {
@ExampleObject("[" + "{op:remove, path:/a}," + "{op:add, path: /b, value: val}" + "]")
}))
JsonPatch patch)
throws IOException, ParseException {
PatchResponse<Thread> response =
dao.patch(uriInfo, UUID.fromString(id), securityContext.getUserPrincipal().getName(), patch);
return response.toResponse();
}
@GET
@Path("/count")
@Operation(

View File

@ -21,15 +21,18 @@ import static org.openmetadata.catalog.exception.CatalogExceptionMessage.entityN
import static org.openmetadata.catalog.security.SecurityUtil.authHeaders;
import static org.openmetadata.catalog.util.TestUtils.ADMIN_AUTH_HEADERS;
import static org.openmetadata.catalog.util.TestUtils.NON_EXISTENT_ENTITY;
import static org.openmetadata.catalog.util.TestUtils.assertListNotNull;
import static org.openmetadata.catalog.util.TestUtils.assertResponse;
import static org.openmetadata.catalog.util.TestUtils.assertResponseContains;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.ws.rs.client.WebTarget;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@ -55,6 +58,7 @@ import org.openmetadata.catalog.resources.feeds.FeedResource.ThreadList;
import org.openmetadata.catalog.type.Column;
import org.openmetadata.catalog.type.ColumnDataType;
import org.openmetadata.catalog.type.Post;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.TestUtils;
@Slf4j
@ -266,6 +270,38 @@ public class FeedResourceTest extends CatalogApplicationTest {
assertEquals(POST_COUNT, postList.getData().size());
}
@Test
void patch_thread_200() throws IOException {
// create a thread
CreateThread create = create().withMessage("message");
Thread thread = createAndCheck(create, ADMIN_AUTH_HEADERS);
String originalJson = JsonUtils.pojoToJson(thread);
// update message and resolved state
Thread updated = thread.withMessage("updated message").withResolved(true);
patchThreadAndCheck(updated, originalJson, ADMIN_AUTH_HEADERS);
}
@Test
void patch_thread_not_allowed_fields() throws IOException {
// create a thread
CreateThread create = create().withMessage("message");
Thread thread = createAndCheck(create, ADMIN_AUTH_HEADERS);
String originalJson = JsonUtils.pojoToJson(thread);
// update the About of the thread
String originalAbout = thread.getAbout();
Thread updated = thread.withAbout("<#E/user>");
patchThread(updated.getId(), originalJson, updated, ADMIN_AUTH_HEADERS);
updated = getThread(thread.getId(), ADMIN_AUTH_HEADERS);
// verify that the "About" is not changed
validateThread(updated, thread.getMessage(), thread.getCreatedBy(), originalAbout);
}
@Test
void list_threadsWithPostsLimit() throws HttpResponseException {
Thread thread = createAndCheck(create(), AUTH_HEADERS);
@ -402,4 +438,33 @@ public class FeedResourceTest extends CatalogApplicationTest {
linkThreadCount.stream().filter(l -> l.getEntityLink().equals(entityLink)).findFirst().orElseThrow();
return threadCount.getCount();
}
protected final Thread patchThreadAndCheck(Thread updated, String originalJson, Map<String, String> authHeaders)
throws IOException {
// Validate information returned in patch response has the updates
Thread returned = patchThread(updated.getId(), originalJson, updated, authHeaders);
compareEntities(updated, returned, authHeaders);
// GET the entity and Validate information returned
Thread getEntity = getThread(updated.getId(), authHeaders);
compareEntities(updated, getEntity, authHeaders);
return returned;
}
public final Thread patchThread(UUID id, String originalJson, Thread updated, Map<String, String> authHeaders)
throws JsonProcessingException, HttpResponseException {
String updatedThreadJson = JsonUtils.pojoToJson(updated);
JsonPatch patch = JsonUtils.getJsonPatch(originalJson, updatedThreadJson);
return TestUtils.patch(getResource(String.format("feed/%s", id)), patch, Thread.class, authHeaders);
}
public void compareEntities(Thread expected, Thread patched, Map<String, String> authHeaders) {
assertListNotNull(patched.getId(), patched.getHref(), patched.getAbout());
assertEquals(expected.getMessage(), patched.getMessage());
assertEquals(expected.getResolved(), patched.getResolved());
assertEquals(TestUtils.getPrincipal(authHeaders), patched.getUpdatedBy());
}
}