Problem/Motivation
We want to support revisions in order to provide a fully featured editorial experience and support content preview.
Proposed resolution(s)
Terminology
We will be following the RFC 5829 from the IETF. That means that:
- We will refer to the current/published revision as:
"working-copy" - We will refer to the latest revision as:
"latest-version"
Revision Negotiation
We will enable revision negotiation by supporting a custom query string parameter with the name of "resource_type". Consumers will transfer the desired revision by setting the value in this query string parameter.
We will introduce a new plugin type that will enable different ways to specify a revision. This way we will be able to do https://example.org/jsonapi/node/article/f6da07d8-eb1a-4422-b19e-d209a408b7d6?resource_version=rel:latest-version to load the latest version, https://example.org/jsonapi/node/article/f6da07d8-eb1a-4422-b19e-d209a408b7d6?resource_version=id:6 to load revision with ID 6, https://example.org/jsonapi/node/article/f6da07d8-eb1a-4422-b19e-d209a408b7d6?resource_version=date:2018-01-09T13:08:22Z to load the revision that was in effect on that particular date.
In addition to the plugin type, we'll implement the id:{revision-ID} and the rel:{(working-copy|latest-version|…)} (although initially we'll only support "latest-version" and "working-copy").
- DONE: #2992833: Add a version negotiation to revisionable resource types
- TODO: #3031271: Support version negotiation for any entity type (currently only Node & Media are supported)
Revision Discovery
As stated in RFC 5829 we will need to add links to the JSON API entities (both collections and individual entities) so consumers can discover the URLs for the revisions. That includes the presence of a Link HTTP header and an entry in the "links" section in the response payload.
- DONE: #2992836: Provide links to resource versions (entity revisions)
- TODO: #3009588: Provide a collection resource where a version history can be obtained (`version-history`, `predecessor-version` and `successor-version` link relations)
Revisions on Collections
Due to the nature of collections we can't ensure correct support for all revision ID negotiation plugins. For instance we know we can't support the ID plugin (because each item requires a different revision_id). However we can easily support Rel with "latest-version" and "working-copy" because they apply to any entities and Entity Query supports them (yay!).
Full CRUD on Revisions
This issue does not address write operations like deleting a revision, updating a particular revision, etc.
- DONE: #3037989: Test coverage with and without Content Moderation: POST/PATCH/DELETE revisions results in 400, auto-revisioning PATCH with CM, PATCH/DELETE when pending revision exists
- DONE: #3038453: When RevisionableEntityBundleInterface::shouldCreateNewRevision is TRUE, create a new revision on PATCH requests and add an entry in the revision log.
- TODO: #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API.
Note: Place here the issue to plan Full CRUD on Revisons. Per #2350939-151: Implement a generic revision UI, this needs a generic revision access API, which doesn't yet exist for reading (also not for writing). The issue for that is #3043321: Use generic access API for node and media revision UI.
Comments
Comment #2
David Hernández commentedThere is a PR open on the JSON API specification about adding revisions as part of a resource. Is not closed yet, but it might help us to figure out how will work and how we can do it:
https://github.com/json-api/json-api/pull/824
Comment #3
e0ipsoComment #4
wim leershttps://github.com/json-api/json-api/pull/824 hasn't gone anywhere in a year's time. Last comment in May.
Comment #5
e0ipsoRemoving beta blocker.
Comment #6
e0ipsoI think it would be interesting to have something in this regard. Revisions are important in Drupal.
Comment #7
e0ipsoThere is a neat suggestion to this in #2832164: JSON API: forward compatibility with Workspace module.
Comment #8
Chris Gillis commentedSo to confirm, Is it true that there is no way to GET the latest revision (or a specific revision) using JSONAPI?
Comment #9
e0ipsoNo. Only the latest published revision is returned.
Comment #10
Chris Gillis commentedThank you. I'll go ahead and write a custom endpoint.
Comment #11
wim leersThis is blocked on https://github.com/json-api/json-api/pull/824.
Comment #12
gabesulliceWhile responding to a core issue, I had some thoughts about how we could handle this. I've updated the IS with my proposal.
See: #2937850-5: Mark revision_default as internal for REST consumers
I think we have both advantages and disadvantages when it comes to supporting revisions when compared to core REST. I think it's important that we're part of the conversation so that we don't get boxed into a corner if things become to REST-module-centric.
Comment #13
gabesulliceFixed some formatting issues.
Comment #14
gabesulliceAgain...
Comment #15
e0ipsoHow most people I talked about this feature think about this is in terms of temporal anchoring, like a preview system. We will not be able to provide the same feature set we have now but for revisions.
For me that's enough reason to suggest to implement this in a separate contrib. Our main problems are: non-revisionable entity references make traversing a data tree at a given revision snapshot impossible, nested filters across revisions are a sci-fi atm (even nested filters for a revision other than default), spec shortcomings (like everything is referenced by ID, no room for alternate IDs, we'd need to swap the uuid for the vuuid or get creative), …
This is what people expect: Give me all the revisions of pages where the related article had/will have a tag with the name "foo" and exclude revisions older than vid:78..
Comment #16
wim leersI'm glad people are already starting to think about this (go @gabesullice!) but for me personally, there's just too much other stuff we have tackle/stabilize first.
I also think translations are a bigger priority. They're A) more doable, B) at least partially working in core REST already, C) we'll need to handle revisions+translations eventually, so it's important we take "revisions of translations" into account while working on revisions!
Comment #17
valderama commentedHi there!
I reading this issue with great interest. I seems that a general solution to support revisions in JSONAPI is currently quite complex. However I wonder if there might me quick and dirty way to alter the JSONAPI output to return the latest draft version instead of the public version of a node.
Would you suggest trying to alter JSONAPI output or better go with a custom endpoint to get the latest draft version of a node?
Thanks in advance!
Walter
Comment #18
wim leersFor now, I personally think a custom endpoint is probably better, because then it's very explicit that this is an upstream feature that you need.
Comment #19
Snugug commented@Wim translations already, surprisingly, work with JSOON:API. If you prepend the langcode in front of the API endpoint, it'll return the translated content!
/api/node--foo/12345returns the default translation,/fr/api/node--foo/12345returns the french!w/r/t revisions, given that the referenced issue hasn't been updated in over 2 years, maybe its worthwhile experimenting a little? I think the
put/patchusecase for revisions is much harder than theget</em> usecase, and we can probably bikeshed <em>a little</em> to come to a first-pass implementation of <code>getting a specific revision.Comment #20
e0ipsoYeah, GETs for the translations have been stable for a while now after #2794431-31: [META] Formalize translations support got merged. However it's very hard to provide a translation for existing content in a decoupled environment.
Comment #21
wim leersThat's the same way it "works" for core's
rest.module: it works, but only semi-accidentally. There's no test coverage. I can seeGETworking fine, but I doubtPATCH,POSTandDELETEwork.Absolutely! That's exactly what's necessary. Nobody has simply started working on it. The reason I haven't prioritized it yet (as one of the API-First Initiative coordinators) is that there
iswas SO MUCH other stuff that was broken in simple cases (not involving revisions/translations). It's now finally getting to a point where things are okayish. Compare #2941316: REST: top priorities for Drupal 8.6.x with #2721489: REST: top priorities for Drupal 8.2.x to see how far we've come (there are summaries at the end of every one of those "top priorities" issues).I can't wait to see your experiments with revision support! 😄
:partyparrot:Comment #22
wim leersComment #23
TikaL13 commented@Wim... you mention creating a custom endpoint to hit for displaying nodes in a "draft" or "unpublished" state/status. How would one go about doing so?
My current setup is such:
D8.5.3
Bricks Module
Jsonapi Module
Jsonapi Extras
Eck
Content Moderation
Vuejs on the FE
I have a content type called "Dynamic" where I have a Bricks field that references ECK entity types. The problem that I have at the moment are edits that are made inline to the ECK entity type no matter if I set the page to "draft" status the GET request is always displaying the latest. So edits that should not be displayed are. Is there a way to check for the current revision and or the status of the ECK entity type referenced within the page?
Comment #24
wim leers@TikaL13: your question is out of scope here. You want to create a new REST resource plugin. See https://www.drupal.org/docs/8/api/restful-web-services-api/restful-web-s... for getting started. This issue is specifically about revision support for JSON API. If you still have more questions about this, please create a issue.
Comment #25
wim leersIdeally, the JSON API spec would describe this for us. But I don't think we can wait for https://github.com/json-api/json-api/pull/824 to happen. It'll take too long. We should implement it already, and do so in a way that allows the spec to still define it in the future.
Let's do this! 🤘 🚀
Comment #26
e0ipsoYEAH!
Comment #27
e0ipsoIt may be worth researching the Memento protocol for this.
This seems to put us on the track of standarization on this feature.
Comment #28
wim leers#27 sounds SUPER fascinating! 😍 Thank you! 🙏
Comment #29
gabesulliceI really like the idea of getting to a point where we can implement the memento protocol. It's a great concept.
But I think it's getting a little ahead of ourselves. A prerequisite to negotiating revisions at a particular time means that those revisions are actually identified by a URI.
- https://tools.ietf.org/html/rfc7089#section-1.2
In essence, each memento needs a URI.
To start, we first need to be able to GET different versions of a resource. From there, we can add a way to negotiate which of those versions corresponds to a datetime.
FWIW, even though I put URLs in the IS, I don't think that's the best way any longer.
I'd like to propose a query parameter based strategy with heavy inspiration from this RFC: 5829: Link Relation Types for Simple Version Navigation between Web Resources
For example,
/jsonapi/node/article?resource_version={type}:{value}where {type} is a pluggable strategy for evaluating {value} to a specific revision ID. The default strategies might be:id,relanddatetime. ID would obviously just be the raw revision ID,relwould use the link relation types registered by RFC 5829 to resolve a revision ID, anddatetimewould resolve the revision ID of the resource that was canonical at the given datetime.So, using the
relstrategy, one could GET/jsonapi/node/article?resource_version=rel:latestor/jsonapi/node/article?resource_version=rel:predecessor-version:working-copyor/jsonapi/node/article?resource_version=rel:predecessor-version:{int}to get the predecessor version of the latest draft or the predecessor version of the given revision ID.Comment #30
e0ipso@gabesullice++
I think that is an accurate summary of the agreement reached in the phone call earlier today.
I think that an MVP should only implement
relforlatestandworking-copy, andid. Other rels and datetime can be a follow up.Comment #31
wim leersThanks for summarizing the key discussion items and outcomes in the call!
Comment #32
skyredwangwow, indeed, those proposed endpoints are magical, I can see them very useful in many scenarios!
Comment #33
wim leers@skyredwang Wonderful to have you chiming in! I'd love to hear from you in a separate issue how JSON API has been working for you in the past few months, as we've been working to stabilize it, expand test coverage, and so on.
Comment #34
e0ipsoMaking this a meta. We are about to start sending patches for partial support on this.
Comment #35
justageek commentedComment #36
justageek commentedComment #37
e0ipsoWorked on the summary to reflect the consensus and start progress.
Comment #38
e0ipsoComment #39
wim leers@pwolanin created #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API..
Comment #40
meangreen commentedThis is a great feature, just curious when would this be implemented (time range)?
I realize the team has all these other issues and stuff to work on. My boss wants this feature though, so I just want to give him a ballpark, (not official estimate) about when this might be released.
Comment #41
e0ipso@dazdiggityz it's great to hear that this feature is useful. Will your boss be interested in sponsoring further development on this? Otherwise this is like any other open source project. It'll happen when it happens, there's no way to predict volunteer availability.
Comment #42
meangreen commentedHe might let me work on it on my own for a day or two, or just here and there. Right now we have "bigger fish to fry" for the next coming weeks for this sprint, but I might have time to contribute after that depending on how things go. I'll let you know in a few weeks.
Right now, I think* his way of sponsoring will be like telling me "go and hotwire the jsonapi to make it do what I want it to do" type of thing :D
Comment #43
wim leers#2992833 just reached RTBC in #2992833-279: Add a version negotiation to revisionable resource types!
Next: #2992836-15: Provide links to resource versions (entity revisions), which was blocked on that. And then: #3009588: Provide a collection resource where a version history can be obtained (`version-history`, `predecessor-version` and `successor-version` link relations).
Comment #44
wim leers#2992833: Add a version negotiation to revisionable resource types landed!
Comment #45
gabesullice#2992836: Provide links to resource versions (entity revisions) landed!
Comment #46
wim leersYay :)
I think we need an updated issue summary here, to indicate what is blocking the remainder. Because AFAICT neither the CUD in CRUD nor version history are achievable with the current set of core APIs?
Comment #47
wim leersUpdated.
The most often needed functionality wrt revisions has shipped! 🥳
What remains is:
MediaandNode, blocked on #2350939: Implement a generic revision UI in coreI think we should consider creating two concrete feature request issues for points 2 and 3 and then close this issue? On the other hand, it may be useful to keep this around :) For now, keeping it around and marking it explicitly postponed on those two core issues.
Comment #48
wim leersComment #49
xjmSomething not captured in in #47 is revision creation is an essential part of updating a revisionable entity for entity types that are revisionable. @Wim Leers, @gabesullice, @effulgentsia, and I discussed this today.
This mismatch could be considered data loss for sites that require a new revision to be created every time content is updated. It could also potentially lead to orphaned revisions if the latest revision is not the default revision.
I promoted #2838395: [PP-1] Entity API bug affecting REST: "last changed" timestamp not updated and "create new revision" setting not respected when PATCHing via REST and #1863258: Move entity preparation from form controller to entity class: "last changed" timestamp not updated and "create new revision" setting not respected when updating entities via Entity API to critical based on the discussion.
Comment #50
larowlanContent moderation makes sure that is set by default for all bundles - see \Drupal\content_moderation\Entity\Handler\NodeModerationHandler::enforceRevisionsBundleFormAlter and \Drupal\content_moderation\Entity\Handler\NodeModerationHandler::enforceRevisionsEntityFormAlter for the form alters and \Drupal\content_moderation\Entity\Handler\ModerationHandler::onPresave for the non-form route.
Comment #51
wim leers#50: that
::onPresave()is crucial here. Related: @hchonov's comments at #1863258-35: Move entity preparation from form controller to entity class: "last changed" timestamp not updated and "create new revision" setting not respected when updating entities via Entity API and #1863258-37: Move entity preparation from form controller to entity class: "last changed" timestamp not updated and "create new revision" setting not respected when updating entities via Entity API.Comment #52
wim leersIssue created: #3037989: Test coverage with and without Content Moderation: POST/PATCH/DELETE revisions results in 400, auto-revisioning PATCH with CM, PATCH/DELETE when pending revision exists.
Comment #53
xjmI discussed this issue with @larowlan yesterday. One thing @larowlan pointed out about the "safe" revisionable types (Nodes and Media) defined in
\Drupal\jsonapi\ResourceType\ResourceTypeRepository::isVersionableResourceTypewas:We'd also like to see hard failures instead of partial support for things that aren't supported, and test coverage.
@gaborhojtsy listed out these concerns for the translation support meta, but I think they apply here as well:
So, i.e., do we have test coverage:
Comment #54
wim leers\Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRevisions().\Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRevisions()+ #3037989: Test coverage with and without Content Moderation: POST/PATCH/DELETE revisions results in 400, auto-revisioning PATCH with CM, PATCH/DELETE when pending revision exists\Drupal\Tests\jsonapi\Functional\ResourceTestBase::testPatchIndividual()and #3037989: Test coverage with and without Content Moderation: POST/PATCH/DELETE revisions results in 400, auto-revisioning PATCH with CM, PATCH/DELETE when pending revision existsComment #55
wim leers… because overriding that would be extremely insecure, since there's no revision access control API yet:
\ResourceTestBase::testRevisions()was already doing much of this. #3037989: Test coverage with and without Content Moderation: POST/PATCH/DELETE revisions results in 400, auto-revisioning PATCH with CM, PATCH/DELETE when pending revision exists added more.Comment #56
xjmThanks @Wim Leers!
Re: #55:
The point I believe is that a contrib module could easily write its own access control e.g. for Block Content, but would have no way to add support for it.
Comment #57
xjmSorry, I don't think #55 is necessarily a blocker; it's just something that came up in review and may need an issue.
Comment #58
wim leersThey could not "easily" write this. Precisely because there is no API for this.
class NodeRevisionAccessCheck implements AccessInterface {…}andclass MediaRevisionAccessCheck implements AccessInterface {…}are route access checks that happen to be written in a way that we can call them too.There is no interface for a contrib module to implement, no class to subclass, no place to inform Drupal that a certain class is supposed to be used for. If there were an
EntityAccessControlHandler-like structure for checking revisions too, we'd definitely support it. We already had to go the extra mile to supportNodeandMediasince there is no official revision access checking API. Workflow Initiative lead and Entity API maintainer @amateescu confirmed this in #2992833-151: Add a version negotiation to revisionable resource types.We already have an explicit issue for this: #3031271: Support version negotiation for any entity type (currently only Node & Media are supported). Updated the issue summary to point to that instead.
EDIT: heh, I already had a response for your new comment that got posted after I started writing this:
So: that issue exists 🙂
Comment #59
wim leershttps://www.drupal.org/docs/8/modules/jsonapi/revisions documents how to retrieve a revision of an entity via JSON:API using either a specific revision ID or a RFC5829 link relation type. (Change record: https://www.drupal.org/node/3015434.)
No, all necessary @todos are already present.
GETting revisions for all revisionable entity types, and mutating revisions once that infrastructure exists.NodeandMedia; it ensures an precise and helpful error response is sent for other entity types.POST,PATCH,DELETE(i.e. anything other than reading) results in an error response.Comment #60
xjmThere are at least two things that are potentially broken as per our call: The existing revision being updated instead of a new one being created when CM is not enabled, and orphaned revisions potentially being created when an entity is updated (needs testing/confirmation). These are the things @effulgentsia raised and they are serious bugs. Maybe we can get @effulgentsia to help with testing and documenting potential issues also.
Comment #61
xjmTo be clear, we need to test (automated tests, but also manually) what happens with updating not only supported things like nodes but also something like block_content, which is revisioned by default, but json_api doesn't support revisions for.
Those two things are what I'm concerned about. They should be tested with both nodes and block content, both with and without content moderation.
Comment #62
gabesulliceI think you may have a bit of confusion WRT what our support for node and media is :) Let me try to clear that up.
The extent of any special handling of revisions (per entity type) in JSON:API is limited to
GETing a particular revision by its ID (or a shortcut, like "working-copy" whichGETs the latest revision) using a query parameter. For example,/jsonapi/node/article/{uuid}?resourceVersion=id:42. It's impossible to use this query parameter withPOST,PATCHorDELETE. For all other entities, JSON:API behaves in exactly the same way (including block_content):ParamConverterprovides by default400 Bad Requestif not)This behavior is exactly the same all entities of any type (node and block_content included).
Additionally, it's explicitly disallowed to
PATCHany entity using theresourceVersionquery parameter (which works only for node and media). This was given explicit test coverage in #3037989: Test coverage with and without Content Moderation: POST/PATCH/DELETE revisions results in 400, auto-revisioning PATCH with CM, PATCH/DELETE when pending revision exists.#61.1a:
JSON:API does not call
setNewRevision()on any entity, regardless of any form defaults.However, when Content Moderation is installed, CM will execute a
hook_entity_presavewhich does create new revisions under certain circumstances, according to its own logic. We have no input in that process.#61.1b:
Without CM installed, the revision ID is never incremented. This is explicitly tested in #3037989 too. I'm not sure that we can be certain of what the site's expectations are, given that the setting you're talking about explicitly says it's a form default. Any developer developing against JSON:API would quickly realize that new revisions are not being created. We have an existing issue to make it possible to create new revisions according to a more clear set of expectations: #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API.. Right now, it is not clear what user expectations are.
With CM installed, the behavior depends on whether the entity type has a moderation handler (which causes the
presavebehavior I described above). If it does have a moderation handler, it is explicitly tested that the revision is incremented. If it does not, then it asserted that the revision ID is not incremented:#61.2
This is explicitly tested in #3037989: Test coverage with and without Content Moderation: POST/PATCH/DELETE revisions results in 400, auto-revisioning PATCH with CM, PATCH/DELETE when pending revision exists. Attempting to
PATCHany revisionable entity, including block_content, that is not the default and latest revision will result in a400 Bad Request.All the
PATCHtests apply to all entities and the revision specific bits apply to all revisionable entities, included node and block content.It's impossible to test the #61.2 case without content moderation, because it's impossible to have a default revision that is not the latest revision unless CM is installed.
Comment #63
xjmThanks @gabesullice!
Regarding:
👍
Then, regarding:
Until the upstream bug is fixed, I think we should go the other direction and always increment the revision ID. Extra revisions are better than lost revisions from a risk management perspective.
Then hopefully, we might (as a temporary measure, until the entity itself has the field for this) check the bundle settings for the default value of the checkbox, and obey that always.
Whether or not we check the bundle setting, we could form alter a message onto the existing "create new revision by default" checkboxes informing the user that JSON:API will ignore the user's choice if the user overrides the default (or always if we can't check the bundle setting).
Comment #64
gabesulliceUnassigning myself.
Comment #65
wim leers#61 has been addressed in #3038453: When RevisionableEntityBundleInterface::shouldCreateNewRevision is TRUE, create a new revision on PATCH requests and add an entry in the revision log., after @plach RTBC'd it! 🎉
Comment #66
larowlanAdded an issue to address the revision access api, as the UI one shouldn't be the place we do that, for the sake of scope
Comment #67
wim leers+1
We linked to #2350939: Implement a generic revision UI for that, since that's what @amateescu linked to, but #2795279: [PP-2] [META] Revisions support is definitely a better fit: tighter scope.
Updated issue summary. Thanks!
Comment #68
wim leersThe second link in #67 should of course have been #3043321: Use generic access API for node and media revision UI, not #2795279: [PP-2] [META] Revisions support 😅
Comment #69
wim leersAdding #2993557: Allow optional creation of new revision when PATCHing revisionable entities to support autosave functionality via JSON:API. to .
Comment #70
wim leersComment #71
wim leersWe need to clear up whether #3009588: Provide a collection resource where a version history can be obtained (`version-history`, `predecessor-version` and `successor-version` link relations) truly is blocked or not. But even then, this is blocked on at least 4 issues: three direct blockers, one indirect blocker.
Comment #76
larowlan#3043321: Use generic access API for node and media revision UI landed
Comment #80
mustanggb commented#2350939: Implement a generic revision UI is fixed.