diff --git a/aep/general/0162/aep.md.j2 b/aep/general/0162/aep.md.j2 index 7b235f55..0bafbe5c 100644 --- a/aep/general/0162/aep.md.j2 +++ b/aep/general/0162/aep.md.j2 @@ -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) \ No newline at end of file diff --git a/aep/general/0162/aep.yaml b/aep/general/0162/aep.yaml index d70cd014..ee2258d7 100644 --- a/aep/general/0162/aep.yaml +++ b/aep/general/0162/aep.yaml @@ -1,7 +1,7 @@ --- id: 162 -state: reviewing +state: approved slug: revisions -created: 2023-01-22 +created: 2024-07-10 placement: category: design-patterns