-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Luke Sneeringer
committed
May 19, 2021
1 parent
17ef1fc
commit ba39650
Showing
3 changed files
with
489 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
# Resource Revisions | ||
|
||
Some APIs need to have resources with a revision history, where users can | ||
reason about the state of the resource over time. There are several reasons for | ||
this: | ||
|
||
- Users may want to be able to roll back to a previous revision, or diff | ||
against a previous revision. | ||
- An API may create data which is derived in some way from a resource at a | ||
given point in time. In these cases, it may be desirable to snapshot the | ||
resource for reference later. | ||
|
||
**Note:** We use the word _revision_ to refer to a historical reference for a | ||
particular resource, and intentionally avoid the term _version_, which refers | ||
to the version of an API as a whole. | ||
|
||
## Guidance | ||
|
||
APIs **may** store a revision history for a resource if it is useful to users. | ||
|
||
APIs implementing resources with a revision history **must** provide a | ||
`revision_id` field on the resource: | ||
|
||
```typescript | ||
interface Book { | ||
// The ID of the book. | ||
id: string; | ||
|
||
// Other fields… | ||
|
||
// The revision ID of the book. | ||
// A new revision is committed whenever the book is changed in any way. | ||
// The format is an 8-character hexadecimal string. | ||
readonly revisionId: string; | ||
|
||
// The timestamp that the revision was created. | ||
readonly revisionCreateTime: string; // ISO 8601 | ||
} | ||
``` | ||
|
||
- The resource **must** contain a `revision_id` field, which **should** be a | ||
string and contain a short, automatically-generated random string. A good | ||
rule of thumb is the last eight characters of a UUID4. | ||
- The `revision_id` field **must** document when new revisions are created | ||
(see [committing revisions](#committing-revisions) below). | ||
- The `revision_id` field **should** document the format of revision IDs. | ||
- The resource **must** contain a `revision_create_time` field, which | ||
**should** be a timestamp (see AIP-142). | ||
|
||
**Note:** A randomly generated string is preferred over other operations, such | ||
as an auto-incrementing integer, because there is often a need to delete or | ||
revert revisions, and a randomly generated string holds up better in those | ||
situations. | ||
|
||
### Referencing revisions | ||
|
||
When it is necessary to refer to a specific revision of a resource, APIs | ||
**must** use the following syntax: `{resource_id}@{revision_id}`. For example: | ||
|
||
publishers/123/books/les-miserables@c7cfa2a8 | ||
|
||
**Note:** The `@` character is selected because it is the only character | ||
permitted by [RFC 1738 §2.2][] for special meaning within a URI scheme that is | ||
not already used elsewhere. | ||
|
||
APIs **should** generally accept a resource reference at a particular revision | ||
in any place where they ordinarily accept the resource ID. However, they **must | ||
not** accept a revision in situations that mutate the resource, and should | ||
error with `400 Bad Request` if one is given. | ||
|
||
**Important:** APIs **must not** require a revision ID, and **must** default to | ||
the current revision if one is not provided, except in operations specifically | ||
dealing with the revision history (such as rollback) where failing to require | ||
it would not make sense (or lead to dangerous mistakes). | ||
|
||
### Getting a revision | ||
|
||
APIs implementing resource revisions **should** accept a resource ID with a | ||
revision ID in the standard `Get` operation (AIP-131): | ||
|
||
{% tab proto %} | ||
|
||
{% sample 'revisions.proto', 'message GetBookRequest' %} | ||
|
||
{% endtabs %} | ||
|
||
If the user passes a revision ID that does not exist, the API **must** fail | ||
with a `404 Not Found` error. | ||
|
||
APIs **must** return a `id` value corresponding to what the user sent. If the | ||
user sent a resource ID with no revision ID, the returned `id` string **must | ||
not** include the revision ID either. Similarly, if the user sent a resource ID | ||
with a revision ID, the returned `id` string **must** explicitly include it. | ||
|
||
### Tagging revisions | ||
|
||
APIs implementing resource revisions **may** provide a mechanism for users to | ||
tag a specific revision with a user provided name by implementing a "Tag | ||
Revision" custom operation: | ||
|
||
{% tab proto %} | ||
|
||
{% sample 'revisions.proto', 'rpc TagBookRevision', 'message TagBookRevisionRequest' %} | ||
|
||
{% endtabs %} | ||
|
||
- The `id` field **should** require an explicit revision ID to be provided. | ||
- The field **should** be [annotated as required][aip-203]. | ||
- The field **should** identify the [resource type][aip-123] that it | ||
references. | ||
- The `tag` field **should** be [annotated as required][aip-203]. | ||
- Additionally, tags **should** restrict letters to lower-case. | ||
- Once a revision is tagged, the API **must** support using the tag in place of | ||
the revision ID in `id` fields. | ||
- If the user sends a tag, the API **must** return the tag in the resource's | ||
`id` field, but the revision ID in the resource's `revision_id` field. | ||
- If the user sends a revision ID, the API **must** return the revision ID in | ||
both the `id` field and the `revision_id` field. | ||
- If the user calls the `Tag` operation with an existing tag, the request | ||
**must** succeed and the tag updated to point to the new requested revision | ||
ID. This allows users to write code against specific tags (e.g. `published`) | ||
and the revision can change in the background with no code change. | ||
|
||
### Listing revisions | ||
|
||
APIs implementing resource revisions **should** provide a custom operation for | ||
listing the revision history for a resource, with a structure similar to | ||
standard `List` operations (AIP-132): | ||
|
||
{% tab proto %} | ||
|
||
{% sample 'revisions.proto', 'rpc ListBookRevisions', 'message ListBookRevisionsRequest' %} | ||
|
||
{% endtabs %} | ||
|
||
While revision listing operations are mostly similar to standard `List` | ||
operations (AIP-132), the following important differences apply: | ||
|
||
- The first field in the request message **must** be called `id` rather than | ||
`parent` (this is listing revisions for a specific book, not a collection of | ||
books). | ||
- The URI **must** end with `:listRevisions`. | ||
- Revisions **must** be ordered in reverse chronological order. An `order_by` | ||
field **should not** be provided. | ||
- The returned resources **must** include an explicit revision ID in the | ||
resource's `id` field. | ||
- If providing the full resource is expensive or infeasible, the revision | ||
object **may** only populate the `id`, `revision_id`, and | ||
`revision_create_time` fields instead. The `id` field **must** include the | ||
resource ID and an explicit revision ID, which can be used for an explicit | ||
`Get` request. The API **must** document that it will do this. | ||
|
||
### Child resources | ||
|
||
Resources with a revision history **may** have child resources. If they do, | ||
there are two potential variants: | ||
|
||
- Child resources where each child resource is a child of the parent resource | ||
as a whole. | ||
- Child resources where each child resource is a child of _a single revision | ||
of_ the parent resource. | ||
|
||
If a child resource is a child of a single revision, the child resource's name | ||
**must** always explicitly include the parent's resource ID: | ||
|
||
publishers/123/books/les-miserables@c7cfa2a8/pages/42 | ||
|
||
In `List` requests for such resources, the service **should** default to the | ||
latest revision of the parent if the user does not specify one, but **must** | ||
explicitly include the parent's revision ID in the `id` field of resources in | ||
the response. | ||
|
||
If necessary, APIs **may** explicitly support listing child resources across | ||
parent revisions by accepting the `@-` syntax. For example: | ||
|
||
GET /v1/publishers/123/books/les-miserables@-/pages | ||
|
||
APIs **should not** include multiple levels of resources with revisions, as | ||
this quickly becomes difficult to reason about. | ||
|
||
### Committing revisions | ||
|
||
Depending on the resource, different APIs may have different strategies for | ||
when to commit a new revision, such as: | ||
|
||
- Commit a new revision any time that there is a change | ||
- Commit a new revision when something important happens | ||
- Commit a new revision when the user specifically asks | ||
|
||
APIs **may** use any of these strategies. APIs that want to commit a revision | ||
on user request **should** handle this with a `Commit` custom operation: | ||
|
||
{% tab proto %} | ||
|
||
{% sample 'revisions.proto', 'rpc CommitBook', 'message CommitBookRequest' %} | ||
|
||
{% endtabs %} | ||
|
||
- The operation **must** use the `POST` HTTP method. | ||
- The operation **should** return the resource, and the resource ID **must** | ||
include the revision ID. | ||
- The request message **must** include the `id` field. | ||
- The field **should** be [annotated as required][aip-203]. | ||
- The field **should** identify the [resource type][aip-123] that it | ||
references. | ||
|
||
### Rollback | ||
|
||
A common use case for a resource with a revision history is the ability to roll | ||
back to a given revision. APIs **should** handle this with a `Rollback` custom | ||
operation: | ||
|
||
{% tab proto %} | ||
|
||
{% sample 'revisions.proto', 'rpc RollbackBook', 'message RollbackBookRequest' %} | ||
|
||
{% endtabs %} | ||
|
||
- The operation **must** use the `POST` HTTP method. | ||
- The operation **should** return the resource, and the resource ID **must** | ||
include the revision ID. | ||
- The request message **must** have a `id` field to identify the resource being | ||
rolled back. | ||
- The field **should** be [annotated as required][aip-203]. | ||
- The field **should** identify the [resource type][aip-123] that it | ||
references. | ||
- The request message **must** include a `revision_id` field. | ||
- The API **must** fail the request with `NOT_FOUND` if the revision does not | ||
exist on that resource. | ||
- The field **should** be [annotated as required][aip-203]. | ||
|
||
**Note:** When rolling back, the API should return a _new_ revision of the | ||
resource with a _new_ revision ID, rather than reusing the original ID. This | ||
avoids problems with representing the same revision being active for multiple | ||
ranges of time. | ||
|
||
### Deleting revisions | ||
|
||
Revisions are sometimes expensive to store, and there are valid use cases to | ||
want to remove one or more revisions from a resource's revision history. | ||
|
||
APIs **may** define a operation to delete revisions, with a structure similar | ||
to `Delete` (AIP-135) operations: | ||
|
||
{% tab proto %} | ||
|
||
{% sample 'revisions.proto', 'rpc DeleteBookRevision', 'message DeleteBookRevisionRequest' %} | ||
|
||
{% endtabs %} | ||
|
||
- The request message **must** have a `id` field to identify the resource | ||
revision being deleted. | ||
- The explicit revision ID **must** be required (the operation **must** fail | ||
with `INVALID_ARGUMENT` if it is not provided, and **must not** default to | ||
the latest revision). | ||
- The field **should** be [annotated as required][aip-203]. | ||
- The field **should** identify the [resource type][aip-123] that it | ||
references. | ||
- The API **must not** overload the `DeleteBook` operation to serve both | ||
purposes (this could lead to dangerous or confusing mistakes). | ||
- If the resource supports soft delete, then revisions of that resource | ||
**should** also support soft delete. | ||
- The operation **must not** cause the _resource_ to be deleted. | ||
- Because a resource **should not** exist with zero revisions, the operation | ||
**must** fail with `FAILED_PRECONDITION` if the user attempts to delete the | ||
only revision. | ||
|
||
### Character Collision | ||
|
||
Most resource IDs have a restrictive set of characters, but some are very open. | ||
For example, Google Cloud Storage allows the `@` character in filenames, which | ||
are part of the resource ID, and therefore uses the `#` character to indicate a | ||
revision. | ||
|
||
APIs **should not** permit the `@` character in resource IDs, and if APIs that | ||
do permit it need to support resources with revisions, they **should** pick an | ||
appropriate separator depending on how and where the API is used, and **must** | ||
clearly document it. | ||
|
||
[rfc 1738 §2.2]: https://tools.ietf.org/html/rfc1738 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
id: 162 | ||
state: approved | ||
created: 2019-09-17 | ||
placement: | ||
category: design-patterns | ||
order: 88 |
Oops, something went wrong.