How to deal with complex actions


#1

How do I deal with action that requires additional arguments? A resource has “onToMany” relationship and along with push (POST to relationship link) I want to add replace operation. What is approved way to go?


#2

Supporting actions with additional arguments is a complicated topic, and there’s not one strategy that works for all cases. (We have some open issues about this on Github if you’re curious.)

But, to answer your particular question about supporting a replace operation on a relationship, this actually already exists: if you send a PATCH to the relationship link, the "data" in that PATCH is linkage that totally replaces the relationship’s contents!


#3

I’m curious and want to learn possible workarounds. May be some samples on how people solve such cases. Examples of using meta.

PATCH works as long as you have short related collection and you have to find replaces yourself server side.

I’m considering POST into relationships’ link with “meta”. No idea how to friend all this with ember’s store, but soon I will have to deal with all this.


#4

@ruz I’m not sure I follow the question since you do not seem to be content with the previous answer. Are you looking to perform multiple actions within the context of a single request? Why would you look for a workaround on a feature that already exists? Are you bound to a POST operation for some reason?


#5

@dmcwherter PATCH == “set people to [X, B, C]”, not “replace A with X in people”.

Difference is big if you think about concurrent requests when multiple people replace at the same time. Take a look at the following:

start: [A, B, C]
C1: PATCH [X, B, C]
C2: PATCH [Z, B, C]
result:
  server: [Z, B, C]
  C1: [X, B, C]
  C2: [Z, B, C]

With replace operation:

start: [A, B, C]
C1: REPLACE A->X
C2: REPLACE A->Z
result:
  server: [X, B, C]
  C1: [X, B, C]
  C2: error or [X, B, C]  

C2 gets an error or success with C1’s version as there was no A in the collection by the time he sent his request.


#6

@ruz Concurrent requests are more difficult. The way we worked around this in our framework was to piggy-back on the JSON Patch Extension. All requests performed in this context occur in a single logical transaction. In many cases, this logical transaction translates to an actual DB transaction assuming it’s supported by the backing store and rollbacks (I’m looking at you redis).

In our framework, each operation within this request directly correlates to a direct JSON API request type, so your backend wouldn’t have to implement the same processing code in two places.


#7

@dmcwherter Also, want to note that DELETE+POST pair in two requests is not the same thing as replace action. Such way can become closer with bulk operations and special server treatment.

Will look at patch extension.


#8

@ruz I do recommend looking at patch; it does support replace! :smile:


#9

Ahh, I understand now… I misunderstood how you were using the word “replace” earlier, since the spec uses the term “full replacement” to describe “set people to [X, B, C]”.

I agree with @dmcwherter that the json patch extension can be used to handle this case. However, do remember that extensions are still unstable, and the patch extension in particular is likely to change (based on the discussion in #795). Therefore, it might be better to do this one action outside of JSON API for now. Simply POST to the relationship URI, but POST a media type other than JSON API’s. Then, you can format that request however is convenient. The response can even be JSON API.

So something like this:

POST /rooms/1/relationships/people
Content-Type: application/vnd.my.custom.type+json

{
  // you can define whatever body format you like here for replacement,
  // since you're not sending data with the JSON API content type
}

Then the response would be the standard JSON API response, but with the new linkage:

200 Ok
Content-Type: application/vnd.api+json

{
  "data": [
    /* new people resource identifier objects */
  ],
  "meta": {
    /* optional, you could put some information 
       in here about what was replaced */
  }
}

#10

Good we are all at the page now. What do you think about using JSONAPI’s meta rather than my custom content type?

POST /rooms/1/relationships/people
Content-Type: application/vnd.api+json

{
  "data": [
     { "type": "people", "id": "X", "meta": { "replace": {"type": "people", "id":"A"} }
  ]
}

I believe such usage is within the spec. Am I wrong?


#11

@ruz it looks like the meta block is defined outside of the “data” block. But even so, @ethanresnick may have a better opinion on this, but my strong feeling is that meta should be used sparingly (i.e. in the case of optional or additional data). It is, by definition, non-standard.

Where specified, a meta member can be used to include non-standard meta-information.

While certain specs may change (i.e. Json Patch Ext-- I still need to read through that linked discussion), keeping things as close to the standard as possible is important imo. Otherwise, your client and server implementations are tightly-coupled which negates much of the purpose for having a specification in the first place.

As a result, the suggestion to do this out-of-band looks more appealing to me as well.


#12

@ruz My interpretation of "meta" is also that it should be used very sparingly in general and, when used, its appropriate for including extra details. What you’re talking about here, though, is using meta to fundamentally change the behavior of the request… That seems like a bad idea to me.


#13

Hm. Can you point me at good uses of meta in requests with “resource identifier objects”?

I’m actually surprised that you decided to use PATCH on relationships to mean replace whole thing with this new list instead of using special format that can do various operations with list of “resource identifier objects”, something like:

PATCH /rooms/1/relationships/people
Content-Type: application/vnd.api+json

{
  "data": [
     { "op": "add", "value": { "type": "people", "id": "X" } },
     { "op": "delete", "value": {"type": "people", "id": "Y"} },
     { "op": "replace", "value": {"type": "people", "id": "Y"}, "with": {"type": "people", "A"} }
  ]
}

There is no road back…

Also, JSON Patch extension doesn’t solve my problem (“replace one object in a relationship with another object”) as JSON Patch uses JSON Pointer spec which has very limited support for array manipulations. This actually renders extension almost completely useless in working with hasMany relationships.

Combining multiple operations in one request sounds promising. As I would be able to pair up add+delete operations into replace, but still this would make my server side so much complicated.

I think it’s better to consider standardizing action URLs. What I mean here is that you would never cover complex actions in some declarative style of combining CRUD operations together. Even if you would accomplish that then it would be too complicated. I think it’s better to provide people with standard way of discovering (links) such complex actions and some well defined way to invoke them. I very much enjoying free form of page and filter arguments. This can be quite free too, for example declare how that POST should be used, that payload should be JSON object with “data” element, but don’t tell how data should look like. Let people experiment and play with data formats.


#14

I’m not sure there’s actually been a real-world need for it so far; "meta" was just added everywhere as an “escape hatch” before we had a real extension system. Here’s one hypothetical example, though: it could be used to store which user added the resource identifier object to the relationship (or when, or whatever).

Yup, unfortunately :wink:

Yup! I totally agree. What you’re looking for is templates/forms that tell the client how to formulate specific (POST) requests to trigger different actions. There’s already an issue for a limited version of this, and I think most/all of the editors are on board with the concept. It just hasn’t been a priority so far. I think the hope is that once the extension system is done the community will be able to design a solution for this!