-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(revisions): adopt aep-162 revisions (#200)
Adapted from aip.dev/162. list of changes include: - changing references to "name" to "path". - minor formatting changes like consolidating proto examples so we can minimize the proto/openapi tabs. - removed some sections from rationale that spoke to aip.dev more than aeps, and didn't provide valuable context (like the existence of "tag" in older versions of the pattern). - added openapi schemas. --------- Co-authored-by: Richard Frankel <[email protected]> Co-authored-by: dv-stewarts <[email protected]>
- Loading branch information
1 parent
b1775be
commit 0982852
Showing
2 changed files
with
390 additions
and
5 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 |
---|---|---|
@@ -1,5 +1,390 @@ | ||
# Resource Revisions | ||
|
||
**Note:** This AEP has not yet been adopted. See | ||
[this GitHub issue](https://github.com/aep-dev/aep.dev/issues/16) for more | ||
information. | ||
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** expose a revision history for a resource. Examples of when it is | ||
useful include: | ||
|
||
- When it is valuable to expose older revisions of a resource via an API. This | ||
can avoid the overhead of the customers having to write their own API to store | ||
and enable retrieval of revisions. | ||
- Other resources depend on or descend from different revisions of a resource. | ||
- There is a need to represent the change of a resource over time. | ||
|
||
APIs implementing resources with revisions **must** model resource | ||
revisions as a subresource of the resource. | ||
|
||
{% tab proto %} | ||
|
||
```proto | ||
message BookRevision { | ||
// The path of the book revision. | ||
string path = 1; | ||
|
||
// The snapshot of the book | ||
Book resource = 2 | ||
[(google.api.field_behavior) = OUTPUT_ONLY]; | ||
|
||
// The timestamp that the revision was created. | ||
google.protobuf.Timestamp create_time = 3 | ||
[(google.api.field_behavior) = OUTPUT_ONLY]; | ||
|
||
// Other revision IDs that share the same snapshot. | ||
repeated string aliases = 4 | ||
[(google.api.field_behavior) = OUTPUT_ONLY]; | ||
} | ||
``` | ||
|
||
- The `message` **must** be annotated as a resource (AIP-123). | ||
- The `message` name **must** be named `{ResourceType}Revision`. | ||
|
||
{% tab oas %} | ||
|
||
```json | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "object", | ||
"properties": { | ||
"path": { | ||
"type": "string" | ||
}, | ||
"resource": { | ||
"$ref": "#/definitions/Book", | ||
"readOnly": true, | ||
}, | ||
"create_time": { | ||
"type": "string", | ||
"format": "date-time", | ||
"readOnly": true, | ||
}, | ||
"aliases": { | ||
"type": "array", | ||
"items": { | ||
"type": "string" | ||
}, | ||
"readOnly": true, | ||
} | ||
}, | ||
"required": ["path"], | ||
} | ||
``` | ||
|
||
{% endtabs %} | ||
|
||
- The resource revision **must** contain a field with a message type of the | ||
parent resource, with a field name of `resource`. | ||
- The value of `resource` **must** be the original resource | ||
at the point in time the revision was created. | ||
- The resource revision **must** contain a `create_time` field (see [AIP-142][]). | ||
- The resource revision **may** contain a repeated field `aliases`, which would | ||
contain a list of resource IDs that the revision is also known by (e.g. `latest`) | ||
|
||
### Creating Revisions | ||
|
||
Depending on the resource, different APIs may have different strategies for | ||
creating a new revision. Specifically examples of strategies include: | ||
|
||
- Creating a revision when there is a change to the resource | ||
- Creating a revision when important system state changes | ||
- Creating a revision when specifically requested | ||
|
||
APIs **may** use any of these strategies or any other strategy. APIs **must** | ||
document their revision creation strategy. | ||
|
||
Resources that support revisions **should** always have at least one revision. | ||
|
||
### Resource names for revisions | ||
|
||
When referring to the revisions of a resource, the subcollection name | ||
**must** be `revisions`. Resource revisions have paths with the format | ||
`{resource_path}/revisions/{resource_revision}`. For example: | ||
``` | ||
publishers/123/books/les-miserables/revisions/c7cfa2a8 | ||
``` | ||
|
||
The resource **must** be named `{resource_singular}Revision` (for example, `BookRevision`). | ||
|
||
### Server-specified aliases | ||
|
||
Services **may** reserve specific IDs to be [aliases][alias] (e.g. | ||
`latest`). These are read-only and managed by the service. | ||
|
||
``` | ||
GET /v1/publishers/{publisher_id}/books/{book_id}/revisions/{revision_id} | ||
``` | ||
|
||
If specific IDs are reserved, services **must** document those IDs. | ||
|
||
- If a `latest` ID exists, it **must** represent the most recently created | ||
revision. The content of `publishers/{publisher}/books/{book}/revisions/latest` | ||
and `publishers/{publisher}/books/{book}` can differ, as the latest revision may | ||
be different from the current state of the resource. | ||
|
||
### User-specified aliases | ||
|
||
APIs **may** provide a mechanism for users to assign an [alias][] ID to an | ||
existing revision with a custom method "alias": | ||
|
||
{% tab proto %} | ||
|
||
```proto | ||
|
||
rpc AliasBookRevision(AliasBookRevisionRequest) returns (Book) { | ||
option (google.api.http) = { | ||
post: "/v1/{name=publishers/*/books/*/revisions/*}:alias" | ||
body: "*" | ||
}; | ||
} | ||
|
||
message AliasBookRevisionRequest { | ||
string path = 1 [ | ||
(google.api.field_behavior) = REQUIRED, | ||
(google.api.resource_reference) = { | ||
type: "library.googleapis.com/BookRevision" | ||
}]; | ||
|
||
// The ID of the revision to alias to, e.g. `CURRENT` or a semantic | ||
// version. | ||
string alias = 2 [(google.api.field_behavior) = REQUIRED]; | ||
|
||
// If false, this request will fail when the alias already | ||
// exists. | ||
bool overwrite = 3 [(google.api.field_behavior) = OPTIONAL]; | ||
} | ||
``` | ||
|
||
{% tab oas %} | ||
|
||
```json | ||
{ | ||
"paths": { | ||
"/v1/publishers/{publisher_id}/books/{book}/revisions/{revision}:alias": { | ||
"post": { | ||
"requestBody": { | ||
"content": { | ||
"application/json": { | ||
"schema": { | ||
"$ref": "#/components/schemas/AliasBookRevisionRequest" | ||
} | ||
} | ||
} | ||
}, | ||
"responses": { | ||
"200": { | ||
"content": { | ||
"application/json": { | ||
"schema": { | ||
"$ref": "#/components/schemas/Book" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"components": { | ||
"schemas": { | ||
"AliasBookRevisionRequest": { | ||
"type": "object", | ||
"properties": { | ||
"path": { | ||
"type": "string" | ||
}, | ||
"alias": { | ||
"type": "string" | ||
}, | ||
"overwrite": { | ||
"type": "boolean" | ||
} | ||
}, | ||
"required": ["path", "alias"] | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
{% endtabs %} | ||
|
||
- The request message **must** have a `path` field: | ||
- The field **must** be [annotated as required](/field-behavior-documentation). | ||
- The field **must** identify the [resource type](/resource-types) that it | ||
references. | ||
- The request message **must** have a `alias` field: | ||
- The field **must** be [annotated as required][aip-203]. | ||
- If the user calls the method with an existing `alias` with `overwrite` set to | ||
true, the request **must** succeed and the alias will be updated to refer to the | ||
provided revision. If `overwrite` is false, the request must fail with an error code | ||
ALREADY_EXISTS (HTTP 409). | ||
|
||
### Rollback | ||
|
||
A common use case for a resource with a revision history is the ability to roll | ||
back to a given revision. APIs that support this behavior **should** do so with | ||
a `Rollback` custom method: | ||
|
||
{% tab proto %} | ||
|
||
```proto | ||
rpc RollbackBook(RollbackBookRequest) returns (BookRevision) { | ||
option (google.api.http) = { | ||
post: "/v1/{name=publishers/*/books/*/revisions/*}:rollback" | ||
body: "*" | ||
}; | ||
} | ||
|
||
message RollbackBookRequest { | ||
// The revision that the book should be rolled back to. | ||
string path = 1 [ | ||
(google.api.field_behavior) = REQUIRED, | ||
(google.api.resource_reference) = { | ||
type: "library.googleapis.com/BookRevision" | ||
}]; | ||
} | ||
``` | ||
|
||
{% tab oas %} | ||
|
||
```json | ||
{ | ||
"paths": { | ||
"/v1/publishers/{publisher}/books/{book}/revisions/{revision}:rollback": { | ||
"post": { | ||
"requestBody": { | ||
"content": { | ||
"application/json": { | ||
"schema": { | ||
"$ref": "#/components/schemas/RollbackBookRequest" | ||
} | ||
} | ||
} | ||
}, | ||
"responses": { | ||
"200": { | ||
"description": "Successful rollback", | ||
"content": { | ||
"application/json": { | ||
"schema": { | ||
"$ref": "#/components/schemas/BookRevision" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"components": { | ||
"schemas": { | ||
"RollbackBookRequest": { | ||
"type": "object", | ||
"properties": { | ||
"path": { | ||
"type": "string" | ||
} | ||
}, | ||
"required": ["path"] | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
{% endtabs %} | ||
|
||
- The method **must** use the `POST` HTTP verb. | ||
- The method **should** return a resource revision. | ||
- The request message **must** have a `path` field, referring to the resource | ||
revision whose configuration the resource should be rolled back to. | ||
- The field **must** be [annotated as required][aip-203]. | ||
- The field **must** identify the [resource type][aip-123] that it | ||
references. | ||
|
||
### Child resources | ||
|
||
Resources with a revision history **may** have child resources. If they do, they | ||
**should** be a subset of the descendants of the original resource, and a given | ||
revision's descendants must be a subset of the descendants of the resource at | ||
the time the revision was created. | ||
|
||
### Standard methods | ||
|
||
Any standard methods **must** implement the corresponding AIPs (AIP-131, | ||
AIP-132, AIP-133, AIP-134, AIP-135), with the following additional behaviors: | ||
|
||
- List methods: By default, revisions in the list response **should** be ordered | ||
in reverse chronological order. APIs **may** support the [`order_by`](./list#ordering) field to override the | ||
default behavior. | ||
- If the revision supports aliasing, a delete method with the resource path | ||
of the alias (e.g. `revisions/1.0.2`) **must** remove the alias instead of | ||
deleting the resource. | ||
|
||
As revisions are nested under the resource, also see [cascading delete][]. | ||
|
||
## Rationale | ||
|
||
### For the name "revision" | ||
|
||
There was significant debate about what to call this pattern, with the following | ||
as proposed options: | ||
|
||
- snapshots | ||
- revisions | ||
- versions | ||
|
||
Among those, revision was chosen because: | ||
|
||
- The term "version" is often used in multiple different contexts (e.g. API | ||
version), and using that noun here may result in confusion during | ||
conversations where it could mean one or the other. | ||
- The term "snapshot" is also used for snapshots of datastores, which may not | ||
follow this pattern. | ||
- The term "revision" does not have many conflicts with terms when describing an | ||
API or resource. | ||
|
||
|
||
## History | ||
|
||
### Switching from a collection extension to a subcollection | ||
|
||
In aip.dev prior to 2023-09, revisions were more like extensions of an existing | ||
resource by using `@` symbol. List and delete revisions were custom methods on | ||
the resource collection. A single Get method was used to retrieve either the | ||
resource revision, or the resource. | ||
|
||
Its primary advantage was allowing a resource reference to seamlessly refer to | ||
a resource, or its revision. | ||
|
||
It also had several disadvantages: | ||
|
||
- List revisions is a custom method (`:listRevisions`) on the resource. | ||
- Delete revision is a custom method on the resource. | ||
- Not visible in API discovery documenation. | ||
- Resource IDs cannot use the `@` symbol. | ||
|
||
The guidance was modified ultimately to enable revisions to behave like a | ||
resource, which reduces users' cognitive load and allows resource-oriented | ||
clients to easily list, get, create, update, and delete revisions. | ||
|
||
## Changelog | ||
|
||
- **2024-08-09**: Imported from aip.dev. | ||
|
||
[alias]: ./0122.md#resource-id-aliases | ||
[cascading delete]: ./0135.md#cascading-delete | ||
[UUID4]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random) |
Oops, something went wrong.