Motivation:
An amazing hypermedia story for the JSON API (module) will provide a ladder for Drupal-based decoupled applications to reach a higher level of sophistication. This next generation of decoupled Drupal applications will have rich and domain-specific feature sets that go far beyond the typical "website". These applications are already beginning to be built today, but they require close coupling to the backend because their possible interactions cannot be communicated via an API in a generic and standardized way.
We first allowed presentation to be decoupled from data storage, now we need operations on that data to be decoupled from its presentation.
Background:
Recently, @e0ipso shared this link: http://gtramontina.com/h-factors/ which highlights JSON APIs poor hypermedia support. At the same time, I've been thinking a lot about hypermedia recently.
We've not even begun to scratch the surface for what excellent hypermedia controls could do for APIs built with JSON API. We're exploring that space a bit in #2795279: [PP-2] [META] Revisions support, but it's still a fairly static implementation not extensible by the applications built on top of this module.
The only control we already provide is fixed across all instances of JSON API and enables only one, well-defined use case. Pagination.
Means:
The next level of support that we can provide is to allow developers to define and customize their own links between JSON API resources. Every API is built for a different purpose and every API needs to serve specific business logic. To drive client-server interactions specific to those needs, our responses need to contain links specific to every application.
This will take time and will need to happen incrementally. Let's let this issue serve as a hub for those issue that improve JSON API's hypermedia developer story. I propose we begin writing this story of hypermedia support by building the infrastructure below and dog-fooding that infrastructure by migrating the 2.x version of revision support to it.
Components:
Things which I'm thinking about, most of which will probably become issues in the issue queue, in no particular order:
- Work with JSON API maintainers to get better link support/Use a JSON API profile to add better link support. While http://gtramontina.com/h-factors/ shows that the spec doesn't have great hypermedia support, it doesn't preclude it. The only real impediment that the spec places on us is that it limits link cardinality to 1 (although I've seen the spec maintainers speak optimistically about loosening this restriction).
- A custom URI scheme for our use.
- A customizable set of link relation types, defined under our own URI scheme.
- A customizable set of link attributes, defined under our own URI scheme.
- A link relation type for "anchor" links. When present, these links would use a JSON Pointer to link to other members of the response document. See #2993654: Provide JSON Pointer as a link on a resource to ease matching with included items
- A link relation type for adding resource identifiers to a relationship.
- A link relation type for removing resource identifiers from a relationship.
- A link relation type for replacing a relationship's resource identifiers.
- A link relation type for path aliases.
- The ability for other modules to add and remove links from various JSON API objects (to include resource, relationship and error objects as well as the top-level links object).
- A reference JavaScript client that understands these implementations.
Goals:
As a taste of what these kinds of enhancements could enable, here's a complex, juicy tidbit:
{
"type": "product",
"id": "1",
"links": {
"drupal://jsonapi/extensions/commerce/links/relation-types/add-to-cart": {
"href": "https://example.com/order/4/relationships/items",
"drupal://jsonapi/links/target-attributes/relationship-arity": 3,
"drupal://jsonapi/links/target-attributes/operation": "drupal://jsonapi/links/target-attributes/operations/add-to-relationship",
}
}
}
- The
drupal://jsonapi/extensions/commerce/links/relation-types/add-to-cart
URI is in reference to a custom link relation type that defines the semantics of the link as a URL for adding an item to one's own cart. - The
drupal://jsonapi/links/attributes/operation
URI is in reference to a custom link attribute that defines the link as a URL upon which a specification-defined operation can be performed. - The
drupal://jsonapi/links/target-attributes/operations/add-to-relationship
URI is in reference to a defined HTTP method and request format for adding a resource identifier to a relationship. - The
drupal://jsonapi/links/target-attributes/relationship-arity
URI is in reference to a custom link attribute that communicates the highest arity of any duplicates for the context resource object.
Another example might be drupal://jsonapi/links/target-attributes/operations/add-to-collection/schema
, drupal://core/forms/metadata
and drupal://core/forms/validator
for a link defining the schema for an acceptable request document for an operation, a link defining metadata that describes how a form should be rendered by a frontend application, and a link from which a JavaScript file for validating a form submission could be obtained (yes, it's possible!).
"But those URIs are so ugly!"
JSON API v1.1 will likely contain the ability to assign "aliases" to these URIs. Using those aliases, my example could become:
{
"type": "product",
"id": "1",
"links": {
"add-to-cart": {
"href": "https://example.com/order/4/relationships/items",
"relationship-arity": 3,
"operation": "add-to-relationship",
}
}
}
Comment | File | Size | Author |
---|---|---|---|
#12 | drupal.test_admin_content(iPad).png | 232.28 KB | gabesullice |
Comments
Comment #2
richgerdesI very much so like this and I am very happy to help move this along. Here are some initial thoughts.
+1, I agree and love the aliases, and unclear what the actual use of those urls are. I would argue that a well built api should be usable by a human (developer) as well. The complicated urls make it way harder to understand whats going on there. Granted you sometimes do need to provide more meta data in regards to this.
I just opened #2994211: [Meta] Expose Form Metadata via Schemata to track this concept which has been stewing in my brain since earlier this year when I talked about form with the HAX team. I think there is a lot of potential for this, especially as a successor to the current form ajax system which could come out of implementing form building between jsonapi and schemata.
I am very much for promoting a JavaScript client. As discussed at Decoupled Days, @gabesullice and I have both worked our own clients which understand jsonapi (and mine understands OpenAPI schema). If we can collaborate we should be able to do this well. Ultimately, I would want a client which doesn't depend on anything custom in our api. If we want to implement extensions to the spec, which can be "discovered" by the api, I think this needs to be done via something which is in (or almost in) the JSON API spec I think it would be good to work with the maintainers to outline this. Then as a result the client code should be able to work with any api service which provides the required information (such as OpenAPI spec for JSON API and the links to the api's extensions).
Comment #3
Wim LeersI haven't read anything here yet, I just came to 😂😂😂😂😂😂😂😂 for the title! 👏👏👏
Comment #4
gabesulliceI'm 100% with you on the API being "human-friendly". These aren't URLs they're URIs (i vs L). IOW, they're "Universal Resource Identifiers" and what they are is machine-readable, unique strings that identify codified meanings. Let'ts break one down...
drupal://jsonapi/extensions/commerce/links/relation-types/add-to-cart
This identifies a link relation type unique to Drupal, used by JSON API, but provided by the
commerce
module. Its name is "add-to-cart". If we had these, we would have a mechanism to discover and document the meaning of this link relation type. Its meaning would be "a link of this relation type is a link that tells the client how to add a resource to a user's cart". In my example, it would have a "human-friendly" alias ofadd-to-cart
. Let's try another...drupal://jsonapi/links/target-attributes/operation
This is a target attribute unique to Drupal, used by JSON API. It describes the link as one that provides an "operation" that the client can perform with the link.
drupal://jsonapi/links/target-attributes/operations/add-to-relationship
identifies that operation. That is, it denotes the procedure the client should perform. In this case, that the client should send aPOST
request to the givenhref
using the context resource ({"type": "post", "id": 1}
). These operations would be documented and could be "built in" with the JS client.See also: https://github.com/jsdrupal/drupal-admin-ui/issues/262
Amen. This is probably the lowest priority thing for me at the moment though. I don't think a client can be built that has all those automations until we've formalized the server side of things.
Comment #5
gabesulliceRenamed every instance of
action
tooperation
.Comment #6
gabesulliceAdded a child issue.
Comment #7
e0ipsoI'm sorry it's taking me so long to find the time to read this. I know I align with most principles that Gabe has shared about this in the past. I have been trying to timidly promote them myself, so I have confidence I'll be in line with most of this.
I am worried (before I read on) about the complexity this brings in. I'd like to float the idea to move this to a separate contrib.
Comment #8
gabesulliceDon't be sorry, it's FOSS after all :)
❤️
I see a lot of space here for other modules and so I guess we probably will agree on that sentiment.
Certain parts of this must live here though, like the core idea of adding/removing links from different places in the document in a thoughtful, spec-compliant and BC-safe way.
In my example, I showed an "add-to-cart" link. As I envision it, that link would not be generated by JSON API, it would be generated by something like a
commerce_jsonapi
ormy_store_customizations
module. To get that link into the response, that module would implement an event subscriber. And JSON API would be the one to fire the event. So, it would be JSON API's responsibility to aggregate and validate all those 3rd party links, but not to figure out which links to add/not add.For instance, I'd like JSON API to ensure that any link relation types are in fact valid ones. Or, if two 3rd party modules provide links with the same URI but different link relation types, I'd like to merge those two links into one link with both relation types on it.
Finally, certain core related links should also probably live in JSON API. Just like we generate the
self
link for all resources. This issue might mean that we expand on that a bit and add theedit
link relation type to theself
link if the user has permission to update that entity, etc.Comment #9
e0ipsoI'm still finding this troublesome. While I wholeheartedly agree with most of what you wrote in the IS (I have read it now) I am not ready to agree with your use of must in the quote above. There are many things that are done in JSON API Extras that don't happen in JSON API via extension of internal APIs. There is nothing that precludes us from using that same solution.
In fact, I think we should start developing this in JSON API Extras (or JSON API Links) and then evaluate if we want to move this to JSON API. I see no strong reason against this.
Comment #10
gabesulliceI'm fine with prototyping much of this in a separate contrib. However, that raises the question, what internal API can it extend? I think that's the must I was referring to. I think the "right" API for that is an event subscriber used internally.
I would prefer not to put this in Extras because I fear it's becoming the equivalent of REST UI in that it's practically a necessity (perhaps only perceived) rather than a pure enhancement or proving ground.
Comment #11
e0ipsoI don't see it's a problem that JSON API Extras is very relevant. In fact I see it as a great sign. I agree that Extras is essential to any jsonapi site.
Comment #12
gabesulliceI agree with all of this :) It is not a problem that JSON API Extras is very relevant. Full stop.
What I think is a problem is if JSON API is not useful without Extras, and it's essentially an implicit dependency.
How does that impact this issue?
Let's say that the Admin UI Initiative wants to to show an "Edit" button on the content overview page and that the button must only be shown if
hook_entity_access('update', $entity)
is allowed. It would be poor UX to show that button and when a user clicks it, to then go to a forbidden page (or worse yet, show an error on save). How can they communicate the access results of that hook to the frontend in an elegant way?The answer is of course hypermedia controls. Without them, they'll need to replicate access logic on the frontend or come up with some workaround that couples their implementation tightly to the backend.
Eventually, when their work is added to core, they'll only be able to rely on what's in core, where JSON API proposes to be. They won't be able to rely on JSON API Extras for that and so JSON API won't be useful to them in that scenario.
Comment #13
e0ipsoIt is useful, but not complete. This was like this from start by design. Adding one random piece (the hypermedia support) will not change this. Following that reasoning one could argue that we ought to merge FieldEnhancers into JSON API, otherwise the schema is wrong (yes, even with @DataType level normalizers) and that means that JSON API Extras is an implicit dependency.
I get that you are incredibly passionate about hypermedia controls, and I am convinced you'll do an incredible work on it. I don't think that JSON API is the right place to seed this work. I think it's totally possible that we'll move this in eventually.
This is not relevant to the disagreement, meaning that we both agree on the importance of this.
The Admin UI initiative is hardly a good representative of the use of the JSON API module.
We can talk at length about that. But to summarize, I have 0 problems merging this back into JSON API from JSON API Hypermedia when that's the last blocker for the Admin UI. Following that reasoning we'll need to get Schemata into core first. That'll be a challenge.
Comment #14
Wim LeersI share @e0ipso's concerns.
I think that @gabesullice is saying that today it's impossible to even implement this in a contrib module like JSON API Extras (regardless of whether this additional linkage should be added by JSON API Extras or some other module). We've made changes in JSON API to enable certain features in JSON API Extras before. I'm fine with doing the same for this linkage functionality. But I agree with @e0ipso's main point: the bulk of the linkage functionality (the API that allows other modules like Drupal Commerce to add additional links) ought not to live in the JSON API module.
Comment #15
gabesullice@e0ipso and I came to a compromise in chat, which I hope you'll also agree with. JSON API will have classes for "link collections" (the parallel to JSON APIs "links object") and a "web link" (a value object for a link mirroring RFC8288's conception of a link). The normalizer for the link collection can then be overridden in a separate contrib which provides the extension points that something like Drupal Commerce could use.
Comment #16
mglamanWhat if two contrib want to implement this? Or what if Drupal Commerce provides one for "core", and Shipping needs to provide way? Does this scale?
I just took a peak at \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue::rasterizeValue and saw
I was hoping that we could leverage link relation types (
core.link_relation_types.yml
) and be able to add link templates to entities, and it could be generated that way.So, for example, the Cart API would alter the Order entity definition and add
The order now links to the add to cart route automatically because we've defined it as a link. But, I guess the problem is "how do you know which links should be present, build them all and execute access checks?" That seems like a performance problem.
Comment #17
e0ipso#16 brings up the point of performance. When dealing with HATEOAS we incur in the danger to start adding stateful links to the response. If this happens it will impact cache hit ratios (and therefore performance) greatly.
Comment #18
gabesullice#16
@mglaman, I think you may have missed a subtle point:
I wasn't suggesting that modules all override the same normalizer. I was thinking that a module named
jsonapi_hypermedia
would override the normalizer once. From there, thejsonapi_hypermedia
would dispatch events that 3rd party modules would subscribe to (like Commerce for example).To help mitigate performance concerns, the event names could be specific to particular parts of the document and the events could carry a lot of contextual information so that subscribers can do as little processing as possible before escaping when they don't need to add links.
"Marking up" a document with solid linking will always have some trade-off between response size+speed and usefulness. It's up to the module maintainer adding links to be conscious of cacheing and it's up to the site builder to be conscious about what gets enabled.
#17:
It's not lost on me that adding support for hypermedia as the engine of application state means we'll be introducing stateful links ;). It's kind of tautological really.
I'm not sure what to conclude from that comment. I think you just mean that we should be mindful of this?
My thinking has always been that links need to be able to carry cacheable metadata, link providers need to be considerate of caching, and links should be optional feature of the response document.
Toggling links would be an excellent use case for negotiable profiles (they just landed in JSON API 1.1 last week). But before that, I think enabling or disabling
jsonapi_hypermedia
should be sufficient to get started with this.In any case, I don't think performance concerns are a reason to postpone or hesitate on this feature.
Comment #19
mglamanWhoops, I did.
Performance can always be fixed. To be honest, the lack of data in links now is a detriment to advanced use cases of the API where there are a lot of interactions in the application. When I use GatsbyJS to consume the JSON API output for an ecommerce app, I would love it if the data "just provided" the add to cart links for each variation once resolved.
Comment #20
e0ipsoTechnically you can have stateless responses that facilitate tracking state :-P, but yeah I know what you mean.
This will likely introduce cache contexts to the response, hence reducing cache hit ratios. This was my point.
You are correct, we can fix this later. We just need to remember to revisit this at some point.
Comment #21
Wim LeersHah, I was thinking the same thing after reading @e0ipso's #17 :)
Exactly!
These already exist. Pretty much every response already A) varies by the
user.permissions
cache context. Which other cache contexts do you fear would be added?Comment #22
Wim Leers#2995960: Add a Link and LinkCollection class to support RFC8288 web linking. landed! Which means the foundation for this now exists. @gabesullice already started experimenting with https://www.drupal.org/project/jsonapi_hypermedia, I think he'll be pushing that forward more soon.
I listened to https://www.drupaleasy.com/podcast/2018/12/drupaleasy-podcast-212-commer... a few days ago, where @bojanz and @mglaman go in some detail about their experience providing decoupled capabilities for https://www.drupal.org/project/commerce. I know that https://www.drupal.org/project/commerce_cart_api was also prototyped in JSON:API (at https://github.com/mglaman/commerce_cart_api/tree/jsonapi), but it was abandoned due to blockers. It'd be great to have the Commerce folks chime in and have a fast feedback loop :)
Comment #23
Wim LeersIn the
add-to-cart
example in the issue summary, how would the client know which HTTP method to use and what the appropriate body would be?The concrete example is a
product
that could be added to thecart
through thisadd-to-cart
state change. If we're using the existing API, that'd indeed requirePATCH
ing"href": "https://example.com/order/4/relationships/items"
(see https://jsonapi.org/format/#crud-updating-to-many-relationships for why that URI makes sense).AFAICT this means that the client needs to implement knowledge about this
add-to-relationship
operation, which would prescribe the use ofPATCH
plus a particularly formatted request body. Once the client implements that, it is effectively able to perform any state change that maps to adding to a relationship.But what about state changes that do not involve modifying connections between resources, but that are effectively aliases for
PATCH
requests to the current resource, to improve developer ergonomics?It seems a
change-attribute
operation could make sense — let's look at a hypothetical:And the Content Moderation module then could add additional links such as:
Does this fit with how you see this working?
What is reading material you would recommend, that led you to make this concrete proposal?
Comment #24
e0ipsoGiven that JSON:API's hypermedia support is so poor that we need to create a profile for it, should we model them after solid patterns like https://rawgit.com/uber-hypermedia/specification/master/uber-hypermedia....? That will help with the reinventing the wheel problem.
Comment #25
gabesulliceThis is why my proposal is mostly about link relations. It's the link relations that define the semantics of the link, which can encompass everything from the which methods are acceptable to what data to send.
Since I wrote the issue summary, my thinking has changed a little bit and the example is outdated.
I see the link being more like this:
The above is using my JSON:API links proposal.
We see that
add-to-cart
is no longer a link relation, just a string. Instead, the link has two link relations:https://jsonapi.org/profiles/drupal/actions/#add-to-relationship
drupal://jsonapi/extensions/commerce/links/relation-types/add-to-cart/
The first, is a link relation defined by a yet-to-be-written profile. It would be defined like so:
I didn't mention
arity
, just to keep it simple here.The
drupal://jsonapi/extensions/commerce/links/relation-types/add-to-cart/
relation would not be defined by a profile, but would reside in a namespace that lets any Drupal module define its own relations. We could define a docs page for these. Essentially, theadd-to-cart
would just "piggy-back" on theadd-to-relationship
relation. It's just there to disambiguate this particularadd-to-relationship
link from another one. It says, this link would add an item to a users cart. Maybe there would also be anadd-to-wishlist
link with exactly the same operational semantics (POST, etc.) but which has different meaning for this e-commerce site.You're spot on! I think the same
actions
profile could define a new link relation,https://jsonapi.org/profiles/drupal/actions/#update-attributes
:So, a
publish
link would be represented like so:If the client processed this link as I defined it above, it would know it must be a
PATCH
because that's what the spec defines for updating attributes. The context object isnode--article:42
, so it would know that's the data it should send. Finally, it would jsut directly set theattributes
member with the provided object. You'd get:Because of the way JSON:API only updates the fields that are provided in a PATCH request and leaves others unchanged, this is super powerful and simple for a client to implement.
Honestly, I haven't looked at that spec, so I can't answer completely.
In general, I think I want stay as faithful to RFC standards as possible. Using link relations and the web linking spec for as much as they'll give us. IMO, simplicity is the name of the game. I want to avoid creating (or using) anything that tries to do all the things, in favor of something that is leap forward in capability but also incrementally adoptable and familiar. IOW, it's better to have something with less features if it means more people will use it.
I realize that might read as me being super negative about UBER, but it's not meant to be! Like I said, I have not even read it. It might be exactly what I'm looking for; I'm just laying out a framework for evaluating any possible solutions :)
Comment #26
Wim LeersWRT Drupal Commerce + JSON:API: https://spree-guides.now.sh/api/v2/storefront/#operation/Completed%20Use... is an e-commerce platform providing JSON:API support.
Comment #27
gabesullice@Wim Leers asked me to summarize where this issue stands...
Since I wrote the issue summary, we've made lots of progress. Less than I would have hoped in the same time frame, but significant.
What happened related to hypermedia in the last six months (roughly, in no particular order)?
meta.errors
to be hypermedia focused.What's left/impacted?
JsonApiResource\Relationship
object. I envisioned the DX for this idea in this comment.Accept-Language
negotiation while using thehreflang
target attribute of resource objects'self
links. This would be very similar to #2992836: Provide links to resource versions (entity revisions).What are the obstacles I see?
Update: Added some missing issue links.
Comment #28
Wim LeersThanks, that's very helpful!
Comment #29
gabesulliceI added two new issues and updated the list of issues in #27.
Comment #30
Wim LeersComment #31
Wim Leers#3052931: Permit arrays as target attribute values in Drupal\jsonapi\JsonApiResource\Link landed!
Comment #32
Wim Leers@gabesullice, I believe that with the existence of https://www.drupal.org/project/jsonapi_hypermedia and even stable releases, this can now be considered "done"?
It'd be great if you could update this issue 🙏
Comment #33
mglamanShould this remain open to most child issues which improve Hypermedia support in core?
Otherwise I would say close and document the contrib module.
Comment #34
Wim LeersExactly. But I'd like @gabesullice to follow up on all of the unclosed related issues. We don't want to leave loose ends.
Comment #35
gabesulliceUpdated #2984628-11: Consider how to communicate allowed methods on resource object self links.
Comment #36
gabesulliceClosed #3014704-13: Expose API to allow links handling for entities from other modules
Comment #37
gabesulliceI think this issue has served its purpose and marking it "Fixed". Although I don't think the hypermedia story is completely fleshed out yet, this issue is no longer needed to plan/sequentialize/track fledgling hypermedia issues.
In the long term, JSON:API Hypermedia should be merged into the Drupal core JSON:API module to to finalize "the hypermedia story". I think that avenue is open to us. The PHP API is being expanded and the APIs pioneered by JSON:API Hypermedia are being validated by various modules in the ecosystem (see JSON:API Comment for one example).