Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Atomic Operations Support #2979

Merged
merged 37 commits into from
May 29, 2023
Merged

Add Atomic Operations Support #2979

merged 37 commits into from
May 29, 2023

Conversation

justin-tay
Copy link
Collaborator

Resolves #2915

Description

Adds support for JSON API Atomic Operations.

How Has This Been Tested?

Added the unit and integration tests

License

I confirm that this contribution is made under an Apache 2.0 license and that I have the authority necessary to make this contribution on behalf of its copyright owner.

@aklish
Copy link
Member

aklish commented May 9, 2023

Nice. Will take a look soon!

}
fullPathBuilder.append(ref.getType());

// Only relationship operations or resource update operation should have the id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true? What about remove? That looks like it can also have an ID.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason I recall that the delete needed to be performed on the collection endpoint and needed to have data specified with the type and id. So later on if the data was omitted I will attempt to generate this when specified from the ref.

                    // If data is not provided, but it is to remove a resource the data is optional
                    // and should be generated from the ref
                    if ((data == null || JsonNodeType.NULL.equals(data.getNodeType()))
                            && OperationCode.REMOVE.equals(operation.getOperationCode())) {
                        data = requestScope.getMapper().getObjectMapper().createObjectNode().put("type", ref.getType())
                                .put("id", ref.getId() != null ? ref.getId() : ref.getLid());
                    }

Copy link
Member

@aklish aklish May 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON API core spec has this example of a DELETE with the ID in the path:

DELETE /photos/1 HTTP/1.1
Accept: application/vnd.api+json

The operations spec has this example of specifying the ID in the operation REF (note that data is missing):

{
  "atomic:operations": [{
    "op": "remove",
    "ref": {
      "type": "articles",
      "id": "13"
    }
  }]
}

handleDeleteOp does not append the ID to the fullPath if data is missing. However, if data is present it appends the ID. Not sure what is more common, but the path will be different depending on the presence of data. The spec example shows data missing. This looks to me like it might not work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct I'm not sure why I ended up not specifying the id in the ref for the remove operation. I shall change it to use the id in the remove operation and not generate the data.

Resource resource = mapper.forAtomicOperations()
.readResource(operation.getData());
if (resource.getType() != null) {
if (resource.getAttributes() == null || resource.getAttributes().isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we care about attributes being null/empty here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to tell whether it was an relationship operation. If it is a relationship operation and the attributes would be null or empty.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec says if the operations is targeting a resource, then ref/href MUST be used. The same goes for deleting a resource (non-relationship). Should this be an error instead (instead of inferring the ref)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking at the spec again it looks like you are correct that for delete resource scenario the ref/href is mandatory. I have updated the code to only infer the ref for the add and update resource scenario where the ref/href is optional. The code thus will not attempt to infer for the delete resource and any relationship operations scenario.


Map<String, Object> productAttributes = response.path("'atomic:results'[3].data.attributes");
assertEquals("Product1", productAttributes.get("commonName"));
assertEquals("Product1 Description", productAttributes.get("description"));
Copy link
Member

@aklish aklish May 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a number of patch extenstion requests in ResourceIT (you can search for JSONAPI_CONTENT_TYPE_WITH_JSON_PATCH_EXTENSION). They also verify that a subsequent GET request has the correct values. I recommend doing a GET here and making sure that is as expected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the test cases.

Map<String, Object> attributes = response.path("'atomic:results'[1].data.attributes");
assertEquals("Foo2", attributes.get("commonName"));
assertEquals("Updated Description", attributes.get("description"));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment. Do another round trip request and verify the response matches what we expect (to confirm the database was updated correctly).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the test cases.

),
atomicOperation(AtomicOperationCode.add,
ref(
type("group/com.example.operationsrel1/products"),
Copy link
Member

@aklish aklish May 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a valid way of referencing type? This looks more like a href. It doesn't seem like a ref is a very powerful way of building a path - and to me, it looks like type is referring to type in the core spec.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually not sure. I thought the intent was that the ref could always be expressed as a href and vice-versa. It's not really that clear from the spec why there were 2 ways to express the target of the operation.

Copy link
Member

@aklish aklish May 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree the spec is confusing. My take is that some/most JSON-API APIs only expose models at the root of the URL path (/books, /invoices). Then the REF objects can be used to navigate to most things.

The type of the REF then is really the root collection.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the test case to use the href instead of the ref.

@aklish
Copy link
Member

aklish commented May 13, 2023

One thing that's a little vague in the spec is the use of LID. I assume it means that an ID in a create op is referenced in LID in subsequent ops. We might also want to test that scenario to make sure it works as intended.

@justin-tay
Copy link
Collaborator Author

Have added the test cases for LID. Initially I thought that LID was specific to the atomic operations spec but I see it's actually part of the base 1.1 spec.

I think I should have addressed all the outstanding issues.

@aklish aklish merged commit e570df5 into yahoo:master May 29, 2023
@justin-tay justin-tay deleted the atomicop branch July 18, 2023 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

JSON API Atomic Operations support
2 participants