Skip to content

Commit 9f21dda

Browse files
Luke Sneeringermkistler
authored andcommitted
feat: AIP-162 – Resource revisions
1 parent 5748816 commit 9f21dda

File tree

3 files changed

+489
-0
lines changed

3 files changed

+489
-0
lines changed

aip/general/0162/aip.md.j2

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# Resource Revisions
2+
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** store a revision history for a resource if it is useful to users.
20+
21+
APIs implementing resources with a revision history **must** provide a
22+
`revision_id` field on the resource:
23+
24+
```typescript
25+
interface Book {
26+
// The ID of the book.
27+
id: string;
28+
29+
// Other fields…
30+
31+
// The revision ID of the book.
32+
// A new revision is committed whenever the book is changed in any way.
33+
// The format is an 8-character hexadecimal string.
34+
readonly revisionId: string;
35+
36+
// The timestamp that the revision was created.
37+
readonly revisionCreateTime: string; // ISO 8601
38+
}
39+
```
40+
41+
- The resource **must** contain a `revision_id` field, which **should** be a
42+
string and contain a short, automatically-generated random string. A good
43+
rule of thumb is the last eight characters of a UUID4.
44+
- The `revision_id` field **must** document when new revisions are created
45+
(see [committing revisions](#committing-revisions) below).
46+
- The `revision_id` field **should** document the format of revision IDs.
47+
- The resource **must** contain a `revision_create_time` field, which
48+
**should** be a timestamp (see AIP-142).
49+
50+
**Note:** A randomly generated string is preferred over other operations, such
51+
as an auto-incrementing integer, because there is often a need to delete or
52+
revert revisions, and a randomly generated string holds up better in those
53+
situations.
54+
55+
### Referencing revisions
56+
57+
When it is necessary to refer to a specific revision of a resource, APIs
58+
**must** use the following syntax: `{resource_id}@{revision_id}`. For example:
59+
60+
publishers/123/books/les-miserables@c7cfa2a8
61+
62+
**Note:** The `@` character is selected because it is the only character
63+
permitted by [RFC 1738 §2.2][] for special meaning within a URI scheme that is
64+
not already used elsewhere.
65+
66+
APIs **should** generally accept a resource reference at a particular revision
67+
in any place where they ordinarily accept the resource ID. However, they **must
68+
not** accept a revision in situations that mutate the resource, and should
69+
error with `400 Bad Request` if one is given.
70+
71+
**Important:** APIs **must not** require a revision ID, and **must** default to
72+
the current revision if one is not provided, except in operations specifically
73+
dealing with the revision history (such as rollback) where failing to require
74+
it would not make sense (or lead to dangerous mistakes).
75+
76+
### Getting a revision
77+
78+
APIs implementing resource revisions **should** accept a resource ID with a
79+
revision ID in the standard `Get` operation (AIP-131):
80+
81+
{% tab proto %}
82+
83+
{% sample 'revisions.proto', 'message GetBookRequest' %}
84+
85+
{% endtabs %}
86+
87+
If the user passes a revision ID that does not exist, the API **must** fail
88+
with a `404 Not Found` error.
89+
90+
APIs **must** return a `id` value corresponding to what the user sent. If the
91+
user sent a resource ID with no revision ID, the returned `id` string **must
92+
not** include the revision ID either. Similarly, if the user sent a resource ID
93+
with a revision ID, the returned `id` string **must** explicitly include it.
94+
95+
### Tagging revisions
96+
97+
APIs implementing resource revisions **may** provide a mechanism for users to
98+
tag a specific revision with a user provided name by implementing a "Tag
99+
Revision" custom operation:
100+
101+
{% tab proto %}
102+
103+
{% sample 'revisions.proto', 'rpc TagBookRevision', 'message TagBookRevisionRequest' %}
104+
105+
{% endtabs %}
106+
107+
- The `id` field **should** require an explicit revision ID to be provided.
108+
- The field **should** be [annotated as required][aip-203].
109+
- The field **should** identify the [resource type][aip-123] that it
110+
references.
111+
- The `tag` field **should** be [annotated as required][aip-203].
112+
- Additionally, tags **should** restrict letters to lower-case.
113+
- Once a revision is tagged, the API **must** support using the tag in place of
114+
the revision ID in `id` fields.
115+
- If the user sends a tag, the API **must** return the tag in the resource's
116+
`id` field, but the revision ID in the resource's `revision_id` field.
117+
- If the user sends a revision ID, the API **must** return the revision ID in
118+
both the `id` field and the `revision_id` field.
119+
- If the user calls the `Tag` operation with an existing tag, the request
120+
**must** succeed and the tag updated to point to the new requested revision
121+
ID. This allows users to write code against specific tags (e.g. `published`)
122+
and the revision can change in the background with no code change.
123+
124+
### Listing revisions
125+
126+
APIs implementing resource revisions **should** provide a custom operation for
127+
listing the revision history for a resource, with a structure similar to
128+
standard `List` operations (AIP-132):
129+
130+
{% tab proto %}
131+
132+
{% sample 'revisions.proto', 'rpc ListBookRevisions', 'message ListBookRevisionsRequest' %}
133+
134+
{% endtabs %}
135+
136+
While revision listing operations are mostly similar to standard `List`
137+
operations (AIP-132), the following important differences apply:
138+
139+
- The first field in the request message **must** be called `id` rather than
140+
`parent` (this is listing revisions for a specific book, not a collection of
141+
books).
142+
- The URI **must** end with `:listRevisions`.
143+
- Revisions **must** be ordered in reverse chronological order. An `order_by`
144+
field **should not** be provided.
145+
- The returned resources **must** include an explicit revision ID in the
146+
resource's `id` field.
147+
- If providing the full resource is expensive or infeasible, the revision
148+
object **may** only populate the `id`, `revision_id`, and
149+
`revision_create_time` fields instead. The `id` field **must** include the
150+
resource ID and an explicit revision ID, which can be used for an explicit
151+
`Get` request. The API **must** document that it will do this.
152+
153+
### Child resources
154+
155+
Resources with a revision history **may** have child resources. If they do,
156+
there are two potential variants:
157+
158+
- Child resources where each child resource is a child of the parent resource
159+
as a whole.
160+
- Child resources where each child resource is a child of _a single revision
161+
of_ the parent resource.
162+
163+
If a child resource is a child of a single revision, the child resource's name
164+
**must** always explicitly include the parent's resource ID:
165+
166+
publishers/123/books/les-miserables@c7cfa2a8/pages/42
167+
168+
In `List` requests for such resources, the service **should** default to the
169+
latest revision of the parent if the user does not specify one, but **must**
170+
explicitly include the parent's revision ID in the `id` field of resources in
171+
the response.
172+
173+
If necessary, APIs **may** explicitly support listing child resources across
174+
parent revisions by accepting the `@-` syntax. For example:
175+
176+
GET /v1/publishers/123/books/les-miserables@-/pages
177+
178+
APIs **should not** include multiple levels of resources with revisions, as
179+
this quickly becomes difficult to reason about.
180+
181+
### Committing revisions
182+
183+
Depending on the resource, different APIs may have different strategies for
184+
when to commit a new revision, such as:
185+
186+
- Commit a new revision any time that there is a change
187+
- Commit a new revision when something important happens
188+
- Commit a new revision when the user specifically asks
189+
190+
APIs **may** use any of these strategies. APIs that want to commit a revision
191+
on user request **should** handle this with a `Commit` custom operation:
192+
193+
{% tab proto %}
194+
195+
{% sample 'revisions.proto', 'rpc CommitBook', 'message CommitBookRequest' %}
196+
197+
{% endtabs %}
198+
199+
- The operation **must** use the `POST` HTTP method.
200+
- The operation **should** return the resource, and the resource ID **must**
201+
include the revision ID.
202+
- The request message **must** include the `id` field.
203+
- The field **should** be [annotated as required][aip-203].
204+
- The field **should** identify the [resource type][aip-123] that it
205+
references.
206+
207+
### Rollback
208+
209+
A common use case for a resource with a revision history is the ability to roll
210+
back to a given revision. APIs **should** handle this with a `Rollback` custom
211+
operation:
212+
213+
{% tab proto %}
214+
215+
{% sample 'revisions.proto', 'rpc RollbackBook', 'message RollbackBookRequest' %}
216+
217+
{% endtabs %}
218+
219+
- The operation **must** use the `POST` HTTP method.
220+
- The operation **should** return the resource, and the resource ID **must**
221+
include the revision ID.
222+
- The request message **must** have a `id` field to identify the resource being
223+
rolled back.
224+
- The field **should** be [annotated as required][aip-203].
225+
- The field **should** identify the [resource type][aip-123] that it
226+
references.
227+
- The request message **must** include a `revision_id` field.
228+
- The API **must** fail the request with `NOT_FOUND` if the revision does not
229+
exist on that resource.
230+
- The field **should** be [annotated as required][aip-203].
231+
232+
**Note:** When rolling back, the API should return a _new_ revision of the
233+
resource with a _new_ revision ID, rather than reusing the original ID. This
234+
avoids problems with representing the same revision being active for multiple
235+
ranges of time.
236+
237+
### Deleting revisions
238+
239+
Revisions are sometimes expensive to store, and there are valid use cases to
240+
want to remove one or more revisions from a resource's revision history.
241+
242+
APIs **may** define a operation to delete revisions, with a structure similar
243+
to `Delete` (AIP-135) operations:
244+
245+
{% tab proto %}
246+
247+
{% sample 'revisions.proto', 'rpc DeleteBookRevision', 'message DeleteBookRevisionRequest' %}
248+
249+
{% endtabs %}
250+
251+
- The request message **must** have a `id` field to identify the resource
252+
revision being deleted.
253+
- The explicit revision ID **must** be required (the operation **must** fail
254+
with `INVALID_ARGUMENT` if it is not provided, and **must not** default to
255+
the latest revision).
256+
- The field **should** be [annotated as required][aip-203].
257+
- The field **should** identify the [resource type][aip-123] that it
258+
references.
259+
- The API **must not** overload the `DeleteBook` operation to serve both
260+
purposes (this could lead to dangerous or confusing mistakes).
261+
- If the resource supports soft delete, then revisions of that resource
262+
**should** also support soft delete.
263+
- The operation **must not** cause the _resource_ to be deleted.
264+
- Because a resource **should not** exist with zero revisions, the operation
265+
**must** fail with `FAILED_PRECONDITION` if the user attempts to delete the
266+
only revision.
267+
268+
### Character Collision
269+
270+
Most resource IDs have a restrictive set of characters, but some are very open.
271+
For example, Google Cloud Storage allows the `@` character in filenames, which
272+
are part of the resource ID, and therefore uses the `#` character to indicate a
273+
revision.
274+
275+
APIs **should not** permit the `@` character in resource IDs, and if APIs that
276+
do permit it need to support resources with revisions, they **should** pick an
277+
appropriate separator depending on how and where the API is used, and **must**
278+
clearly document it.
279+
280+
[rfc 1738 §2.2]: https://tools.ietf.org/html/rfc1738

aip/general/0162/aip.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
id: 162
3+
state: approved
4+
created: 2019-09-17
5+
placement:
6+
category: design-patterns
7+
order: 88

0 commit comments

Comments
 (0)