Problem / Motiviation
To support loading revisions and to comply with the JSON specification for feature pertaining to resource versions, the jsonapi will "negotiate" the Drupal revision id value for a requested entity, allowing a URL to request an entity revision using a query parameter with a value conforming to a specific structure.
Proposed Resolution
Create a Drupal plugin that will provide the negotiation of revision id values using data submitted to a json api route in a new query parameter. The query parameter value will take the form {negotiator}:{value}, where type is a Drupal plugin id for a revision id negotiator plugin and value is a supported version value, beginning with one of working-copy, latest-version, or a Drupal revision id value.
# Assuming a page node loaded via the json api: jsonapi/node/page/{uuid}
# Load the working copy of the the page node
GET /jsonapi/node/page/{uuid}?resource_version=rel:working-copy
# Load the latest version of the the page node
GET /jsonapi/node/page/{uuid}?resource_version=rel:latest-version
# Load a version using a specific Drupal revision id value.
GET /jsonapi/node/page/{uuid}?resource_version=id:2
| Comment | File | Size | Author |
|---|---|---|---|
| #289 | 2992833--revision-id-negotiation--289.patch | 22.42 KB | barbun |
| #286 | 2992833-286.patch | 115.75 KB | wim leers |
| #286 | interdiff.txt | 4.39 KB | wim leers |
| #284 | 2992833-284.patch | 110.66 KB | gabesullice |
| #284 | interdiff.txt | 1.89 KB | gabesullice |
Comments
Comment #2
justageek commentedComment #3
justageek commentedComment #4
wim leersThanks for filing this! This would be the first and only PHP API that JSON API has. (See
jsonapi.api.php.) If we add this API, then it'd definitely have to be marked@internal. At least initially.That still would allow you on projects to add your own plugins. But the JSON API module would have no responsibility to retain BC for this PHP API.
Comment #5
gabesulliceI've started experimenting with this locally and have something that could apply.
Questions that I ran into while doing so:
content_moderationinstalled and referencing the definitions given in RFC 5829, I think these are one and the same.selflink be for a resource? Should it include the revision ID negotiator? Perhaps only when it is not the default revision.Re: the proposed solution:
Can we change
current_versiontoresource_version?Can we not make this a plugin right now? Rather, let's have code that will make it easy to add different negotiators tomorrow, but hardcode this for now.
Finally, must this be a 1.x issue? This certainly feels like a 2.1 kind of thing. If not, I think we'll need parallel development tracks for 1.x and 2.x. I don't know that the 1.x version should then ever be committed, but instead just be a patch that will continue to apply since the 1.x is not receiving anything but critical support from at least 2 of the 3 most active maintainers.
Most importantly! Thanks for opening an issue! I'm super excited to see this progress!
Comment #6
gabesulliceHere's that code if it'll help. I got this started off 2.x and haven't tried it on 1.x.
UPDATE: I hadn't seen #2992836: Provide links to resource versions (entity revisions) when I posted this. This patch includes that feature but it's easily extracted.
Comment #7
e0ipso🙃
Comment #8
e0ipsoThis is what we plan on running for our codebase based on 1.x. Reviews will be greatly appreciated.
My current plan is to release and support this feature in 1.x, unless there is a strong reason not to.
Comment #10
e0ipsoLet's see how many of those we fix with these simple updates.
Comment #11
e0ipsoComment #12
e0ipsoThis patch negotiates the revision on a param enhancer. This is inspired on @gabesullice's patch in #6.
PROS of this approach: It's a very well scoped service that responds to URL input and modifies a parameter that will be used later on. It loads the revision for all places that it may be used now and the future.
CONS of this approach: It is a bit hacky to replace the value of a route parameter in the route. It uses a parameter enhancer when there is not a route parameter (we have a query string parameter).
Overall I like the new approach, but I'm willing to switch back to #10 if anyone has concerns about this.
Comment #13
e0ipsoFixed DCS.
Comment #14
e0ipsoThis adds support for collections when asking for
rel:latest-version. This even works with filtering on future revisions thanks to Entity Query API!Comment #16
e0ipsoSome additional minor fixes.
Comment #18
e0ipsoMaybe this will fix the tests.
Comment #20
wim leers#12: thanks for explaining PROs and CONs! I was going to ask about collection support, and how the two approaches compare when you add support for that.
But I see that you added that in #14, splendid! 🤘
Attached is a small contribution towards making this pass tests. 😊
Comment #21
wim leersActually, I'd still like to get your thoughts on this.
In the case of collections, we need to perform an entity query, and hence in that scenario, the param enhancer does not help us at all.
Initial review:
"versionable" and "revisionable" are being mixed up many times here.
If we want "version" to be Drupal-agnostic, that's fine by me. But then we need to very clearly document that, and be consistent about it.
VersionById
VersionByRel
This looks quite nice!
This is insufficient. This needs to call
\Drupal\Core\Entity\EntityTypeInterface::isRevisionable()instead. See the docs for\Drupal\Core\Entity\RevisionableInterface.That call to
isRevisionable()is essential, glad to see it here!(And yes, that's another Entity API WTF, but alas, nothing we can fix… 🙃)
Either
VersionNegotiationorRevisionNegotiation.Right now, this seems to be related to the
IdNegotiationclass. Very confusing.Why not make
getRevisionId()the plugin interface/API? Then we don't need this logic at all. Then this base class can disappear!If there's a reason to have this logic here, I'd love to learn more :)
Use
\Drupal\jsonapi\Routing\Routes::getResourceTypeNameFromParameters()instead.👏
This is probably a force of habit, so probably this is here unintentionally.
I'd really like this to not have an alter hook. To not create API surface.
I … don't like this, but let's wait and see if it disappears automatically as this patch gets improved :)
Comment #22
wim leersTwo general questions for now:
version-historyComment #23
justageek commentedI have what I think are the remaining tests passing locally, I will likely not get this into the patch until next week. I had already added the call to ::isRevisionable() as part of the solution to the failing tests, so glad it is already rolled into the code / patch. Most of the remaining failures were differences in the structure of the json api output for entity types that are revisionable vs those that are not.
I can incorporate updates from the review and comments in #21, let just me know when those are agreed upon.
Comment #24
e0ipsoI think that may be OK since in collections we can only support a very limited array of negotiation options. So it makes sense for it to be its own thing. But I don't feel attached to any solution.
Addressing feedback from #21
1. That was semi-intentional. Throwing the terms at the wall to see what sticks. I think I like "versionable" a bit better. You?
2. 👌🏽
3. 👌🏽
4. 🙂
5. Ah, yest. We're are doing it in the
isVersionablelogic but missed it here. Good catch.6. Yup.
7. I'm going with
VersionNegotiation.8. We had that prior to #14. Since we will need to load the revision we can either do it in the plugin or as the immediate statement after getting the revision ID. If we do it here we simplify the calling code since we don't have to inject entity type manager everywhere we inject the negotiator plugin manager.
9. 👌🏽
10. Fast errors FTW.
11. Agreed. Let's remove it.
12. I found myself repeating this code in several places and I wanted to abstract into a static method. Do you have suggestions on where to put this? Maybe rename to
throwBadRevisionInput?I think that GET is a good place to start. My gut feeling is that we'll be able to use the same qs negotiation for mutations. I don't see an obvious problem, but I haven't thought about it much.
For now all the version discovery happens via hypermedia links. However this version-history implies the creation of a new endpoint. I don't think we are ready to tackle that. Maybe a follow up? In any case I think this could follow the same structure as our entry point.
Comment #25
wim leersTo me, it feels like a
collection-like endpoint, that does not return resource objects, but resource identifier objects.Having written that, it seems like
version-historyought to be a virtual relationship on any versionable resource type? What do you think?P.S.: d'oh, I wanted to remove my points 2 and 3 because I wasn't entirely sure about them. But I forgot.
Comment #26
garphyI just noticed that when using
IdNegotiation, you're able to request the revision of another entity that the one your pointing at, because there's no validation that the passed revision id is indeed part of the requested entity revision history.I'll try to write a test case which exhibits this behavior.
Comment #27
garphyI added initial test coverage for
IdNegotiation&RelNegotiationand also added a fix to prevent the use of a revision id of another entity.Comment #29
garphyForgot to make my test base class abstract.
Also fixed some code styling issues reported by the bot.
Comment #30
wim leersWoah, great find! 🤯 And one with security implications too … Thanks for adding test coverage! Will review in more detail tomorrow.
Comment #31
e0ipsoI agree that this fixes some DX, but I don't see the security implications? While serial IDs are frowned upon because they allow for discoverability of unlisted content, that can hardly be considered insecure (otherwise REST core would be in trouble).
@garphy thanks for the contribution!
Comment #32
e0ipsoIn case anyone else is curious, this is the interdiff for the patch above.
Comment #33
e0ipsoFixes some DCS.
Comment #34
e0ipsoThis addresses @Wim Leers' feedback in #21. This is mostly renaming stuff. Let's settle with these new names now.
Comment #36
e0ipsoSome renaming woes. I'm expecting more.
Comment #38
wim leers#31: If entity access checks happen first and then you end up loading a revision … but it's of a different entity, then you would've bypassed entity access!
Comment #39
garphyI also wondered about security implications of this "bug" but I don't think that the entity access would be bypassed because all of this happen in a route enhancer, thus the wrongly selected entity would be subsequently checked by JSON API request controller afterwards (unless I'm mistaken here).
Everything here rely upon $entity->access() which is called in EntityResource::getIndividual(), after the revision id is resolved.
Comment #40
gabesulliceI disagree. For now, I would like to keep version navigation as a hypermedia-only thing and not try to confuse it with resource fields. That gives us a lot of flexibility and makes it easier to write a profile extension for.
That would be a "relationship-like" route. Again, I have to disagree. I do believe this should be a collection of fully fledged resource objects, w/ support for sparse field sets (and perhaps includes after a v1). This would be far more useful for a revision history type view in a client and diff illustrations.
If you're thinking of
version-historyas a relationship field, I understand why resource identifiers would feel natural. However, I think making it a relationship field gets us into a conceptually weird recursive self-referential mess and would behave very bizarrely with includes (even though it's "virtual").A relationship-like endpoint would need to consider how to deal with a PATCH and POST (it would be 404 of course, but considering what it would mean illustrates why it doesn't "fit").
My 3 initial questions in #5 are still open:
content_moderationinstalled and using the definitions given in RFC 5829, I thinkworking-copyandlatest-versionshould actually be the same thing.selflink be for a resource? Should it include the revision ID negotiator? Perhaps only when it is not the default revision.Comment #41
e0ipsoI'd like to descope
version-historyfor now. It's unclear atm and I believe that it will require a new endpoint. I'll open up a new issue to discuss it. Please post your thoughts there.The working copy is the default revision.
Agreed. This is the current behavior.
It should reflect the currently loaded resource, hence it should it include the revision ID negotiator. I concur that we should not add the revision dimension if it's the default.
This is not currently the case, so I'll follow up.
Comment #42
wim leersversion-historyI … am not sure how that would work, given that revisions aren't linear: they're a graph. A graph that can't be reconstructed even. You can't even do
previousandnextlinks.And yes, you're right in :) You convince me that this is a bad idea when you say that
It seems you'd both like to descope it, but shouldn't we first have an idea of how to achieve that while achieving the other things, to ensure we don't make it impossible to fit in later?
My concern is that we make it possible to load a specific revision by ID, but how can you even figure out what that revision ID would be without having some listing of available revisions, which
version-historywould provide?I'd really appreciate it if either or both of you could share slightly more of your thoughts about how you think this ought to work. That would probably address my concerns about descoping that to a separate issue ☺️🙏
The #5 questions
Asked at the bottom of #40, answered in #41: that all sounds great! 🤘
Comment #43
garphyHow so ? I thought it was pretty linear. AFAIK, we don't store the "parent" revision of a given revision so we can't really build that graph. Unless I'm mistaken, revisions are only list.
Comment #44
gabesulliceThis isn't the first time you've said that, like @garphy, I'm a little confused though. I don't know that to be true, even with
content_moderationinstalled. Could you explain how they're a DAG? AFAICT, revisions form a linear history. Certain revisions are marked "default" and with CM installed, the latest version can be in a different state. IOW, there's no concept of merging or forking.This comes back to the linear history thing... if it is a linear history, then it's the
self,predecessor-versionandsuccessor-versionlinks that would reveal the revision ID specific links. Also,selflinks alone need them.Regardless, even a DAG would have
predecessor-versionandsuccessor-versionlinks (plural). RFC5829§3.5 even takes this into account specifically: "Some systems may allow more than one of these link relations in order to support branching."I didn't say that ;) But I agree with Mateu that it doesn't need to be in the patch, but I agree with you that we need to understand how it will work whether or not we implement it. I don't see the point of moving the discussion to a different issue though.
I disagree. It's easy to confuse when
latest-versionandworking-copyare the same thing though. So what's my reasoning?New revisions always proceed from the tip of the version history. So, I think the "working-copy" is the revision with the highest revision ID.
OTOH, "the latest version is defined by the system." Which makes me think that
latest-versionshould be the default version.Here is how I think about it:
Something subtle that I suggested in #2795279-29: [PP-2] [META] Revisions support that I don't want to lose is the idea of "chained"
relnegotiators. I gave the example of/jsonapi/node/article?resource_version=rel:predecessor-version:working-copy. You can see how those would work and be different in my example above. Perhaps we need a special delimiter for chained rels.I don't want to lose that ^ but it doesn't need to be implemented here. We just shouldn't do anything to preclude it.
Comment #45
justageek commentedI have added fixes for the failing functional tests, I was not able to create the interdiff before I headed for the airport, my apologies.
Comment #46
justageek commentedFound and fixed issues with last patch, this is the latest.
Comment #47
gabesulliceX-posting this from IRC to the issue. This is a refinement of #44.
The only missing link relation type is then
successor-copyfor forward navigation in a "working history" as there issuccessor-versionfor the "version history". I don't think that's terribly important though and we can live without it.Comment #48
wim leersCorrect, we don't store a reference to the "parent" revision.
Time to dig in and figure out if I'm misremembering correctly. It definitely sounds like that's the case.
I think I may be confusing what's currently implemented with what the Workflow Initiative plans to do. See #2786133: WI: Phase B: Extend the revision API with support for parents and specifically #2725523: Add a revision_parent field to revisionable entities. "nonlinear revisions" is explicitly mentioned in #2721129: Workflow Initiative for phase B. So yeah … I'm betting that's why I was convinced that revisions were a DAG, sorry!
I found beautiful confirmations of intended linearness in the UI improvement proposals at #1776796-94: Provide a better UX for creating, editing & managing draft revisions., even though that's 6 years old :P
So, yes, you guys are right, revisions are linear. But only in the sense that each revision is created at a certain time. Because if you restore an older revision, an older revisions suddenly reappears, even though it is not actually a successor to the revision that precedes it timewise. So our revisions are "linear but not really". Linear timewise, not linear contentwise. Of course, we lack the necessary metadata to track revision history *contentwise*. That's exactly what the Workflow Initiative aims to fix.
In doing that research, I also identified the following issues as impacting this feature at some point:
… since all of that is planned revision-related work in Drupal core
Comment #49
e0ipsoFrom what I see in the patch in #46 that @justageek merged #2992833: Add a version negotiation to revisionable resource types in here. I'll try to separate the two again.
Comment #50
e0ipsoThis fixes the tests from #36. More updates to come.
Comment #51
e0ipsoI'm not ignoring this. I'm just putting that thought on pause. I think we may need to define that feature a bit better. For instance you may want to request:
Note the cross-negotiators interaction. While it is unlikely that this level of flexibility will be useful and pragmatic, I think that with the current implementation (modular and pluggable) makes it "easy" to implement.
Comment #52
e0ipsoThis patch changes the nomenclature according to #47. It also adds support for
working-copy-ofandpredecessor-versionbut throws a cacheable 501 (Not Implemented) for now. We should address that in a follow up.I still think that
default revision == rel:latest-versionandlatest revision == rel:working-copywill be a huge source of confusion. However, at this point I'm willing to make this concession to bring this patch closer to merge.Comment #53
wim leersWoah, fascinating! 👌
That's fine by me. Is @e0ipso also +1?
Wow, this is very very confusing!
It doesn't help that https://tools.ietf.org/html/rfc5829#section-3.2 says , while Drupal has both
QueryInterface::currentRevision()andQueryInterface::latestRevision()and the docs for the latter say .This means that also in the case of Drupal, just like in the case of RFC5829,
latestis considered a level of indirection whose behavior follows certain rules (and could be customized).As far as I can tell, that last emphasized statement () maps 1:1 to concepts in RFC5829. Especially if we factor in the concept of
working-copy, which is something that cannot exist in "plain" Drupal, without Content Moderation. Forworking-copyto make sense, you at least need to distinguish between "draft" vs "live", which you cannot do in Drupal without Content Moderation.canonicalworking-copylatest-versionworking-copy-ofpredecessor-versionsuccessor-versionworking-copy==latest-revision. If there is *no* pending revision, thenworking-copy==canonical.Drupal has no direct equivalent for
working-copy-of. This can only be reliably implemented once #2725523: Add a revision_parent field to revisionable entities is done. Because when you restore a prior revision in Drupal, it bears no relation at all to the revision directly preceding it in time or revision ID. Note that once this exists,predecessor-versionandsuccessor-versiontechnically also need to change their behavior. Which is why I would err on the side of caution and omit those too (This is why I was saying #42 that you can't even reliably do previous/next links.)canonicalandlatest-versionworking-copyworking-copy-ofpredecessor-versionsuccessor-versionThe one thing that's in my proposal that hasn't been mentioned before on this issue, is the notion of using
canonicalto link to the default/current/live version.I also think it's essential that
latest-versionalways points to the actual latest, no matter if it's pending/draft (non-default) or default (current/live).Looking forward to your feedback :)
Comment #54
e0ipsoThat's a very different interpretation from what @gabesullice suggests. I honestly don't care what we end up deciding, although I have a slight preference. I only care about the feature being there. In other words, whatever interpretation we go with we should be able to load the revision we want with relations that point to current revision and latest revision.
However, I would love to get everyone on the same page relatively quickly if that is possible.
For the sake of discussion let's keep the 2 scenarios in #53 (with and without content moderation). And let's have in mind the revision history in #47.
Also, when we refer to revisions in Drupal lets talk about:
aaf=ef=ddhMaybe we can have a table of the relation types in RFC5829 according to both interpretations.
Comment #55
wim leersI don't think it's very different. I think there is a significant risk of future BC breaks if we go with #44/#47.
Thanks for outlining the definitions. That's in line with what #47 says. But it results in two version histories. How do we plan to have two
version-historyexposed? The definition titles in #54 clearly show that there are two parallel version histories: there's "first/previous/next/last revision" and "first/previous/next/last DEFAULT revision", plus one special case: current default revision, which is effectively the live revision.But RFC5829 clearly says:
IOW: a single history.
I hope you're seeing that I'm concerned about going in a direction that will prevent us from exposing certain things in the future, because we go with a less-than-ideal mapping of RFC5829 concepts onto Drupal's implementation, which then means we can't improve that mapping in the future, which means we'll have to forever support this pattern.
I don't even have a strong opinion about any of this! I'm merely concerned.
I wanted to start doing this, but there are a few obstacles:
his aworking-copy… but ford, probablye,f,gandhare all working copies! (The spec literally says there can be multiple working copies.) I say "probably", because actually we don't know ifeis a working copy started fromd— it could also have been started froma,borc. This is why we need #2725523: Add a revision_parent field to revisionable entities forworking-copy-of.ewas started from. Which is why I don't think we can implementpredecessor-versionorsuccessor-version. I would be okay withpreviousandnext, because e.g.previous. We do have a single ordered list of all revisions, regardless of the actual predecessor version of each revision: based on revision ID we have an ordered listing, and hencepreviousandnextare fine.So, let me refine what I wrote in #53 (now there is no distinction between with/without Content Moderation anymore), and apply it to the diagram from #47:
canonicaldlatest-versionhprevioush→g, …,b→anexta→b, …,g→hworking-copyd→e,d→f,d→gandd→h.Perhaps also
a→e,a→f,a→ganda→h?Note that once #2725523 lands, we'd probably want to limit it to those "next" revisions whose parent traces back to a given revision, which is technically also a BC break, but a very soft one.
Finally: if we want to be truly cautious about BC breaks, we'll want to not add
working-copyuntil after #2725523, unless we know what its upgrade path will look like and we can match that behavior. See #2725523-12: Add a revision_parent field to revisionable entities.working-copy-of(see concerns above: need to know parent)predecessor-version(see concerns above: need to know parent)successor-version(see concerns above: need to know parent)latest-version==canonical. If the latest revision is not a default revision, it must be a working copy, and hencelatest-version== lastworking-copy. "latest" just always means "highest revision ID/highest revision timestamp".P.S.: I left a comment at #2725523-10: Add a revision_parent field to revisionable entities to indicate that that is probably a blocker to this.
Comment #56
wim leersPer #2725523-13: Add a revision_parent field to revisionable entities, we actually do know the strategy that will be used. Which means we can do
working-copy,working-copy-of,predecessor-versionandsuccessor-versiontoday!That means that
previous==predecessor-versionandnext==successor-version, until #2725523: Add a revision_parent field to revisionable entities lands. Once it lands, they'll start to diverge for any revision created after the site updates to a Drupal core version that contains #2725523, which means there will NOT be a BC break.canonicaldlatest-versionhprevioush→g, …,b→anexta→b, …,g→hworking-copyd→e,d→f,d→gandd→h. i.e. all descendantsPerhaps also
a→e,a→f,a→ganda→h?Note that once #2725523 lands, we'd probably want to limit it to those "next" revisions whose parent traces back to a given revision, which is technically also a BC break, but a very soft one.
Finally: if we want to be truly cautious about BC breaks, we'll want to not add
working-copyuntil after #2725523, unless we know what its upgrade path will look like and we can match that behavior. See #2725523-12: Add a revision_parent field to revisionable entities.previousAfter #2725523: Parent revisions
working-copy-ofworking-copyAfter #2725523: assuming
ewas created by reverting toc:e→ce→b,e→aaka all ancestors.previousAfter #2725523: First parent revision (i.e. mother/father, not grandmother/father)
predecessor-versionpreviousAfter #2725523: assuming
ewas created by reverting toc:e→cnextAfter #2725523, Child (not grandchild)
successor-versionnextAfter #2725523: assuming
ewas created by reverting toc:c→elatest-version==canonical. If the latest revision is not a default revision, it must be a working copy, and hencelatest-version== lastworking-copy. "latest" just always means "highest revision ID/highest revision timestamp".Comment #57
gabesullice👍We're on the same page :)
I don't disagree with that. We'll need to communicate this well and consider the DX. A target attribute could help. I'm consoled by the fact that I think it's simple to make the point that
rel:latest-versiongets the latest "live" revision andrel:working-copygets the latest draft revision. "draft" sounds a lot like "working-copy".I completely agree.
I think that a distinction that I made implicitly but should have called out is that I treat the word "version" to be distinct from and not synonymous with "revision". A "version" in the RFC language is a "revision" that was/is a default.
All my examples presume the "draft" vs "live" distinction. Let's not forget that Content Moderation is stable in core.
I'd like to float the idea that JSON API revision support should require that CM be enabled. Or, at the very least, that without CM enabled we only provide a very spartan implementation, e.g. only having the
idnegotiator and aversion-historycollection, specifically excluding theX-versionandworking-copylinks and therelnegotiator from use. I see very little practical benefit to this feature without CM installed. IOW, I think providing a content preview mechanism is far more valuable than a mechanism for displaying a revision log. And preview is only possible with CM enabled.@e0ipso I think you'd agree with the relative priority there? ^
I think this interpretation goes back to my assumption that
version != revision. I thinklatest-version = isDefaultRevision() = TRUEandworking-copy = isPendingRevision() = TRUE.Why do I think
version != revision? Let me pull some quotes:Each of these examples seem to indicate that a "working copy" is not a version. Then what is it? It must be a draft. With CM, we have drafts. Drafts are pending revisions. All versions are revisions, but not all revisions are versions.
To call it out even more strongly: "A "checkin" is an operation on a working copy that creates a new version of its corresponding versioned resource."
"[A working copy's] corresponding versioned resource" really strongly indicates that a working copy is a derivative of a version and not a version itself.
I think it's highly informative that RFC 5829 makes no reference to
canonical. To me, the need for thecanonicallink to identifydmakes me a little skeptical of that interpretation. If it were necessary part, it would have been mentioned at least in contrast.With my interpretation, we can identify every significant revision with only the link relation types mentioned. The ones we struggle with are prior revisions that were not default (
b, c, e, f & g). These are the least significant in the system and would be identifiable with aparent-revisionlink relation type. I.e., we'd still need to have an link relation type outside the scope of the spec, but it's still very useful without it.To summarize:
All revisions are not versions. If not, then which revisions are versions? Those that were/are default. What then is a revision that was/is not default? It is a revision that is/was a "working copy". What is a working-copy-of? The working copy's "corresponding versioned resource." IOW. the last revision that was a default revision in that revision's ancestry.
What is missing from that summary?
@Wim Leers and my own interpretations have both identified a need for forward and backward revision navigation and additional link relation types. I think the problem is that the spec was not written to account for multiple revisions of a draft. To fill that gap, I propose we have our own links then:
parent-revisionandchild-revisionin future. This is an additive addition that leaves a very functional implementation using only spec-defined relation types (withoutcanonical#56 is not very functional).What about
version-history? I think it's entirely reasonable to include revisions that are not versioned in the version history. The "versioned" revisions will be identifiable by their links. For example,selfwill be equal tolatest-versionorselfwill be equal toworking-copy. I don't think it's necessary to have separate collections for a "default" history and another collection for a different history. It can be a superset of just the official "versioned" revisions that includes "working copy" revisions.Comment #58
e0ipso👏🏽 @gabesullice. As I pointed out the other day in IRC what it makes more sense to me is the correspondence of "version" with "default revision" and "working copy" with "in-progress draft revision". I'm glad to see your articulation of it here.
I am ready to buy #57.
Random thoughts:
To put things in the language of the spec, a "check in" operation would be
(∅ | Draft) ⇒ Published. A "check out" would bePublished ⇒ (Archived | Draft). With Content Moderation we can create a new version by either checking out the version (hence making a working-copy) and then checking it back in (by publishing it). We can also create a new version by updating the content entity keeping it published (this workflow is also explicitly mentioned in the spec).While I buy most of #57 I'm not ready for this. For me it brings down all the cohesiveness of the terminology.
version-historyis a history of versions. If versions are "default revisions" then I don't think it's entirely reasonable to include working copies there. Because working-copies != versions according to #57.I think it makes sense to have a watered down versioning negotiation & discovery feature if CM is not enabled. However, I think that having our code paths split depending on other modules being enabled will be hard to maintain. If the
VersionByRelwas provided by Content Moderation (or we mocked that somehow in JSON API), I'd be open to that solution. In any case I'd like to see a very clear separation for the negotiation and linking.FWIW the patch in #52 is compatible with the proposal in #57.
Comment #59
gabesulliceI think I can convince you with one question: Is a draft not part of a version's history?
Fine with me.
Agreed.
I'm open to that too :) Let's see what it looks like. Perhaps we can put it under
modules/content_moderationwithout aninfo.ymland force the plugin manager to discover it if the realcontent_moderationis enabled.This x 💯. I was really pleased to see that you even made two separate issues for this :) I honestly can't articulate why this should be, but my spidey-sense tells me that it's important :P
Comment #60
wim leers#57:
We had a call yesterday evening where we discussed our interpretations of RFC5829 and how its concepts map to Drupal. This was indeed a key thing that came up!
I think RFC5829 basically intends that the different working copies, which can be created off one another, do NOT have an accessible version history. They're "ephemeral" as far as the spec is concerned.
I … am not sure I agree with that. Drupal has not had Content Moderation for >1.5 decades and got by just fine.I got stuck on the next step of what I was going to write. I tried using the revision functionality on a D8 site without Content Moderation, and concluded that this proposal of yours makes perfect sense :)Thanks for extracting those specific examples. Consider me convinced that this is indeed the intention of the RFC authors. — if only the RFC authors had included this in their RFC! 😞 Would have saved us a lot of time!
Fair.
I'm still concerned that this means that it's going to be impossible to create a client that talks to JSON API and is able to provide a complete authoring experience, because without access to all revisions (Drupal terminology) or working copies (RFC5829 terminology), it cannot be considered complete.
This is in part why I never even would have considered that perhaps "working copies" are not "versions": because then how could the "version history" be considered complete?!
So that's my key question then: how do you propose we handle that?
👍 I think this correctly interprets the spec. My one concern still stands though.
EDIT: Perhaps I do still see one bit of ambiguity: does
dhave aworking-copyrelation tocor toe? I could see it argued either way… because the question is whether it means "this version originates from this working copy" or "there is a working copy initiated from this version"?)Did you mean version instead of revision?
#58:
Exactly this! ➕➕➕
Agreed with keeping "maintainability" very high in the list of priorities. I'm not yet sure that it'll necessarily be very complex though: to know that for certain, I'd need to see a patch that does it.
#59:
But …
version-historyis intended to list all versions of a resource. And the spec defines "versions" to be different from "working copies". So to then include working copies in the version history seems contradictory.AFAICT there are two remaining question:
version-historybut @e0ipso and I feel that this is contradictory/undermines cohesiveness.dhave aworking-copyrelation tocor toe? I could see it argued either way… because the question is whether it means "this version originates from this working copy" or "there is a working copy initiated from this version"?)Comment #61
wim leersTo really grok this, I think we need to understand the RFC authors' point of view better. Quoting RFC5829:
RFC3253 is WebDAV.
JSR-283 is the spec for the Java Content Repository, which contains https://docs.adobe.com/content/docs/en/spec/jcr/2.0/3_Repository_Model.h.... I'm 99% certain that RFC5829 was written not with multiple systems in mind, but only JCR. That is … a faux pas. But here we are. All terminology in RFC5829 is identical in here: check in, check out, predecessor, successor, version history — but it's only a subset: "working copy" is missing.
CMIS is the "Content Management Interoperability Services (CMIS)" 1.0 spec: https://docs.oasis-open.org/cmis/CMIS/v1.0/cmis-spec-v1.0.html — it contains a different subset more of the terminology: check in, check out, version history, working copy (no "predecessor" or "successor" this time). This spec dates back to 2010, was updated in 2011 with errata. And in 2015, a 1.1 version was released: https://docs.oasis-open.org/cmis/CMIS/v1.1/CMIS-v1.1.html.
Of specific interest to me is how they define "working copy" (given my second remaining question in #60). https://docs.oasis-open.org/cmis/CMIS/v1.0/cs01/cmis-spec-v1.0.html#_Toc... explains their "Version Series" concept:
Interestingly, https://docs.oasis-open.org/cmis/CMIS/v1.0/cs01/cmis-spec-v1.0.html#_Toc... says:
This literally says there can only ever be ONE working copy per document/resource/entity! 😂
Thanks, CMIS… 😭 … for imposing your limited world view on HTTP link relation RFCs that aim to be generic.
I dug into the CMIS and JSR-283 specs to figure out an answer to #60.2. In doing so, I essentially managed to answer both questions.
Everything indicates that RFC5829 was proposed by CMIS, and to achieve a semblance of broad market support, they also involved JCR authors. A quick search leads to https://en.wikipedia.org/wiki/Content_Management_Interoperability_Servic..., which points to for example its document-centricness. Which is a similar problem as page-centric CMSes. Seen in that light, it's understandable that they chose to KISS and have only a single working copy.
In conclusion,
working-copyin RFC5829 == "private working copy" in CMIS1.0 == a single forward/pending revision in Drupal that all authors must edit at the same time. Except that in RFC5829, multiple of these are allowed, yet they're not allowed to show up in the version history. Seemingly because neither CMIS nor RFC5829 consider it important that working copies (drafts) are listable/navigatable, because they're ephemeral.Comment #62
wim leersBased on #61, I'd say it is impossible to perfectly map RFC5829 to Drupal's capabilities. We just have to make an informed choice.
Comment #63
gabesulliceWhoa! Thank you for going so deep! That's incredibly useful.
Note: I responded to different parts of your comments as I read them, but I think it's important to read my replies below all as a whole because they all inform one another.
Neither?
I think
dshould have a link of typeworking-copytoh.hshould have a link of typeworking-copy-oftod.Quoting myself: "I think the problem is that the spec was not written to account for multiple revisions of a draft." Which is why there should not be a
working-copylink fromdtoe, becauseeis not the latest draft,his.Why do I know that
dhas a link of typeworking-copytohand not of typeworking-copy-of? Because sections 3.3 and 3.4 has "when included on a versioned resource" forworking-copyand "when included on a working copy" forworking-copy-of.dis our "versioned resource" so it must have theworking-copylink pointing toh.No. Very much because of what I just said above. Since
dhas a link toh. There would be no way to navigate toe,forg.parent-revisionandchild-revisionfill in the gaps.Not quite. It says it's "a resource which contains all versions of a versioned resource".
I think it's okay if it contains all versions, but also contains working copies.
Is that a little clever? Yes. Is it in violation of the spec? No.
Are drafts or working copies a part of a version's history? Yes.
Is it less than perfect? Yes. Do we have another proposal that would be more intuitive? No.
Do I believe one exists? Not really, I think that's the best we can do.
Could someone come up with a more intuitive approach? If anyone could, it would be @e0ipso or @Wim Leers ;)
That makes sense. It confirms my earlier intuition: "I think the problem is that the spec was not written to account for multiple revisions of a draft."
I completely agree with this.
"To fill that gap [between RFC5829 and Drupal's capabilities], I propose we have our own [link relation types]:
parent-revisionandchild-revisionin future. This is an additive addition that leaves a very functional implementation using only spec-defined relation types."If
working-copypoints from versiondto the most recent pending revisionh, that's the most useful interpretation.Why? If you're viewing the default version
dwouldn't you want an "edit" button to start fromh? And from the edit page, wouldn't you want the "cancel" button to send you back tod?IOW, since
working-copymust point to "ONE working copy," it should point to the single most logical revision, the last one. It should skip previous revisions of that draft because they've been superseded (i.e.e,f&gare superseded byh).That approach ensures that a spec-only implementation is still quite useful. It can be "enriched" to match Drupal's capability of "a history for working copies" by adding my proposed link relation types,
parent-revisionandchild-revisionto accesse,f, &gfromdorh.Comment #64
gabesulliceI love the symmetry of our conclusions:
#60
#57
What that tells me is that we're on the right track :)
Hopefully #63 satisfactorily addressed #1 for you. While I'm still hopeful you can come up with something better for #2, I'm willing to settle for my own proposal ofc :P
Comment #65
e0ipsoFair warning: I'm still reading through the last updates.
I read the link relations as: The ${link-rel-type-name} link of the current resource is ${link-value}
Examples:
working-copyreads well with that.working-copy-ofnot so much, but I think that the RFC definition helps in that regard. However the definition ofworking-copyof the spec seems to contradict this /sigh./me continues reading.
Comment #66
wim leersI think
parent-revisionandchild-revisionwould be pretty confusing, since there's then 3 concepts in link relations alone: "working copies", "versions" and "revisions". Only a Drupalist will be able to understand it.It's hard to argue with this after having written #62.
Hard to argue with this too.
So … it's like I have to agree with Gabe's proposal for lack of better alternatives 😲😂 As far as I can see, it's the least contradicting and most usable mapping of RFC5829 to Drupal's entity revision system out of all possible mappings.
Eagerly awaiting @e0ipso's feedback! 🙂 Rooting for him to do better, but not sure that's possible.
Comment #67
gabesullice#65
Perhaps we should all just use the "official" way of saying it:
So, your examples would be:
So:
Also, props for truly using HTTPS everywhere :P
#66
This is true. What about
prior-working-copyandsubsequent-working-copy?Comment #68
wim leersWorks for me!
Comment #69
gabesullice@Wim Leers, awesome!
FTR, @e0ipso has also voiced his approval of the HTTP API in various conversations.
Which means... I think we've got consensus on the HTTP API!
Now that we have that, here's my initial review of the patch. FWIW, this is actually the first time I've read it.
This adds a lot of complexity to
EntityResourceand requires us to inject yet another dependency into it.Instead of parsing the parameter in the controller, let's move this logic into
ResourceVersionRouteEnhancerand do$defaults['wants_latest_versions'] = TRUE|FALSEin itsenhancemethod.As you can see, we already have the logic in place to identify a collection route inside the
enhancemethod because we're explicitly saying "there's nothing to do!"s/id/ID/g
same.
I think it's confusing that these constants have different names than their values and that those names don't map to any concepts in the HTTP API. IOW, I don't think it aids understanding.
I'd rather that we have constants matching the terms we've agreed upon above with very verbose commentary on each of them explaining their meanings how each of them relates to Drupal's revisions concepts.
❤️
Whoa! We really don't have a better way than
method_exists?! I'm not surprised and not surprised at the same time.Again, this seems really clunky. Is there no better way?
If we keep plugins, let's just make a
getRevision($negotiator_name, $entity, $negotiator_value)on the plugin manager itself.That eliminates the need to have the clunky
createInstancecall andpublic statics for these exceptions.s/negoation/negotiation/
---
Even though this is @internal, I still feel very uneasy about having a plugin system for version negotiators.
It seems like it introduces a lot of boilerplate and infrastructure code when we don't actually have any identified need for the modularity that it provides.
Getting rid of the plugin manager and making everything more static would reduce the size and scope of this patch quite a bit.
I think this belongs on the
ResourceVersionRouteEnhancersince that is what is actually exploding the query parameter.This definitely needs some Functional tests.
Comment #70
gabesulliceI've rerolled the patch for the 2.x branch (I have not yet ported changes to the Functional tests).
I have not made any of the changes I asked for in #69.
Comment #71
gabesulliceThis moves the version query parameter name constant from
JsonApiSpectoResourceVersionRouteEnhancerbecause it is not part of the spec.Comment #72
gabesulliceThis addresses #69.1 by moving the logic that determines if the collection should load latest revisions or not to the route enhancer.
The logic was also not consistent with our agreed upon definitions. I updated the logic to match those.
Finally, the pattern used to load the latest version could cause lots of unnecessary db loads when most under certain circumstances. The new logic ensures that no unnecessary db loads are made.
Comment #73
gabesulliceThis addresses #69.2
Comment #74
gabesulliceThis begins to address #69.9 by making it impossible for 3rd party modules to implement version negotiation plugins. I still haven't seen the argument for plugins over static code in this issue yet, but I'll wait to remove it for now since it would be a somewhat dramatic change of direction.
Comment #75
gabesulliceThis addresses #69.4.
Comment #76
gabesulliceThis addresses #69.8, making the route enhancer much smaller and easier to read. In doing this, I realized we should probably have a new exception for a "revision not found" so we can issue a 404 instead of a 400 bad request.
More tomorrow.
Comment #78
e0ipsoWe've held this conversation in other forums, but not here. I think that it boils down to extensibility. Eventually we want revision negotiators to be extensible. The best approach for now is to have locked down plugins (plugins with unsupported opt-in). When things WRT revisions are stable we remove the lock.
Comment #79
wim leersThis sounds reasonable.
Any reason not to do this, @gabesullice?
Comment #80
gabesulliceYep. And I wanted it to be documented :)
Personally, I feel like this is a good case for applying the yagni principle. But I'm not going to die on that hill. It's not that important.
I may experiment with a service collector vs. a plugin manager. That will likely result in much less code while providing the same future benefit.
Comment #81
gabesulliceFollows up on #76. I introduced two more articulate exceptions so we can better differentiate between a revision which is missing and version identifier which is malformed.
After doing that, I realized the patch had many duplicated exceptions for impossible states. For example, there were conditions asserting that the entity/storage is revisionable, but the code in question would never be executed if that weren't already true.
Finally, the cacheability of the exceptions was incorrect in a couple places. For example, when the resource type is not revisionable, the cache context should be
url.path, noturl.query_args:resource_versionbecause the resource type differs per route, not by the version identifier.I also realized that the patch that I rerolled from did not include some of @e0ipso's recent changes. I'll roll those in in a minute.
Comment #82
gabesulliceJust fixing a couple typos and readability things.
In #69.7 I questioned why the code used
is_subclass_of. Now I understand why...It's because that avoids calling
$this->entityTypeManager->getStorage(), which would instantiate a storage class for no other reason than to do aninstanceofcheck. IOW, theis_subclass_ofway uses less memory and takes less time.Comment #83
gabesulliceOkay, this rolls in @e0ipso's changes from #52.
There was some more exception clean-up in
VersionByRelto be done as in #81 too.I looked into #69.6, where the patch was doing
if (!method_exists($storage, 'revisionIds')) {. I had hoped to find a cleaner pattern, but it seems that it's a Drupal core shortcoming. See #2986027: Deprecate NodeStorageInterface::revisionIds in favor of entity query. IOW, there isn't a better way to check for that method.BUT, #2986027: Deprecate NodeStorageInterface::revisionIds in favor of entity query does provide a pattern that we can copy. Doing so will enable us to support the full range of link relation types for all revisionable entity types (the current code will only work for nodes).
Then, when #2986027 lands, we can remove that workaround in favor of the core solution.
That will be next.
Comment #84
wim leersWith Workspaces in core, I actually think it's pretty clear that we are going to need it? (Or at least that the probability for needing it is very high.)
Comment #85
gabesullice#84: Let's consider it settled by majority rule then. Like I said, I'm not gonna choose that hill to die on :P
I've implemented the remaining link rels. There's an extensive comment in the interdiff that's worth reading.
Setting to NR for tests, this is still a WIP though.
Comment #87
gabesulliceReworded that long comment.
Doing so made me realize something. Maybe we shouldn't support
predecessor-version,workig-copy-of,prior-working-copyandsubsequent-working-copyat in theVersionByRelnegotiator.My thinking was that this would be useful for navigating forward and backward through a version history, but that's what the
version-historyresource will be for. #2992836: Provide links to resource versions (entity revisions) will also make it possible to navigate forward and backward via hyperlinks.Shall we remove support for those rels entirely? @e0ipso / @Wim Leers?
Comment #88
wim leersI'm fine with going with a MVP in this issue.
Comment #89
gabesulliceDone.
FWIW, I don't think that it's even a feature we want to add later. I think it's just a needless feature altogether. Since by using a version history or hyperlink, you'll be able to get to all those revisions by using the
VersionByIdnegotiator. I did not restore the501response code for that reason.Comment #90
e0ipsoI'm fine with going with a MVP in this issue as well.
Comment #91
gabesulliceCode standards.
Gonna work on tests next.
Comment #93
gabesulliceThis should make the existing tests pass.
Next patch will add functional tests.
Comment #94
gabesulliceHeh, it would have been nicer if I had included the patch to test.Heh, AND if the patch would apply.
Comment #95
gabesulliceThat last patch did not include the latest on 2.x... rerolled.
Comment #96
gabesulliceHere is a suite of Functional tests. I've only tested Node locally. It does not yet cover
content_moderationuse cases.Tests will not pass. I'll post the fixes gradually to better explain the change sets.
Comment #98
gabesulliceRerolled onto the latest 2.x
Comment #100
gabesulliceWhile tests are running, let me call this decision out ^
This means that resource objects'
selflinks will always link to their specific revision's URL. This ensures that the link functions like a permalink for that resource object.OTOH, resource top-level objects'
selflink will always link to the URL requested.For example,
GET /jsonapi/node/article?resource_version=rel:latest-versionwill have a top-levelselflink as/jsonapi/node/article?resource_version=rel:latest-versionand a resource objectselflink as/jsonapi/node/article?resource_version=id:2.Notably, that also means that
GET /jsonapi/node/article(no query parameter) will have a top-level object'sselflink as/jsonapi/node/article(no query parameter) and a resource object'sselflink as/jsonapi/node/article?resource_version=id:2.Comment #101
gabesulliceThis is the first failure to address.
Obviously, the cache needs to consider the resource version requested... for two reasons:
content_moderationis enabled, that will be more important. For example, if rev 1 is published and rev 2 is unpublished, I shouldn't be able to request?resource_version=id:1get an access allowed, then request?resource_version=id:2and get access allowed if I'm not allowed to view unpublished content.Comment #103
gabesulliceCool, next failure:
This is just a leftover from the 1.x version of this patch when we renamed the entity parameter.
Comment #105
gabesulliceThis failure is caused by access denied errors that should have a
vialink that includes a resource version ID.Comment #107
gabesulliceCode standards.
Comment #109
gabesulliceAnd this patch makes sure that resource objects have the right links too.
Comment #111
gabesulliceRerolled after: #3006270: Add ResourceTypeRepository::createResourceType() for easier JSON API Extras support and simpler code
Comment #113
gabesulliceWhew, that messed things up :P
Easy fix. It was a copy/paste thing. Of course one can't call
getRevisionId()on an entity which does not implementRevisionableInterface.Comment #115
gabesulliceAfter going down the path of adding the "correct"
selflinks to error and resource objects. I think thatselflinks actually belong in a #2992836: Provide links to resource versions (entity revisions) too. They raise a new set of questions that could derail this issue and greatly expand the patch size.Comment #117
gabesulliceOkay, the last patch succeeded except for new CS violations. This resolves those.
My next patches should start to make assertions for when
content_moderationis installed.Comment #119
wim leers#115++
Comment #120
gabesulliceThis should fix many of the errors like this one:
Comment #121
gabesulliceThis makes the assertions more generic, ie, not Node specific.
I'm doing this to fix
MediaTest, which will mean we have at least two revisionable entity types tested thus far.Comment #124
gabesulliceRemoving the unused
usestatement.Comment #125
gabesulliceAnd now, here are the tests for revisions + content_moderation. They should pass for at least Node and Media.
Comment #128
gabesulliceCorrect interdiff for #124.
Comment #129
gabesulliceOh look, it's our old friend
MessageTest.Comment #130
gabesulliceEasy fix for
BlockTest.Comment #133
wim leersThis is looking great!
Here's a complete patch review :)
Let's move this to a constant.
Why do we need both of these?
This seems like the wrong place to be doing this?
Why do we need to change this?
This does seem like the right place.
Nit: the comment needs to be updated.
Duplication.
s/current/default/ ?
Let's add an
@seethat points to the place that backs up this statement.I also noticed this while working on #3006270: Add ResourceTypeRepository::createResourceType() for easier JSON API Extras support and simpler code, but managed to resist making this change :P It's a good change, but if we do this … then we might as well fix it properly.
The proper fix would be a few lines higher up: rather than doing
$entity_type_ids = array_keys(…);just do$entity_types = …;— much simpler!Isn't that second check alone sufficient? I think it is. Checked core. It also only checks the latter.
That means we can remove this helper method altogether and just call
$entity_type->isRevisionable().s/negoriation/negotiation/ :)
s/revision/version/
It's "revisions" in Drupal Entity land.
It's "versions" in JSON API land.
Let's explicitly document this in
jsonapi.api.php.c/p remnant
s/Found/Exists/
I'd prefer to see this reuse
\Drupal\jsonapi\JsonApiSpec::MEMBER_NAME_GLOBALLY_ALLOWED_CHARACTER_CLASSand\Drupal\jsonapi\JsonApiSpec::MEMBER_NAME_INNER_ALLOWED_CHARACTERSinstead. That makes the need for this strict validation clearer: for spec compliance.s/$revision_id/$version/
The version negotiator …
AFAICT this means we cannot "just" create new version negotiation plugins and have them work.
Which is what I think you were getting at last week in chat: whether it's okay to hardcode certain things until we actually want to support version negotiation plugins. Correct?
If correct, I think adding a
@todohere would make sense. Something like:@todo If version negotiation plugins become an officially supported API in the future, this logic will need to be moved into the relevant plugin.IMHO this documentation belongs on the
VersionByRelclass.s/valid/isValid/
Why this
assert()?Why not make this as simple as
\Drupal\jsonapi\JsonApiSpec::isValidCustomQueryParameter()?s/Revision ID/version/
s/classes/plugins/
I think this meant to refer to the annotation class?
This actually would be an even better place than
jsonapi.api.phpto document the distinction between and mapping of "revision" and "version".PHPStorm formatting, let's put this on a single line.
👍
Let's use
FQCN::class.Use
assert()instead. Just as many LoC, same autocompletion-in-IDE consequences, but with an extra assurance :)These lines can be removed;
MessageTest::testRevisions()already takes care of this.It never will be possible to test revisions on an entity type that cannot be stored: if it cannot be stored, there can also not be revisions!
😄👏
These are all 4 version (revision) selection options that this patch introduces and that this concrete use case provides. 👍
Übernit:
$original_urlslightly confused me later on, I thought it meant "URL object prior to modifications". But it really means "original revision's URL". IMHO$revision_original_urland$revision_latest_urlwould be slightly clearer. And perhaps$rel_latest_version_url+$rel_working_copy_url.s/normal URL/canonical resource URL/
Nit: I think you can merge these comments:
/** @var Interface1|Interface 2 $entity */Why
'in one place and`in the other?First: yes this is correct per https://www.drupal.org/node/2897586.
Second: I personally believe this should not be in the HTTP API response, see #2933518: The semantics of the "revision_translation_affected" field are unclear to Decoupled Drupal developers (REST/JSON API/GraphQL) users, improve this.
Great to have explicit comments for these! 👏
Please use
Cache::mergeTags()instead.Comment #134
gabesulliceThanks for the epic review @Wim Leers! I'll address that soon.
Now, I'm trying to get
BlockContentTestworking. Testing cache tags is a little more complicated. The field we're revisioning is a text field and so, thus far, we've had to add theconfig:filter.format.plain_textcache tag to the expected cache tags.BlockContentTest::getExpectedCacheTags()already adds that cache tag though.Rather than coming up with a clever way around that, like using
Cache::mergeTags(), let's just change the revisioned field to something simpler. Like a number field.Comment #135
wim leersi noticed that #134's testbot status indicator says: — so I dove in to fix this, see #3006743-7: Follow-up for #2624770: EntityConverter service requires additional parameters since Drupal core 8.7.
Comment #136
gabesulliceIn #130, we can see that
MessageTestis still not passing.That's because of a change that was introduced then not completely reverted. @Wim Leers pointed that out in #133.4.
Reverting that change fixes
MessageTest.Comment #137
wim leersFixed in #3006743-9: Follow-up for #2624770: EntityConverter service requires additional parameters since Drupal core 8.7.
Comment #140
gabesulliceThanks for jumping on that @Wim Leers!
This should get
BlockContentTestto pass.Comment #141
wim leersWoot, green!
Comment #142
gabesulliceEDIT: RIP my patch number :P
1. Done.
2.
$query->latestRevision()does not mean that the latest entities are loaded. It means that the latest revisions are queried.$query->execute()still returns an array of entity IDs which need to be loaded. Still, this remark was a good one because it helped me find a more efficient way to load the latest revisions:RevisionableStorageInterface::loadMultipleRevisions().3. Why did I do it like that?
EntityResource::getIndividualhas this:When an exception is thrown, it does not get normalized by
JsonApiDocumentTopLevelNormalizer. Therefore, the cache context you qouted in #133.5 will not apply. So, the exception needs to carry all the appropriate cacheability metadata and the only way to get cacheability metadata into that class is to pass it in via the$access_resultparameter. I felt like this was reasonable, because what we're saying is that "access to the entity is specific to the revision loaded" and that's true. Access can depend on the loaded revision.4. This was addressed in #140.
5. Actually, it's unnecessary because of #3 above. It was added when I was still trying to put
selflinks in the document withresource_versionquery strings.6. Yes? I think `id` is required for a plugin and we're using the constants elsewhere where we don't have loaded plugins.
7. Fixed.
8. That documentation is surprisingly well hidden lol. Anyways, done.
9. Fixed :) Good call.
I'll address the next items in another comment + interdiff to keep the change set at a reviewable size.
Comment #144
wim leersJsonApiDocumentTopLevel!getPluginId()?Comment #145
gabesulliceLet's fix those new failures, those came from me removing #133.4.
I also noticed that it is possible to use the
resource_versionon PATCH requests and POST requests on relationship routes. We shouldn't allow mutating arbitrary revisions. At least not until #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API. is settled.Comment #146
gabesullice3. Ah, you're right. In
JsonApiDocumentTopLevelNormalizerValue, there's this:I missed the
isErrorDocument()check and forgot about #2949807. Because of that condition, I assumed what I said in #142.3 was the case.I still feel like it's the wrong place to put that cache context though. It reminds me of #2992673: Set collection-specific query parameter cache contexts on collection responses instead of all responses where we tried to move things out of that top level class and put cache info in the most relevant place. If you feel very strongly in the opposite direction, I'll change it.
6. I'm still a little lost. Do you mean that
getPluginId()should be overridden withreturn self::NEGOTIATOR_NAME;? The reason we need those constants is because of #133.19.Comment #147
gabesullice10. You're right. Done. Thanks for doing the research.
11. Fixed.
12. Fixed.
13. Done.
14. Fixed.
15. Done.
16. But this doesn't have to do with spec compliance. This validates the value of a query param, as in,
?key=value. The spec does not have rules for that. Personally, I prefer that the validation be more restrictive rather than less.17. Fixed.
18. Done.
19. Yes. Done.
20. Done.
21. Done.
22. This is just a habit I developed when iterating on a regex to determine if it is invalid or if it's valid but simply didn't match anything. Cleaned up.
23. Done.
24. Fixed.
25. I added documentation in a few places, in addition to this one.
26. Done.
27. Yarr.
28. Good idea.
29. Nice suggestion! I like it.
30. Good point!
31. I know!!! I wish I could have kept those. See #134 for why they were lost :(
32. I like that. Good idea.
33. Fixed.
34. Much nicer.
35. I don't know. I ended up removing all quotes around 'published' and 'draft' since they're english language names, not machine names.
36. Wow, that's a gnarly issue... I agree that this field is confusing af.
37. :D! Hopefully you like all the comments even more!
38. Aye aye.
*Deep breath* WHEW! Epic review @Wim Leers. Thank you!
Comment #148
gabesulliceOff by one.
s/latest-version/working-copy
s/encode/endcode
Given the docs in
jsonapi.api.php, these should both be "version identifier". I think renaming the constant might clear up #133.16 too.s/an/a
Next, this needs a few tests for the collection endpoint and possibly the related/relationship endpoints.
Also, I just discovered
Drupal\node\Access\NodeRevisionAccessCheck... I'm not sure if that's respected already, but I think we need to ensure that it is. @e0ipso and @Wim Leers, do you agree?Comment #149
wim leers#145:
Drupal's UI doesn't even allow editing any revision: editing a revision means creating a new revision; only the latest revision can be modified without creating a new revision: that just means appending changes to the latest revision. But editing arbitrary past revisions has never been supported.
#146:
getPluginId()instead. If there's a reason we cannot callgetPluginId(), then that is probably a valid justification for the constant to exist, but then we should document that justification. Let's at least mark these constants as@internaland add docs with a justification based on #133.19.#147:
VersionNegotiatorinstead ofVersionNegotiation? I don't know.$rel_*_urland$*_revision_url. Would be clearer if it was$revision_*_url.Thanks especially for the
jsonapi.api.phpadditions. I especially like the ASCII art! 👏 I still miss two things in it:#148: Those changes look good. +1 for more tests. And yes,
NodeRevisionAccessCheckMUST be respected, as mustMediaRevisionAccessCheck. This sadly looks to not yet be standardized. I suspect that @amateescu has thoughts about this thanks to his work on #2725433: WI: Phase A: Use the revision API in more places, #2880149: Convert taxonomy terms to be revisionable etc. I'll ping him.s/In future/In the future/
s/timestamp or workspace based/timestamp or workspace-based/ (I think — I'm not 100% clear on the rules in English :))
Comment #150
wim leersPinged @amateescu via #2725433-42: WI: Phase A: Use the revision API in more places:
Comment #151
amateescu commentedWe're adding a generic entity revision access checker in #2350939: Implement a generic revision UI, but, looking at that patch, it doesn't seem to be very close to completion. I think it would benefit the most from a sister issue of #2809177: Introduce entity permission providers that would add generic revision permissions.
OTOH, after reading
NodeRevisionAccessCheckit seems that it's quite focused on providing access to the "revisions overview" and "revert revision" forms, which is not very useful for JSON API I think.The only conclusion that I have so far is that generic revision handling is quite a mess still :/ TBH, I'm not very familiar with everything that's going on in this area because I've been focused only on the storage layer so far, so I can't really provide any meaningful guidance on the access part here..
Comment #152
wim leers:(
This is good to know and helps a lot!
Thank you! :)
Comment #153
gabesulliceGiven that there's no standard way to check revision access yet. I've whitelisted the entity types for which we'll support revisions until that exists.
Next, I'm gonna implement the Node and Media access logic.
Comment #154
wim leersThat seems like a fair compromise to be able to move forward!
Comment #155
gabesulliceSmall fix since #3005999: Revision ID should be `drupal_internal__vid`.
Comment #156
gabesulliceEntityResource::getAccessCheckedEntityhas the nice property that it is a central place to handle access checking. Get it right once, and it's right for everything. I don't want to lose that property.Unfortunately, checking revisions isn't a small piece of code and will require injecting services... conditionally. That's because the revision checking services for node and media will only exist if/when node and media are enabled.
I've been working hard to reduce the size and scope of the
EntityResourceclass and adding revision access checking to it will add yet more complexity. I'd prefer not to do that.So, let's move
EntityResource::getAccessCheckedEntityinto its own service.This makes sense really, we're already accessing it from multiple locations in the codebase. That's why it's
publicandstaticas it stands.That's what this change does. We can move this to a separate issue if @e0ipso or @Wim Leers feel that it's warranted.
Comment #157
gabesulliceNow, we can layer on revision access checking.
This will fail. I'm struggling with cache contexts and the
AccessResult::$reason. It seems I can't have both correct at the same time 😭I'm sure it's possible to get right, maybe @Wim Leers can take a look since it's my EoD.
Comment #159
wim leers+1 to making it a separate service. This is exactly the sort of "refactoring when it makes sense" that we ought to do continuously. Kudos for setting the right example!
This makes #157 get past that failing 403 due to different error messages. It now fails a few dozen lines later. The reason: this test is not yet granting the necessary revision-level permissions. Since you probably have a clear vision in your head of how this test coverage should work, I'm leaving that to you.
I think you were getting tricked by the fact that not having the
access contentpermission forNoderesults in aAccessResult::forbidden(), which always takes precedence overAccessResult::neutral(), and hence also its reason takes precedence.Comment #160
wim leersThis class and service don't indicate it, nor does the original
EntityResource::getAccessCheckedEntity(), but … all of this is specifically for viewing entities.Which is why it's rather odd (and premature) to have a
$operationparameter in the revision access check logic.I modified the revision access check method to omit this parameter (fixing one of the CS violations in the process) to keep this solely about "view" access. If you want to generalize this to check non-view access, then let's do that when the need arises.
entity viewaccess, then we checkentity revision viewaccess if it's revisionable and OR it, and then we checkentity label viewaccess if the combined result so far does not allow access. Of course things then become pretty complex!The thing is, like @amateescu indicated in #151, this is pretty much new territory. The access model for revisions is not very well defined. The closest thing to a formal definition we have is
\Drupal\Tests\node\Functional\NodeRevisionPermissionsTest::testNodeRevisionAccessAnyType(), which was created in 2012 in #880940: view/revert/delete revisions per content type — and that test coverage leaves a lot of (clarity) to be desired.The safest thing to do here, is to AND the access checks together: to be able to view a revision of an entity, you have to be allowed to view the entity at all (the current patch already does this). But merely being allowed to view the labels of an entity does not entitle you to be allowed to view the label of any revision of that entity type — it only allows you to view the label of the default revision.
view labeloperation to override the access results of both the "viewentity" and "viewentity revision" checks, and is hence resulting in a 200 response, but with only the entity label. (At least forNode, which is the entity type I was testing with.)view labelshould only run for the default revision.Comment #163
gabesulliceFixing the CS violation and picking up where @Wim Leers left off... thanks @Wim Leers!
Comment #165
gabesulliceYeah... Complex is a good word. I think I fixed it. 🤞
Comment #167
gabesulliceWell, I fixed the old "it". But I guess there's a new "it" to fix. Collections that aren't on revisionable resource types should get the
url.query_args:resource_versioncache context./me thinks now he's done it.
https://media.giphy.com/media/ebITvSXYKNvRm/giphy.gif
Comment #168
gabesullice🙈. It's obviously time to sign off.
Comment #170
gabesulliceCouldn't resist.
Comment #172
wim leersComment #174
wim leersComment #175
gabesulliceWOOT! @Wim Leers++
Thanks for jumping in!
What's left?
#148:
#149
Neither is incorrect. But I think
VersionNegotiatoris better. Will change.Because I like them. I think they improve readability :P
I'm not sure what you mean by this @Wim Leers ^
Comment #176
gabesulliceI just had this thought: Shouldn't #2942907: Entity system does not provide an API for retrieving an entity variant that is safe for editing inform what revision ID should be considered the "working copy"?
If not, why not?
Comment #177
gabesulliceThis marks the constants as internal and renames the plugins and annotations to
VersionNegotiator.Comment #179
wim leers#175:
+1 to that in general, but if it's just calling another
isSomething()method, that's rather pointless. However, for now you have to hardcode the 3 supported entity types. So yes, then it totally makes sense to keep this helper method.I think that's because perhaps you didn't see the bit just before those two points: . IOW: I think it'd be good to mention those two things in
jsonapi.api.phptoo.#176: excellent point and discovery! #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API. previously linked to it. Thanks for pointing to it again! It's a critical core bug. Which makes me hopeful that it will get dealt with shortly, and whatever logic core decides JSON API can then provide a "forward compatibility shim" for.
#177's interdiff looks splendid 👏
Comment #180
amateescu commentedRe #176, that's exactly the goal of that issue :)
Comment #181
gabesulliceNo, I didn't miss the first part. I don't understand that sentence. Maybe you mean a link to an issue should be added to
jsonapi.api.php?Ohhh. For some reason I had it in my head that you were talking about this:
Not
isVersionableResourceType().That makes my response seem much more snarky 🤭
You're right that that method was rather pointless, even for readability.
FWIW, I originally put that there assuming Extras would
eventually want to override it at some point.
Comment #182
gabesulliceFixing the new CS violation.
Comment #183
gabesulliceHere are the documentation additions that @Wim Leers requested.
I needed to create a new issue: #3009588: Provide a collection resource where a version history can be obtained (`version-history`, `predecessor-version` and `successor-version` link relations)
Comment #185
wim leersThanks!
Comment #186
gabesulliceGah. Silly CS violation.
This adds tests for collections.
Comment #187
gabesulliceHere are some preliminary tests for relationship routes.
We'll need to update
RelationshipFieldAccessto respect the new revision access logic. I'll do that next.Comment #188
gabesulliceThis consolidates entity access checking from
RelationshipFieldAccessinto the new entity access checker.Comment #191
gabesulliceExpecting the
selfandrelatedlinks to be relative to the requestedresource_versionturned out to be pretty challenging and expansive without doing something hacky. That's the same conclusion I reached in #115, but forgot about it. This patch removes this expectation.The current challenge I'm facing is related to caching. In the second iteration of the relationship assertions, after the call to
$this->grantPermissionsToTestedRole(['field_jsonapi_test_entity_ref view access']), the first response for/jsonapi/node/camelids/{{uuid}}gets cached and then the request for/jsonapi/node/camelids/{{uuid}}?resource_version=id:1results in a dynamic page cache "HIT", despite me confirming that theurl.query_args:resource_versioncache context is set on both requests.Comment #193
gabesulliceHere's a little fix for all these errors:
Comment #195
gabesulliceThis should fix the cache HIT/MISS thing.
What was wrong?
When a route access requirement is added to a route (like relationship routes, which use
RelationshipFieldAccess),RouteAccessResponseSubscriber(RARS) will take the route access result and add it as a cacheable dependency to the response (if it's aCacheableResponse).However,
DynamicPageCacheSubscriber(DPCS) has a higher priority than RARS, so when DPCS generates a cache key for the response, it doesn't take the cacheability metadata from the access result into account. Fortunately, route access runs independently of DPC and so access is always respected.The solution is to ensure the the query parameter cache context is added not just to the access result, but also the response. This makes sense since the response can vary based on the loaded revision, regardless of access.
Comment #197
gabesulliceOne would think I'd have figured out to add this check by now!
Comment #198
wim leersLooking great! What are the next steps here?
This definitely needs documentation and a CR. What else?
Since this is by far the most complex (and perhaps even "invasive") addition to the JSON API module that is not defined by the JSON API 1.0 spec, I think this would be a great candidate to apply #2955020: Spec Compliance: JSON API's profile/extention (Fancy Filters, Drupal sorting, Drupal pagination, relationship arity) needs to be explicitly communicated to. Perhaps that is the last thing that ought to happen?
Comment #199
gabesulliceWe still need tests for
relatedroutes.That should be pretty simple because they're so closely related to relationship routes. The complexity will lie in getting the test expectations to be correct.
Then, we need to ensure that our definition of which revision the "working copy" is will not be different than the revision that #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API. will produce.
Finally, I agree with you, we should figure out how this will integrate with profiles. Then document + publish a CR.
Comment #200
gabesulliceThis adds related tests, in 3 parts:
getExpectedRelatedResponses(plural) method, so I just refactored that into a singular and plural one.PS. I cannot wait for Gitlab so I don't have break apart and comment on interdiffs like this. It should just be automatic with my commits!
PPS. Comment 200!
Comment #202
gabesulliceErg.
Comment #204
gabesulliceI think this should fix the last failure.
Comment #205
gabesulliceI said a while ago that I wanted to try a service collector vs plugin managers. It was a relatively quick change and got rid of a lot of boilerplate. I'm curious what you think, @Wim Leers and @e0ipso. Personally, I feel this is a cleaner approach (relative to its enormous already size, it doesn't look like much).
Comment #206
gabesulliceSame interdiff as #205, but I think it's easier to see the changes because files got moved.
Comment #208
gabesulliceIn #199, I said:
I went ahead and dug into this and am fairly confident that we've done this correctly. The editable revision is the latest revision. What we may not have done right is get the intersection of translations and the latest revision correct, but I think we should figure that out in #2794431: [META] Formalize translations support once this lands.
Unfortunately (maybe?), while researching this I discovered yet another bit of access control logic that we missed.
content_moderationdynamically adds a route requirement and route access check to moderated entities. The actual access logic is inDrupal\content_moderation\Access\LatestRevisionCheck.Since it's at the routing layer, not the entity access layer, it was easy to miss and not reusable. That means I had to duplicate it into our code :\
Again, I think that that was the last bit of access control to handle, but I'm no longer confident that I'm not missing yet another obscure thing. So, before we commit this, I would like to have a thorough review from a Content Moderation maintainer. I'm going to reach out to see if I can't arrange that.
Comment #211
gabesulliceI've reached out to @timmillwood for this 🤞🤞🤞
Comment #212
gabesulliceThis should get tests green again.
In both patches, I needed to fix how permissions were granted after implementing the
LatestRevisionChecklogic.In the service collector patch, I just needed to rework the Kernel tests a bit because they were built with the expectation of a plugin based approach.
Comment #214
gabesulliceJust a fixup for a finicky testbot :P
Comment #215
wim leersaccess_check.latest_revision) and pass it a fake route. That way, any customizations by contrib modules would at least still be respected.Comment #216
wim leersRan into #1812202 again (previously in #48), while digging into Media initiative stuff. This time linking it properly. I wish we already had this.
Comment #217
gabesulliceRebasing after #3007274: s/JSON API/JSON:API/.
Comment #218
gabesulliceThis needs to
s/jsonapi/jsonapi\\/, but I think you missed thisassert?Comment #219
wim leersI did! Never mind in that case :)
Seems like there's one last thing we should do here:
(Arguably we should do this for #2958554: Allow creation of file entities from binary data via JSON API requests too. But that one is relatively stand-alone, whereas this issue/feature has potential implications for just about every JSON:API response!)
Comment #220
gabesulliceThe attached primarily does a few things:
block_contententities, but that was not safe, I just emulated the logic of node and media. I'd rather wait for a core API instead. @Wim Leers and I discussed this in person and I indicated that I was going to limit this to just nodes, but I had forgotten that media also has an access check available.#219, I'm not sure why profiles needs to block this, if profiles are not sufficient for what we've done, then the 1.1 spec should be fixed, rather than vice versa.
So, I think I've done just about everything I can on this patch.
I'll work on docs and a change record.
Comment #222
gabesulliceDocs created: https://www.drupal.org/docs/8/modules/json-api/revisions
Comment #223
gabesulliceCR created: https://www.drupal.org/node/3015434
Comment #224
gabesulliceComment #225
gabesulliceFixes #220, I forgot that the access check was running per entity in each revisioned collection.
Comment #226
wim leersInterdiff review:
I think we should document that this is a temporary measure, that eventually JSON:API shouldn't need to be explicitly aware of Content Moderation.
+1
Let's link to the issue(s) linked by @amateescu around #150. That may result in some people contributing to making this happen, since the error would point them directly to the place where they can help make it happen! :)
Shouldn't this just rely on
$resource_type->isVersionable()?Patch review:
block_contentshould be removed here too.Comment #227
wim leersIt seems the #2649268: Entity Field Query throws QueryException when querying all revisions and setting condition on entity reference field where target doesn't support revisions core bug should also affect this issue?
Comment #228
ndobromirov commentedThere is a conflict with this one: #3014232: [regression] ResourceTypeRepository is significantly slower in JSON:API 2, becomes noticeable when handling hundreds of interlinked resources. Whatever gets committed first, the other will need a re-roll.
Comment #229
gabesulliceReroll because #3014232: [regression] ResourceTypeRepository is significantly slower in JSON:API 2, becomes noticeable when handling hundreds of interlinked resources landed.
Comment #230
gabesulliceThis should address everything in #226.
Comment #232
gabesullice🙈
Comment #233
wim leersLet's make this an explicit
@todoso we cannot forget to refactor it away!I'm fine with only having this comment with
@todoin only one place rather than multiple.Comment #234
wim leersThat looks like random failures. Retesting.
Comment #235
wim leersGreen! 😀
I did a thorough final review and performed manual testing. I found a bunch of nits, some things that I think would benefit from an explicit test and especially a bunch of suggestions to make the change record more helpful:
Nit: I think this section should be called "Resource versions" based on the text below it, based on the URL query parameter and based on the
ResourceType::isVersionable()method.Nit: when marking a class
final, then the properties and methods might as well beprivaterather thanprotected?This means it's possible to get the working copy for an entire collection; that's not yet explained in the change record.
Nit: I think we can remove
&& $storage instanceof RevisionableStorageInterface. That could lead to subtle bugs, because every content entity uses\Drupal\Core\Entity\ContentEntityStorageBase, which means this is true for all of them, even though not every content entity type is revisionable.So just respecting the boolean flag should be enough.
Nit: mismatch.
We probably should copy/paste the descriptions for these from
\Drupal\Core\Entity\EntityTypeManagerInterface::getStorage().I think it may be worth having the tests load
COLLECTION_URL?resource_version=id:42and confirm that they get this error response.The change record says this only works if you have Content Moderation installed. I don't think that's true?
This is no longer a plugin manager, so this should be removed.
Übernit: these don't need to be assigned to variables.
I think it'd be good to also explicitly test that
URL?resource_version=rel:latest_versionis equivalent toURL.And document that in the change record.
And #220:
I agree that that should not block this, but given this will be the profile with by far the most impact, I think it would be prudent and highly valuable to do so now, especially since the JSON:API team tagged version 1.1-RC1 of the spec: https://twitter.com/jsonapi/status/1069593631365959680.
Comment #236
ndobromirov commentedThis is for patch #232...
I am not tracking the while history, but there is some regression in the patch against jsonapi + jsonapi_extras. Due to the creation interface change in jsonapi, jsonapi_extras is loosing fields mapping input due to position in the __construct interface and thus does not show any fields on the responses.
If we go with that direction, there should be a follow-up on the jsonapi_extras very soon :)...
Comment #237
gabesullice@ndobromirov, I'm sorry, but I don't really understand what regressed. Can you clarify?
Comment #238
ndobromirov commentedThis is how jsonapi_extras is creating resource types from the repository. As they are not overwriting the constructor, the base class one is invoked and for the place that you see here
$field_mapping, in the jsonapi with this patch the signature is expecting$is_versionable, and the actual list fields falls back to empty list, due to a default argument :(.As there are no additional arguments - this is passing empty list for
fields_mappingresulting in empty responses (no attributes in the objects...).Either way jsonapi_extras will need a follow-up once this is in... Should I create one and patch the case I am trying to describe?
Comment #239
ndobromirov commentedHere is the issue I've mentioned. Patch is coming in a bit.
So this is now patched and I can confirm that it's fixing the issue.
Comment #240
gabesullice#235:
1. Done.
2. What does that buy us other than making it harder to "unfinalize" this at a later point?
3. Will do.
4. I think I did this check for better code completion in the IDE rather than to protect against non-revisionable entities (that's already asserted long before this code runs). To make that more clear, I refactored it into an
assert.5. Sorta, it's an abstract class so the comment would be more correct for the extending class. I compromised with "Constructs a version negotiator instance".
6. Done.
7. Good idea, done.
8. Technically, you can request versions by their rel even when Content Moderation is not enabled, but it's really pointless to do so. Without CM installed, the latest revision is always both the latest revision (working copy) and the default revision (latest-version). IOW,
URL,URL?resource_version=rel:latest-versionandURL?resource_version=rel:working-copyall return the same thing. The change record doesn't say that it doesn't work, it just says that you can use rels if you have CM installed. I'll revise it a little bit.9. Done.
10. Done.
11. Done.
12. I'll open an issue for it and start a draft.
Comment #241
gabesullice@ndobromirov++
That makes sense. Thanks for testing this and for writing a patch! You're awesome!
Comment #243
gabesulliceSilly typo.
I updated the CR: https://www.drupal.org/node/3015434/revisions/view/11201527/11225931
I created an issue for an associated profile.
Comment #244
gabesullice^ for #243
Comment #255
gabesulliceWhen testing against Drupal 8.5:
Comment #256
wim leers#240: That all looks great!
The change record is now much clearer. 👍
Interdiff review:
👍
👍
#255: I say we do just the same as we did in #3001958: 4 test fails due to using deprecated code on 8.6 and 8.7 since #2996789: temporarily fork the test trait for
\Drupal\Tests\field\Traits\EntityReferenceTestTrait: that trait was also only available in >=8.6. Did that in this reroll. Also had to adjust the logic to not create the editorial workflow on 8.5, because it was created automatically then. Not pretty, but necessary to make this work on 8.5, and works fine locally for at leastNodeTest::testRevisions()Comment #257
wim leersThat leaves only one thing: getting the @e0ipso's stamp of approval!
I just talked to him in chat. He needs to recheck the patch, but doubts he'll have many concerns.
There is one key concern he has though: he's not yet sure about the tagged-service-instead-of-plugin approach. That started in #205, in #214 there were green patches for both approaches. In #215 I expressed a slight preference for tagged services since it results in less code and less metadata duplication.
I personally don’t have a strong preference; the primary thing I care about is that we don’t allow new version negotiators, regardless of whether they’re services or plugins. Quoting myself: . I also pinged Plugin system maintainer @tim.plunkett for advice.
Let's figure this out!
I'm excited to see how far this has come. It looks like we'll be able to ship version 2.1 of JSON:API with both #2958554: Allow creation of file entities from binary data via JSON API requests and this at the same time :)
Comment #258
e0ipsoPartial review. Only minor nits and questions.
Let's remove this part:
and should minimize the "Drupalisms" that a developer building against a JSON:API implementation will be required to know.Can we mention version UUID here?
This breaks JSON:API Extras, would you consider setter injection?
finalseems like an unnecessary roadblock. What if this needs to be decorated to include access checks to custom entity types?Temporary until when?
Why not inject these as well?
Comment #259
wim leers#258.3: Are you sure it breaks JSON:API Extras? As of #3004582: Make JSON API Extras 2.x (next release: 2.10) compatible with JSON API 2.0-beta2, JSON:API Extras'
ConfigurableResourceTypeRepositoryno longer overrides theResourceTypeRepositoryconstructor, precisely to avoid breaking it whenever the base implementation's constructor is changed :)Comment #260
e0ipsoWhat about custom & contrib entity types that define revision access checkers?
We should look into allowing them to be supported without requiring changes in JSON:API.
Maybe a factory is a good pattern to consider here.
Comment #261
e0ipso#259 I misread that. It's not (like I thought) on the ResourceType (nor the repository), but in the EntityResource controller instead.
Comment #262
gabesullice#258:
1. Done.
2. Sure!
3. n/a per #261
4. I _really_ don't want this to be decorated lol. Instead, I want a real core API for revision access checking and/or for custom access to be handled by decorating the appropriate entity access control handler. Until then, I figured it was prudent (from a security and BC perspective) and desirable (from a pragmatic/expedience perspective) to go with an MVP that only allows for Node and Media entities to be versioned this way.
5. Temporary until a core revision access API exists. That's documented in the code block in #260.
6. Good catch! This was taken out of
EntityResource.phpwhere it used to be astaticmethod. I never updated it apparently.#260:
See 4 & 5 above. If this is really important and in high demand, I'd like to do it in a followup.
Comment #264
e0ipsoSome more comments on the current patch. I'm sorry I can only do in-depth reviews in chunks. I can imagine how frustrating that may be. I apologize for delaying this.
The
ResourceTypeconstructor was updated after all.This means that JSON:API Extras will be broken by this change.
How can we reconcile that?
Again, this seems to be the biggest disconnect. I don't think Core will get this together in reasonable time.
We should talk about this specifically. I am leaning towards detecting that an entity type has/lacks a revision access checker instead of manually hard-coding that feature.
Would NULL be coming from a valid version identifier? If not, is
InvalidVersionIdentifiermore appropriate here?I think line 107 is dead code, it would've thrown in line 100.
Those cache contexts do not really belong to the entity. This seems to be a way to accumulate cacheable information.
Would it be more appropriate to
new CacheableMetadata()and then merge the entity and add thestatic::CACHE_CONTEXT?What's the reason to have this here instead of at the top of the method?
Maybe we can change the language to unsafe methods, as opposed to mutation.
Also, will this affect pre-flight requests using an OPTIONS method? I always forget how that external library works.
We are not adding the
url.pathcache context here. Do we need to?Now that I see this here, I'm noticing that there is no mention about working copy collections in the documentation above.
Can we add a mention to that?
Would it make sense to have a
VersionByRel::NAMEconstant to duck this magic number?Also, I see there is a strong coupling between this route enhancer and the version by rel negotiator. Would it make sense to move this block out of the route enhancer?
#262.4 let's set this apart, because I suspect this will be a friction point. Let's address the other stuff first and maybe jump into a 10 min video conference to sort this one out.
Comment #265
ndobromirov commented264.1: There is this #3020112: [Regression] API change in jsonapi module. to fix the regression on josonapi_extras side if we keep the current direction. Already discussed from #236 to #241.
Comment #266
wim leers#264:
If there was a way to detect this, then Drupal core would also be using it. There simply is no mechanism, no convention, nothing for this right now. This is exactly why it's hardcoded today.
\Drupal\Core\Entity\EntityRepository::getTranslationFromContext()is doing exactly the same. And then I realized why: both "a translation" and "a revision" are not separate value objects, they're just different states of one and the sameEntityInterfaceobject. So this is actually correct.static::CACHE_CONTEXTis varying by: a specific URL query arg.Comment #267
e0ipsoExactly. That's where I'm going to. However, hard-coding this is to say that only core can provide entity types. We need a way for custom entity types to add their revision access checker.
Comment #268
ndobromirov commented@eoipso - maybe another UI in jsonapi extra that will default to the 2 hard-coded ones and allow for users to change it explicitly?
Comment #269
gabesulliceReroll.
Comment #270
gabesullice#264:
No apologies necessary! That's understandable. For the moment, this isn't really delayed because we can't commit this till 2.0 is released.
1. See #265
2. Yes, let's talk about it :)
3. No, the argument passed to
ensureVersionExistsis the result of$storage->loadRevision($revision_id)which returnsNULLif the revision ID doesn't exist.4. Good catch!
5. See #266.5. This is also an important mechanism to ensure cacheability of 403 responses are per-revision. We need to add it even if a query parameter is not provided or else if you fetch
/paththen fetch/path?resource_version=id:{n-1}, you'll get the cached response for/path.6. Almost the same as #5.
7. Good call.
8. Nope. If a query param is found to be invalid, all requests with that invalid param should get the cached exception, regardless of the path.
9. I don't think I really understand what you're asking for here. I updates the class comment, but I don't think that's what you meant.
10. I think that this is in the right place to have this logic because it's parameterizing the behavior of
getCollectionvia thestatic::WORKING_COPIES_REQUESTEDparam and it keeps this error messages pretty centralized. As for the coupling, I think it's pretty superficial. The negotiator itself doesn't have anything to do with collections. It will never even be loaded. The reasonVersionByRel::LATEST_VERSIONis there is just to get the string "latest-version" instead of hardcoding it. That said, I think to enforce that it isn't coupled the negotiator, it would be better to actually hardcode the link relation names (they're not ever going to change).Comment #272
gabesulliceWhoops, looks like I missed this in the reroll.
Comment #274
gabesullices/isMethodSafe/isMethodCacheable:isMethodSafe()is apparently deprecated.Comment #275
gabesulliceBefore this lands, I'd like to change `resource_version` to `resourceVersion` (snake case vs camel case) to align with v1.1 of the spec. See https://github.com/json-api/json-api/pull/1333#discussion_r243379557 for context.
Leaving as Needs Review for @e0ipso.
Comment #276
gabesullice@e0ipso, @Wim Leers and I discussed support for contrib and custom entity types in the API-First weekly meeting.
We agreed that JSON:API should not have an API to support them. However, that if it is an absolute necessity and cannot wait for official core support, there should be a way. As we typically do, we've decided that the best way to do this is allow JSON:API Extras an internal API it can override to achieve something less-than-ideal but of practical value to real-world applications.
As such, I've removed
finalfrom theEntityAccessCheckerservice and I've returnedAccessResult::neutral()instead ofAccessResult::forbidden()fromEntityAccessChecker::checkRevisionViewAccess().Without JSON:API Extras, the behavior will be the same. Access will not be allowed to entity types other than
nodeandmedia. But if JSON:API Extras is installed, it can decorate theEntityAccessCheckerclass and override thecheckRevisionViewAccessmethod, like so:This will mean that JSON:API can still return
AccessResult::forbidden()orAccessResult::allowed()to override JSON:API Extras, but Extras will be able to fill in gaps where JSON:API can't have an opinion (because there's not an official core API).#275 next. Leaving @e0ipso assigned to get his +1 (to also make sure I've summarized things accurately).
Comment #277
gabesulliceI made the changes mentioned in #276 and also merged the 2.x branch into the patch. I accidentally posted the interdiff for the merge. This is the right interdiff.
Comment #278
wim leersI think we could've achieved the same without removing
finaland requiring JSON:API Extras to decorate thejsonapi.entity_access_checkerservice … except then we'd need to make\Drupal\jsonapi\Access\EntityAccessChecker::checkRevisionViewAccess()public.But … that's really a detail. It shouldn't hold back this patch. I'm only saying it to offer it as an alternative to both @gabesullice and @e0ipso.
Just like in #257, I think this is ready. I'm excited about the next steps: #3009588: Provide a collection resource where a version history can be obtained (`version-history`, `predecessor-version` and `successor-version` link relations) and #3020136: Author and register a JSON:API profile for resource versioning, and everything else in #2795279: [PP-2] [META] Revisions support.
Comment #279
e0ipsoI think this is ready as well. I'm very excited about this one! 🎉
Comment #280
e0ipsoComment #281
wim leersYay! 🎉 So this will then land together with #2958554: Allow creation of file entities from binary data via JSON API requests in the 2.1 release of JSON:API! 🤘
Next step: #2992836: Provide links to resource versions (entity revisions), which was blocked on this. And after that, #3009588: Provide a collection resource where a version history can be obtained (`version-history`, `predecessor-version` and `successor-version` link relations), which is blocked on #2992836. (All part of #2795279: [PP-2] [META] Revisions support).
P.S.: thanks for improving the title, I've been restraining myself for 280 comments to refine it 😅 OCD trigger removed ✅
Comment #282
gabesulliceSince the release of SA-CONTRIB-2018-081, we (the maintainers) have since realized that we may need to be concerned about revision access when filtering on non-default revisions (good catch, Wim!).
Given the tremendous depth of that issue and this one, it was proposed that we simply remove resource versioning from collections and add that feature in a followup.
However, rather than go through all the effort of extracting that feature, I propose that we throw an exception when the
filterquery parameter is detected in conjunction with a non-default revision collection request. That lets us keep this patch as close to its RTBC form as possible. That means we can still provide revisioning on unfiltered collections, which will might meet the needs of most users doing content preview on collections (e.g. decoupled "most recent articles" components).Note: this patch is also re-rolled against the latest 2.x branch.
Comment #284
gabesulliceComment #285
wim leersWFM!
Comment #286
wim leersBefore committing, I wanted to run this to pass tests against not just 8.6, but also 8.5 and 8.7. It failed on 8.5 because
\Drupal\Tests\content_moderation\Traits\ContentModerationTestTraitonly exists in >=8.6, it was introduced in #2952307: Move workflow config into standard profile.So, rerolling this patch with that trait added as another backward compatibility thing (similar to
\Drupal\jsonapi\BackwardCompatibility\tests\Traits\EntityReferenceTestTrait).Actually … we already did this in #256. #262 apparently accidentally dropped that, and from that point forward we only tested with Drupal 8.6 & 8.7… So, bringing back #256's interdiff.
Comment #288
wim leersGreen on all 3 core minors. ✅
We had explicit "ayes" from @gabesullice and @e0ipso to commit this late last night, so … let's do this! 💪
Comment #289
barbun commentedI had to re-apply #52 against the latest 1.x version and thought I'd post the updated patch. Obviously, be mindful that there might be things that would not work anymore and its an interim fix before assessing and upgrading to 2.x
Comment #290
wim leersHah … my research comment in #61 that covered CMIS and led me to less-than-favorable conclusions apparently was itself skeptically welcomed: https://roy.gbiv.com/untangled/2008/no-rest-in-cmis.
Comment #291
wim leersThis unfortunately also caused a regression on PHP 5.5: #3027501-7: [regression] Follow-up for #2995960 and #2992833: syntax errors in PHP 5.5 & 5.6.
Comment #294
rpayanmMoving to Drupal core's issue queue.
I'm working on https://www.drupal.org/project/drupal/issues/3122113