Skip to content

Commit

Permalink
feat(revisions): adopt aep-162 revisions (#200)
Browse files Browse the repository at this point in the history
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
3 people authored Sep 25, 2024
1 parent b1775be commit 0982852
Show file tree
Hide file tree
Showing 2 changed files with 390 additions and 5 deletions.
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
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)
Loading

0 comments on commit 0982852

Please sign in to comment.