|
| 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 |
0 commit comments