Skip to content

Commit 0982852

Browse files
toumorokoshirofrankeldv-stewarts
authored
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]>
1 parent b1775be commit 0982852

File tree

2 files changed

+390
-5
lines changed

2 files changed

+390
-5
lines changed

aep/general/0162/aep.md.j2

+388-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,390 @@
11
# Resource Revisions
22

3-
**Note:** This AEP has not yet been adopted. See
4-
[this GitHub issue](https://github.com/aep-dev/aep.dev/issues/16) for more
5-
information.
3+
Some APIs need to have resources with a revision history, where users can
4+
reason about the state of the resource over time. There are several reasons for
5+
this:
6+
7+
- Users may want to be able to roll back to a previous revision, or diff
8+
against a previous revision.
9+
- An API may create data which is derived in some way from a resource at a
10+
given point in time. In these cases, it may be desirable to snapshot the
11+
resource for reference later.
12+
13+
**Note:** We use the word _revision_ to refer to a historical reference for a
14+
particular resource, and intentionally avoid the term _version_, which refers
15+
to the version of an API as a whole.
16+
17+
## Guidance
18+
19+
APIs **may** expose a revision history for a resource. Examples of when it is
20+
useful include:
21+
22+
- When it is valuable to expose older revisions of a resource via an API. This
23+
can avoid the overhead of the customers having to write their own API to store
24+
and enable retrieval of revisions.
25+
- Other resources depend on or descend from different revisions of a resource.
26+
- There is a need to represent the change of a resource over time.
27+
28+
APIs implementing resources with revisions **must** model resource
29+
revisions as a subresource of the resource.
30+
31+
{% tab proto %}
32+
33+
```proto
34+
message BookRevision {
35+
// The path of the book revision.
36+
string path = 1;
37+
38+
// The snapshot of the book
39+
Book resource = 2
40+
[(google.api.field_behavior) = OUTPUT_ONLY];
41+
42+
// The timestamp that the revision was created.
43+
google.protobuf.Timestamp create_time = 3
44+
[(google.api.field_behavior) = OUTPUT_ONLY];
45+
46+
// Other revision IDs that share the same snapshot.
47+
repeated string aliases = 4
48+
[(google.api.field_behavior) = OUTPUT_ONLY];
49+
}
50+
```
51+
52+
- The `message` **must** be annotated as a resource (AIP-123).
53+
- The `message` name **must** be named `{ResourceType}Revision`.
54+
55+
{% tab oas %}
56+
57+
```json
58+
{
59+
"$schema": "http://json-schema.org/draft-07/schema#",
60+
"type": "object",
61+
"properties": {
62+
"path": {
63+
"type": "string"
64+
},
65+
"resource": {
66+
"$ref": "#/definitions/Book",
67+
"readOnly": true,
68+
},
69+
"create_time": {
70+
"type": "string",
71+
"format": "date-time",
72+
"readOnly": true,
73+
},
74+
"aliases": {
75+
"type": "array",
76+
"items": {
77+
"type": "string"
78+
},
79+
"readOnly": true,
80+
}
81+
},
82+
"required": ["path"],
83+
}
84+
```
85+
86+
{% endtabs %}
87+
88+
- The resource revision **must** contain a field with a message type of the
89+
parent resource, with a field name of `resource`.
90+
- The value of `resource` **must** be the original resource
91+
at the point in time the revision was created.
92+
- The resource revision **must** contain a `create_time` field (see [AIP-142][]).
93+
- The resource revision **may** contain a repeated field `aliases`, which would
94+
contain a list of resource IDs that the revision is also known by (e.g. `latest`)
95+
96+
### Creating Revisions
97+
98+
Depending on the resource, different APIs may have different strategies for
99+
creating a new revision. Specifically examples of strategies include:
100+
101+
- Creating a revision when there is a change to the resource
102+
- Creating a revision when important system state changes
103+
- Creating a revision when specifically requested
104+
105+
APIs **may** use any of these strategies or any other strategy. APIs **must**
106+
document their revision creation strategy.
107+
108+
Resources that support revisions **should** always have at least one revision.
109+
110+
### Resource names for revisions
111+
112+
When referring to the revisions of a resource, the subcollection name
113+
**must** be `revisions`. Resource revisions have paths with the format
114+
`{resource_path}/revisions/{resource_revision}`. For example:
115+
```
116+
publishers/123/books/les-miserables/revisions/c7cfa2a8
117+
```
118+
119+
The resource **must** be named `{resource_singular}Revision` (for example, `BookRevision`).
120+
121+
### Server-specified aliases
122+
123+
Services **may** reserve specific IDs to be [aliases][alias] (e.g.
124+
`latest`). These are read-only and managed by the service.
125+
126+
```
127+
GET /v1/publishers/{publisher_id}/books/{book_id}/revisions/{revision_id}
128+
```
129+
130+
If specific IDs are reserved, services **must** document those IDs.
131+
132+
- If a `latest` ID exists, it **must** represent the most recently created
133+
revision. The content of `publishers/{publisher}/books/{book}/revisions/latest`
134+
and `publishers/{publisher}/books/{book}` can differ, as the latest revision may
135+
be different from the current state of the resource.
136+
137+
### User-specified aliases
138+
139+
APIs **may** provide a mechanism for users to assign an [alias][] ID to an
140+
existing revision with a custom method "alias":
141+
142+
{% tab proto %}
143+
144+
```proto
145+
146+
rpc AliasBookRevision(AliasBookRevisionRequest) returns (Book) {
147+
option (google.api.http) = {
148+
post: "/v1/{name=publishers/*/books/*/revisions/*}:alias"
149+
body: "*"
150+
};
151+
}
152+
153+
message AliasBookRevisionRequest {
154+
string path = 1 [
155+
(google.api.field_behavior) = REQUIRED,
156+
(google.api.resource_reference) = {
157+
type: "library.googleapis.com/BookRevision"
158+
}];
159+
160+
// The ID of the revision to alias to, e.g. `CURRENT` or a semantic
161+
// version.
162+
string alias = 2 [(google.api.field_behavior) = REQUIRED];
163+
164+
// If false, this request will fail when the alias already
165+
// exists.
166+
bool overwrite = 3 [(google.api.field_behavior) = OPTIONAL];
167+
}
168+
```
169+
170+
{% tab oas %}
171+
172+
```json
173+
{
174+
"paths": {
175+
"/v1/publishers/{publisher_id}/books/{book}/revisions/{revision}:alias": {
176+
"post": {
177+
"requestBody": {
178+
"content": {
179+
"application/json": {
180+
"schema": {
181+
"$ref": "#/components/schemas/AliasBookRevisionRequest"
182+
}
183+
}
184+
}
185+
},
186+
"responses": {
187+
"200": {
188+
"content": {
189+
"application/json": {
190+
"schema": {
191+
"$ref": "#/components/schemas/Book"
192+
}
193+
}
194+
}
195+
}
196+
}
197+
}
198+
}
199+
},
200+
"components": {
201+
"schemas": {
202+
"AliasBookRevisionRequest": {
203+
"type": "object",
204+
"properties": {
205+
"path": {
206+
"type": "string"
207+
},
208+
"alias": {
209+
"type": "string"
210+
},
211+
"overwrite": {
212+
"type": "boolean"
213+
}
214+
},
215+
"required": ["path", "alias"]
216+
}
217+
}
218+
}
219+
}
220+
```
221+
222+
{% endtabs %}
223+
224+
- The request message **must** have a `path` field:
225+
- The field **must** be [annotated as required](/field-behavior-documentation).
226+
- The field **must** identify the [resource type](/resource-types) that it
227+
references.
228+
- The request message **must** have a `alias` field:
229+
- The field **must** be [annotated as required][aip-203].
230+
- If the user calls the method with an existing `alias` with `overwrite` set to
231+
true, the request **must** succeed and the alias will be updated to refer to the
232+
provided revision. If `overwrite` is false, the request must fail with an error code
233+
ALREADY_EXISTS (HTTP 409).
234+
235+
### Rollback
236+
237+
A common use case for a resource with a revision history is the ability to roll
238+
back to a given revision. APIs that support this behavior **should** do so with
239+
a `Rollback` custom method:
240+
241+
{% tab proto %}
242+
243+
```proto
244+
rpc RollbackBook(RollbackBookRequest) returns (BookRevision) {
245+
option (google.api.http) = {
246+
post: "/v1/{name=publishers/*/books/*/revisions/*}:rollback"
247+
body: "*"
248+
};
249+
}
250+
251+
message RollbackBookRequest {
252+
// The revision that the book should be rolled back to.
253+
string path = 1 [
254+
(google.api.field_behavior) = REQUIRED,
255+
(google.api.resource_reference) = {
256+
type: "library.googleapis.com/BookRevision"
257+
}];
258+
}
259+
```
260+
261+
{% tab oas %}
262+
263+
```json
264+
{
265+
"paths": {
266+
"/v1/publishers/{publisher}/books/{book}/revisions/{revision}:rollback": {
267+
"post": {
268+
"requestBody": {
269+
"content": {
270+
"application/json": {
271+
"schema": {
272+
"$ref": "#/components/schemas/RollbackBookRequest"
273+
}
274+
}
275+
}
276+
},
277+
"responses": {
278+
"200": {
279+
"description": "Successful rollback",
280+
"content": {
281+
"application/json": {
282+
"schema": {
283+
"$ref": "#/components/schemas/BookRevision"
284+
}
285+
}
286+
}
287+
}
288+
}
289+
}
290+
}
291+
},
292+
"components": {
293+
"schemas": {
294+
"RollbackBookRequest": {
295+
"type": "object",
296+
"properties": {
297+
"path": {
298+
"type": "string"
299+
}
300+
},
301+
"required": ["path"]
302+
}
303+
}
304+
}
305+
}
306+
```
307+
308+
{% endtabs %}
309+
310+
- The method **must** use the `POST` HTTP verb.
311+
- The method **should** return a resource revision.
312+
- The request message **must** have a `path` field, referring to the resource
313+
revision whose configuration the resource should be rolled back to.
314+
- The field **must** be [annotated as required][aip-203].
315+
- The field **must** identify the [resource type][aip-123] that it
316+
references.
317+
318+
### Child resources
319+
320+
Resources with a revision history **may** have child resources. If they do, they
321+
**should** be a subset of the descendants of the original resource, and a given
322+
revision's descendants must be a subset of the descendants of the resource at
323+
the time the revision was created.
324+
325+
### Standard methods
326+
327+
Any standard methods **must** implement the corresponding AIPs (AIP-131,
328+
AIP-132, AIP-133, AIP-134, AIP-135), with the following additional behaviors:
329+
330+
- List methods: By default, revisions in the list response **should** be ordered
331+
in reverse chronological order. APIs **may** support the [`order_by`](./list#ordering) field to override the
332+
default behavior.
333+
- If the revision supports aliasing, a delete method with the resource path
334+
of the alias (e.g. `revisions/1.0.2`) **must** remove the alias instead of
335+
deleting the resource.
336+
337+
As revisions are nested under the resource, also see [cascading delete][].
338+
339+
## Rationale
340+
341+
### For the name "revision"
342+
343+
There was significant debate about what to call this pattern, with the following
344+
as proposed options:
345+
346+
- snapshots
347+
- revisions
348+
- versions
349+
350+
Among those, revision was chosen because:
351+
352+
- The term "version" is often used in multiple different contexts (e.g. API
353+
version), and using that noun here may result in confusion during
354+
conversations where it could mean one or the other.
355+
- The term "snapshot" is also used for snapshots of datastores, which may not
356+
follow this pattern.
357+
- The term "revision" does not have many conflicts with terms when describing an
358+
API or resource.
359+
360+
361+
## History
362+
363+
### Switching from a collection extension to a subcollection
364+
365+
In aip.dev prior to 2023-09, revisions were more like extensions of an existing
366+
resource by using `@` symbol. List and delete revisions were custom methods on
367+
the resource collection. A single Get method was used to retrieve either the
368+
resource revision, or the resource.
369+
370+
Its primary advantage was allowing a resource reference to seamlessly refer to
371+
a resource, or its revision.
372+
373+
It also had several disadvantages:
374+
375+
- List revisions is a custom method (`:listRevisions`) on the resource.
376+
- Delete revision is a custom method on the resource.
377+
- Not visible in API discovery documenation.
378+
- Resource IDs cannot use the `@` symbol.
379+
380+
The guidance was modified ultimately to enable revisions to behave like a
381+
resource, which reduces users' cognitive load and allows resource-oriented
382+
clients to easily list, get, create, update, and delete revisions.
383+
384+
## Changelog
385+
386+
- **2024-08-09**: Imported from aip.dev.
387+
388+
[alias]: ./0122.md#resource-id-aliases
389+
[cascading delete]: ./0135.md#cascading-delete
390+
[UUID4]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)

0 commit comments

Comments
 (0)