Skip to content

Commit

Permalink
Add Atomic Operations Support (#2979)
Browse files Browse the repository at this point in the history
* Add atomic operations extension

* Add test dsl.

* Fix tests

* Add operations to json api endpoint

* Fix delete case with no data

* Fix don't wrap runtimeexception in another runtimeexception

* Add life cycle tests

* Add tests for json api atomic operations mapper

* Add tests for relationship

* Add tests for invalid input

* Add dsl and tests for local id

* Fix tests

* Adds ability to customize the object mapper used by the JsonApiMapper

* Refactor naming

* Add tests

* Add standalone test

* Handle media type parameters according to spec

* Verify responses

* Infer ref only for add and update resource.

* Use id for remove resource

* Add ResourceIT tests

* Add lid to resource and tests for lid

* Change ref to href

* Set id when lid set

* Add json api lid test case

* Ref cannot be explicitly specified when creating resources

* Validate ref and href mutually exclusive

* Atomic operations result may contain meta

* Add lid test

* Validate ref cannot have both id and lid specified

* Update error messages for consistency

* Update ResourceIT lid test

* Add dsl tests

* Update error message

* Add native hints

* Fix test
  • Loading branch information
justin-tay committed May 29, 2023
1 parent a23401f commit e570df5
Show file tree
Hide file tree
Showing 106 changed files with 6,253 additions and 314 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void testResourceToCSV() {
resourceAttributes.put("queryType", queryObj.getQueryType());
resourceAttributes.put("createdOn", queryObj.getCreatedOn());

Resource resource = new Resource("tableExport", "0", resourceAttributes, null, null, null);
Resource resource = new Resource("tableExport", "0", null, resourceAttributes, null, null, null);

PersistentResource persistentResource = mock(PersistentResource.class);
when(persistentResource.getObject()).thenReturn(queryObj);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void testFormat() {
resourceAttributes.put("queryType", queryObj.getQueryType());
resourceAttributes.put("createdOn", queryObj.getCreatedOn());

Resource resource = new Resource("tableExport", "0", resourceAttributes, null, null, null);
Resource resource = new Resource("tableExport", "0", null, resourceAttributes, null, null, null);
PersistentResource persistentResource = mock(PersistentResource.class);
when(persistentResource.getObject()).thenReturn(queryObj);
when(persistentResource.getRequestScope()).thenReturn(scope);
Expand Down Expand Up @@ -115,7 +115,7 @@ public void testResourceToJSON() {
resourceAttributes.put("query", "{ tableExport { edges { node { query queryType} } } }");
resourceAttributes.put("queryType", QueryType.GRAPHQL_V1_0);

Resource resource = new Resource("tableExport", "0", resourceAttributes, null, null, null);
Resource resource = new Resource("tableExport", "0", null, resourceAttributes, null, null, null);
PersistentResource persistentResource = mock(PersistentResource.class);
when(persistentResource.getObject()).thenReturn(queryObj);
when(persistentResource.getRequestScope()).thenReturn(scope);
Expand Down
128 changes: 106 additions & 22 deletions elide-core/src/main/java/com/yahoo/elide/Elide.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.yahoo.elide.core.exceptions.HttpStatusException;
import com.yahoo.elide.core.exceptions.InternalServerErrorException;
import com.yahoo.elide.core.exceptions.InvalidURLException;
import com.yahoo.elide.core.exceptions.JsonApiAtomicOperationsException;
import com.yahoo.elide.core.exceptions.JsonPatchExtensionException;
import com.yahoo.elide.core.exceptions.TransactionException;
import com.yahoo.elide.core.security.User;
Expand All @@ -29,9 +30,12 @@
import com.yahoo.elide.core.utils.coerce.converters.ElideTypeConverter;
import com.yahoo.elide.core.utils.coerce.converters.Serde;
import com.yahoo.elide.jsonapi.EntityProjectionMaker;
import com.yahoo.elide.jsonapi.JsonApi;
import com.yahoo.elide.jsonapi.JsonApiMapper;
import com.yahoo.elide.jsonapi.extensions.JsonApiPatch;
import com.yahoo.elide.jsonapi.extensions.PatchRequestScope;
import com.yahoo.elide.jsonapi.extensions.JsonApiAtomicOperations;
import com.yahoo.elide.jsonapi.extensions.JsonApiAtomicOperationsRequestScope;
import com.yahoo.elide.jsonapi.extensions.JsonApiJsonPatch;
import com.yahoo.elide.jsonapi.extensions.JsonApiJsonPatchRequestScope;
import com.yahoo.elide.jsonapi.models.JsonApiDocument;
import com.yahoo.elide.jsonapi.parser.BaseVisitor;
import com.yahoo.elide.jsonapi.parser.DeleteVisitor;
Expand Down Expand Up @@ -73,9 +77,9 @@
*/
@Slf4j
public class Elide {
public static final String JSONAPI_CONTENT_TYPE = "application/vnd.api+json";
public static final String JSONAPI_CONTENT_TYPE = JsonApi.MEDIA_TYPE;
public static final String JSONAPI_CONTENT_TYPE_WITH_JSON_PATCH_EXTENSION =
"application/vnd.api+json; ext=jsonpatch";
JsonApi.JsonPatch.MEDIA_TYPE;

@Getter private final ElideSettings elideSettings;
@Getter private final AuditLogger auditLogger;
Expand Down Expand Up @@ -390,13 +394,13 @@ public ElideResponse patch(String baseUrlEndPoint, String contentType, String ac
String apiVersion, UUID requestId) {

Handler<DataStoreTransaction, User, HandlerResult> handler;
if (JsonApiPatch.isPatchExtension(contentType) && JsonApiPatch.isPatchExtension(accept)) {
if (JsonApiJsonPatch.isPatchExtension(contentType) && JsonApiJsonPatch.isPatchExtension(accept)) {
handler = (tx, user) -> {
PatchRequestScope requestScope = new PatchRequestScope(baseUrlEndPoint, path, apiVersion, tx,
user, requestId, queryParams, requestHeaders, elideSettings);
JsonApiJsonPatchRequestScope requestScope = new JsonApiJsonPatchRequestScope(baseUrlEndPoint, path,
apiVersion, tx, user, requestId, queryParams, requestHeaders, elideSettings);
try {
Supplier<Pair<Integer, JsonNode>> responder =
JsonApiPatch.processJsonPatch(dataStore, path, jsonApiDocument, requestScope);
JsonApiJsonPatch.processJsonPatch(dataStore, path, jsonApiDocument, requestScope);
return new HandlerResult(requestScope, responder);
} catch (RuntimeException e) {
return new HandlerResult(requestScope, e);
Expand Down Expand Up @@ -482,6 +486,87 @@ public ElideResponse delete(String baseUrlEndPoint, String path, String jsonApiD
});
}

/**
* Handle operations for the Atomic Operations extension.
*
* @param baseUrlEndPoint base URL with prefix endpoint
* @param contentType the content type
* @param accept the accept
* @param path the path
* @param jsonApiDocument the json api document
* @param opaqueUser the opaque user
* @param apiVersion the API version
* @return Elide response object
*/
public ElideResponse operations(String baseUrlEndPoint, String contentType, String accept,
String path, String jsonApiDocument,
User opaqueUser, String apiVersion) {
return operations(baseUrlEndPoint, contentType, accept, path, jsonApiDocument,
null, opaqueUser, apiVersion, UUID.randomUUID());
}

/**
* Handle operations for the Atomic Operations extension.
*
* @param baseUrlEndPoint base URL with prefix endpoint
* @param contentType the content type
* @param accept the accept
* @param path the path
* @param jsonApiDocument the json api document
* @param queryParams the query params
* @param opaqueUser the opaque user
* @param apiVersion the API version
* @param requestId the request ID
* @return Elide response object
*/
public ElideResponse operations(String baseUrlEndPoint, String contentType, String accept, String path,
String jsonApiDocument, MultivaluedMap<String, String> queryParams, User opaqueUser, String apiVersion,
UUID requestId) {
return operations(baseUrlEndPoint, contentType, accept, path, jsonApiDocument, queryParams, null, opaqueUser,
apiVersion, requestId);
}

/**
* Handle operations for the Atomic Operations extension.
*
* @param baseUrlEndPoint base URL with prefix endpoint
* @param contentType the content type
* @param accept the accept
* @param path the path
* @param jsonApiDocument the json api document
* @param queryParams the query params
* @param opaqueUser the opaque user
* @param apiVersion the API version
* @param requestId the request ID
* @return Elide response object
* @return
*/
public ElideResponse operations(String baseUrlEndPoint, String contentType, String accept, String path,
String jsonApiDocument, MultivaluedMap<String, String> queryParams,
Map<String, List<String>> requestHeaders, User opaqueUser, String apiVersion, UUID requestId) {

Handler<DataStoreTransaction, User, HandlerResult> handler;
if (JsonApiAtomicOperations.isAtomicOperationsExtension(contentType)
&& JsonApiAtomicOperations.isAtomicOperationsExtension(accept)) {
handler = (tx, user) -> {
JsonApiAtomicOperationsRequestScope requestScope = new JsonApiAtomicOperationsRequestScope(
baseUrlEndPoint, path,
apiVersion, tx, user, requestId, queryParams, requestHeaders, elideSettings);
try {
Supplier<Pair<Integer, JsonNode>> responder = JsonApiAtomicOperations
.processAtomicOperations(dataStore, path, jsonApiDocument, requestScope);
return new HandlerResult(requestScope, responder);
} catch (RuntimeException e) {
return new HandlerResult(requestScope, e);
}
};
} else {
return new ElideResponse(HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type");
}

return handleRequest(false, opaqueUser, dataStore::beginTransaction, requestId, handler);
}

public HandlerResult visit(String path, RequestScope requestScope, BaseVisitor visitor) {
try {
Supplier<Pair<Integer, JsonApiDocument>> responder = visitor.visit(JsonApiParser.parse(path));
Expand Down Expand Up @@ -559,8 +644,7 @@ private ElideResponse handleNonRuntimeException(Exception error, boolean isVerbo
return buildErrorResponse(mappedException, isVerbose);
}

if (error instanceof JacksonException) {
JacksonException jacksonException = (JacksonException) error;
if (error instanceof JacksonException jacksonException) {
String message = (jacksonException.getLocation() != null
&& jacksonException.getLocation().getSourceRef() != null)
? error.getMessage() //This will leak Java class info if the location isn't known.
Expand Down Expand Up @@ -589,34 +673,34 @@ private ElideResponse handleRuntimeException(RuntimeException error, boolean isV
throw error;
}

if (error instanceof ForbiddenAccessException) {
ForbiddenAccessException e = (ForbiddenAccessException) error;
if (error instanceof ForbiddenAccessException e) {
if (log.isDebugEnabled()) {
log.debug("{}", e.getLoggedMessage());
}
return buildErrorResponse(e, isVerbose);
}

if (error instanceof JsonPatchExtensionException) {
JsonPatchExtensionException e = (JsonPatchExtensionException) error;
log.debug("JSON patch extension exception caught", e);
if (error instanceof JsonPatchExtensionException e) {
log.debug("JSON API Json Patch extension exception caught", e);
return buildErrorResponse(e, isVerbose);
}

if (error instanceof HttpStatusException) {
HttpStatusException e = (HttpStatusException) error;
if (error instanceof JsonApiAtomicOperationsException e) {
log.debug("JSON API Atomic Operations extension exception caught", e);
return buildErrorResponse(e, isVerbose);
}

if (error instanceof HttpStatusException e) {
log.debug("Caught HTTP status exception", e);
return buildErrorResponse(e, isVerbose);
}

if (error instanceof ParseCancellationException) {
ParseCancellationException e = (ParseCancellationException) error;
if (error instanceof ParseCancellationException e) {
log.debug("Parse cancellation exception uncaught by Elide (i.e. invalid URL)", e);
return buildErrorResponse(new InvalidURLException(e), isVerbose);
}

if (error instanceof ConstraintViolationException) {
ConstraintViolationException e = (ConstraintViolationException) error;
if (error instanceof ConstraintViolationException e) {
log.debug("Constraint violation exception caught", e);
String message = "Constraint violation";
final ErrorObjects.ErrorObjectsBuilder errorObjectsBuilder = ErrorObjects.builder();
Expand All @@ -637,7 +721,7 @@ private ElideResponse handleRuntimeException(RuntimeException error, boolean isV
}

log.error("Error or exception uncaught by Elide", error);
throw new RuntimeException(error);
throw error;
}

public CustomErrorException mapError(Exception error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class HttpStatus {
public static final int SC_NOT_FOUND = 404;
public static final int SC_METHOD_NOT_ALLOWED = 405;
public static final int SC_TIMEOUT = 408;
public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
public static final int SC_LOCKED = 423;
public static final int SC_INTERNAL_SERVER_ERROR = 500;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2016, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.core.exceptions;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.tuple.Pair;

/**
* Exception describing error caused from JSON API Atomic Extension request.
*/
public class JsonApiAtomicOperationsException extends HttpStatusException {
private static final long serialVersionUID = 1L;
private final Pair<Integer, JsonNode> response;

public JsonApiAtomicOperationsException(int status, final JsonNode errorNode) {
super(status, "");
response = Pair.of(status, errorNode);
}

@Override
public Pair<Integer, JsonNode> getErrorResponse() {
return response;
}

@Override
public Pair<Integer, JsonNode> getVerboseErrorResponse() {
return response;
}
}
29 changes: 29 additions & 0 deletions elide-core/src/main/java/com/yahoo/elide/jsonapi/JsonApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2023, the original author or authors.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.jsonapi;

/**
* JSON:API.
*/
public class JsonApi {
private JsonApi() {
}
public static final String MEDIA_TYPE = "application/vnd.api+json";

public static class JsonPatch {
private JsonPatch() {
}

public static final String MEDIA_TYPE = "application/vnd.api+json; ext=jsonpatch";
}

public static class AtomicOperations {
private AtomicOperations() {
}

public static final String MEDIA_TYPE = "application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"";
}
}

0 comments on commit e570df5

Please sign in to comment.