-
Notifications
You must be signed in to change notification settings - Fork 890
Introduce Atomic Operations extension #1437
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
Conversation
Doubts:
Suggestions:
|
Re: point 1 - Error objects may contain a Re: point 3 - the Re: point 4.i -
So the end result would be the same as if multiple separate requests to the API were issued, but with the guarantee of atomicity. |
Overall looks good to me, would be very happy to see this as an official extension and hopefully one day in the base spec. I may try implementing this soon so could give more feedback as a result of that. Should the extension have anything to say about how an implementation should handle security? e.g. limiting the number of operations per request. I suppose not as it's in the same domain as rate-limiting which the base spec doesn't say anything about. |
Would this mean, this will also cover the case of wanting to At first I thought it was fine to Personally I would prefer the above notation for such a case though instead of using an extension. |
@jugaadi:
WRT to @tobyzerner:Thanks for your reply! I already wrote the above, before I saw this. You're right about Re: point 3, spot on! Re: point 4, exactly :) Good spec reading! Re: 200 vs 204, I believe the spec is this way for two reasons: 1) so that a client does not have to unnecessarily update its internal representation and 2) to save bytes. I'm glad to hear you like the spec and are considering implementing it! That's great :) TBH, I'm ambivalent about a security section. There's nothing normative we ought to say about it, but I know many specs do try to explicitly call out specific security considerations. @Doqnach:Yes. This allows you to create many resource objects in one request. I agree that your syntax would be simpler for the simple use case you describe, but there are many hidden edge cases. F.e. can those added resources reference one another? If so, must they be created in a specific order? If we support serial additions, why not serial mutations? This extension solves these complex cases while also solving the use case you mentioned, albeit with a little bit less syntactic sugar. |
@tobyzerner @gabesullice @dgeb Thanks for the clarifications. Below are some of my concerns. Do correct me if Im wrong.
UPDATING RESOURCES
UPDATING RELATIONSHIPS
Suggestions
|
Responding to your concerns @jugaadi (also, thank you for taking the time to think so deeply about this!):
{
"trace:requestID": "{client-generated-uuid}",
"atomic:operations": [{}, {}, "..."]
} The server could trace requests and reject requests with IDs that it has already processed. This would work for atomic operations and also for
We could give explicit guidance for clients like this:
Just for context: Order was made a requirement to simplify server implementations. @dgeb and I felt that clients would generally have an easier time ordering operations than servers would since, in most cases, UI interactions will spawn operations. As long as those spawned operations are kept in order, they should automatically be processable in the order that they were spawned (even if it might be an inefficient order) since anything else would have been a nonsensical user flow. |
Thanks @gabesullice for the explanation.
If it still gives the same impression, we can avoid it. We can go ahead with the current format.
|
Any updates? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks great!
In the base json:api spec, a server MUST return 201 No Content with no response document only for delete and relationship update. In all other cases, it MAY, allowing the server to return the same data as in the request. This can be useful in cases where it's expensive to determine what has changed. I believe the equivalent for atomic:operations is: "the server MUST return a result with no data or, if all results are empty, the server MAY respond with 204 No Content and no document." The cases where this rule applies are the same as in the base spec, except for one: "If a server accepts an update and doesn’t update any attributes besides those provided, the server MUST return a result with no data or, if all results are empty, the server MAY respond with 204 No Content and no document." Compared to the base spec: "If an update is successful and the server doesn’t update any attributes besides those provided, the server MUST return either a 200 OK status code and response document (as described above) or a 204 No Content status code with no response document." My conclusion is that the atomic:operations spec is more strict than the base spec for resource updates. Is this intentional or an oversight? I would prefer this to be loosened to MAY, as in: |
@bart-degreed, great attention to detail! I do not see a problem with loosening the extension to allow the server to either respond with an empty result or "complete" result. The only thing that must be preserved is that the order and count of results must match the order and count of the requested operations. @dgeb, am I missing some nuance that requires the extension to be more strict? The only reason that I can think of to choose the stricter wording in the extension is to allow the client to skip work if the result is empty. |
While updating my earlier implementation of the Operations proposal to the new Atomic Operations extension, I found that we do not clearly state if the "atomic:operations" array can be empty, in an operations request. |
- `ref`: an object that **MAY** contain any of the following combinations of | ||
members: | ||
|
||
- `type` and `id`: to target an individual resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first sight, I think it may induce that both type
& id
were required, but of course it's not in case of an "add" operation. Should we be more specific ?
- `type` and `id`: to target an individual resource. | |
- `type` and `id`: to target an individual resource. `id` **MAY** be omitted in case of an "add" operation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I agree with this refinement. What do you think @gabesullice ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I misunderstand the context here. After talking with @gabesullice we realized that ref
is unnecessary in an add
operation, which will already contain a type
in the data
member that represents the resource to be added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we still need a ref
to represents the "endpoint" to which the resource will be sent ?
If ref
is considered unnecessary for add
(we infer the endpoint from the resource itself), it also become unnecessary for update
, right ?
From an implementer POV, I like the idea of just having to look at ref
(or href
) to determine to which endpoint pass the data which is treated like an opaque blob at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, in this particular context of Atomic Operations, if ref
is not necessary in an add
operation, why should we have different endpoints when adding new resources through "plain" JSON:API calls ? A single /jsonapi/add would suffice.
But (in Drupal JSON:API implementation, at least), add
operations are performed on different endpoints, based on the resource type, and then there's a check that the data's type
(and also id
, for in case of updates) matches the endpoint URL.
I agree that this can be perceived as a kind of "redundancy", but this is already the case with existing "regular" JSON:API, right ?
Let's say I build a JSON:API implementation where every write calls goes to a /jsonapi
accepting PATCH or POST requests and inferring everything from the passed data, would it still be compliant with the original JSON:API spec ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's say I build a JSON:API implementation where every write calls goes to a /jsonapi accepting PATCH or POST requests and inferring everything from the passed data, would it still be compliant with the original JSON:API spec?
Yes, I think it would be. Imagine something like calendar.mydomain.com/
. Here, I could POST
reminders and events. A GET
request to /
(root) might return a mixed collection of reminders and events in chronological order. Is it good API design? I think you could argue both yes and no, but the spec doesn't prevent it.
One of the use cases that I wanted to support by using the href
member in an operation object was the concept of creating resource objects via editorialized endpoints. For example: adding both a tshirt
and a sandal
resource object to a /summer-styles
endpoint.
I think that's sort of similar to what you're asking. You might be implementing a /operations
endpoint where you want to add bothtshirt
and sandal
resource objects:
[{
"op": "add",
"ref": {
"type": "tshirt"
},
"data": {
"type": "tshirt",
"attributes": {
"sex": "male",
"color": "blue"
}
}
}, {
"op": "add",
"ref": {
"type": "sandal"
},
"data": {
"type": "sandal",
"attributes": {
"sex": "unisex",
"size": "gargantuan"
}
}
}]
That feels very redundant to me, honestly. However, you do say:
From an implementer POV, I like the idea of just having to look at ref (or href) to determine to which endpoint pass the data which is treated like an opaque blob at this point.
Knowing that you're implementing this within Drupal I'll take some liberties with my example...
I suspect you're talking about routing subrequests and I see why you'd want to treat the data
member as an opaque blob, but you have to deserialize the entire JSON document anyway. So, it seems like the code for handling an omitted ref
member below isn't too onerous or inelegant:
$operation = $deserialized['atomic:operations'][0];
$url = $operation['href'] ?? Url::fromRoute("jsonapi.{$operation['data']['type']}.collection.post")->toString();
$subrequest = Request::create($url);
Since allowing ref
to be omitted is elegant from the spec's perspective and there are still relatively elegant solutions for the server to handle that as well. I think it's okay. WDYT?
@bart-degreed as @gabesullice said, thanks for your attention to detail! I agree with your assessment. I have loosened the language around updates to support the same level of strictness as the base spec. The section in question now reads:
Note that I've changed |
@morvans Thanks for raising this. It's an edge case that I have not considered, but will discuss with @gabesullice. Possible update to consider:
|
- `atomic:operations` - an array of one or more [operation | ||
objects](#operation-objects). | ||
|
||
- `atomic:results` - an array of one or more [result objects](#result-objects). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been writing a simple implementation of this and this flexibility is causing a bit of extra work. Would it make sense to heighten this requirement to:
In addition, such a document MUST include one of the following members, but not both:
while changing A document that supports this extension...
above to A document using this extension...
I guess it comes down to a decision about whether we want to force servers to flexibly validate documents according to their declared extensions or whether we want clients to be able to be less precise about the extensions they're really expecting the server to process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For context, here's a snippet of my code:
$supported_extensions = [
'https://jsonapi.org/extensions/#atomic'
];
$unsupported_extensions = array_diff($extensions, $supported_extensions);
if (!empty($unsupported_extensions)) {
throw new UnsupportedMediaTypeException(sprintf('The %s JSON:API media type extension is not supported', current($unsupported_extensions)));
}
if (in_array($supported_extensions[0], $extensions, TRUE)) {
$request_document = Json::decode((string) $request->getContent());
if (!isset($request_document['atomic:operations'])) {
throw new BadRequestHttpException('Request documents using the %s JSON:API media type extension must include an `atomic:operations` top-level member.');
}
}
It's that final throw new BadRequestException()
that I'm concerned about. According to the language above, I think it's technically valid for a client to send a Content-Type
header with the atomic extension URI, even when it's not including an atomic:operations
member.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I guess it's not that bad. All it means is that I have to do:
$operations = isset($request_document['atomic:operations'])
? $request_document['atomic:operations']
: [];
a4893e1
to
021a93b
Compare
Just gave this another light scan. A few thoughts:
I think the Operation objects section needs to be rewritten. It feels clunky right now, probably because it grew organically as we tweaked and retweaked it. I don't think the actual structure of operation objects need to change, but I think that if we were to start that section with an empty canvas we could come up with a clearer way of describing the document structure. |
@gabesullice those are all good points. I've just pushed commits that attempt to address them, although I'm certainly open to discussing and refining them further.
Yes, I'd be open to collaborating on this 👍 |
Use more clear /operations endpoint in example Co-Authored-By: Gabe Sullice <gabriel@sullice.com>
2803d94
to
3acb504
Compare
Thanks everyone for all the input on this! I think it's time to get this merged 🎉 |
|
…h an invalid content type. This change additionally allows extensions proposed at json-api/json-api#1437. Added test + fixes for running an endpoint that is not exposed through JsonApiDotNetCore (and we should not interfere)
This is a proposal for an official extension to the JSON:API spec, as described in #1435. This proposal is based upon #1254, and supersedes that PR.
This extension provides a means to perform multiple "operations" in a linear and atomic manner. Operations are a serialized form of the mutations allowed in the base JSON:API specification. It uses the namespace
atomic
to emphasize the atomicity guarantee it provides.