Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(revisions): adopt aep-162 revisions #200

Merged
merged 13 commits into from
Sep 25, 2024
391 changes: 388 additions & 3 deletions aep/general/0162/aep.md.j2
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
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved
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
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved
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
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved

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`.
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved

### 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.

toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved
### Child resources
toumorokoshi marked this conversation as resolved.
Show resolved Hide resolved

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)
Loading
Loading