1
1
# Resource Revisions
2
2
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