Skip to content

Commit

Permalink
feat: AIP-164 – Soft delete (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
Luke Sneeringer authored Nov 16, 2021
1 parent cfc42e7 commit 8977b65
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 0 deletions.
129 changes: 129 additions & 0 deletions aip/general/0164/aip.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Soft delete

There are several reasons why a client could desire soft delete and undelete
functionality, but one over-arching reason stands out: recovery from mistakes.
A service that supports undelete makes it possible for users to recover
resources that were deleted by accident.

## Guidance

Services **may** support the ability to "undelete", to allow for situations
where users mistakenly delete resources and need the ability to recover.

If a resource needs to support undelete, the `Delete` method **must** simply
mark the resource as having been deleted, but not completely remove it from the
system. If the method behaves this way, it **should** return `200 OK` with the
updated resource instead of `204 No Content`.

Resources that support soft delete **should** have an `expire_time` field as
described in AIP-148. Additionally, resources **should** include a `DELETED`
state value if the resource includes a `state` field (AIP-216).

### Undelete

A resource that supports soft delete **should** provide an `Undelete` method:

{% tab proto %}

{% sample 'undelete.proto', 'rpc UndeleteBook', 'message UndeleteBookRequest' %}

- The HTTP method **must** be `POST`.
- The `body` clause **must** be `"*"`.
- The response message **must** be the resource itself. There is no
`UndeleteBookResponse`.
- The response **should** include the fully-populated resource unless it is
infeasible to do so.
- If the undelete RPC is [long-running](#long-running-undelete), the response
message **must** be a `google.longrunning.Operation` which resolves to the
resource itself.
- A `name` field **must** be included in the request message; it **should** be
called `name`.
- The field **should** be [annotated as required][aip-203].
- The field **should** identify the [resource type][aip-123] that it
references.
- The comment for the field **should** document the resource pattern.
- The request message **must not** contain any other required fields, and
**should not** contain other optional fields except those described in this
or another AIP.

{% tab oas %}

{% sample 'undelete.oas.yaml', 'paths' %}

- The HTTP method **must** be `POST`.
- The response message **must** be the resource itself.
- The response **should** include the fully-populated resource unless it is
infeasible to do so.
- The operation **must not** require any other fields, and **should not**
contain other optional query parameters except those described in this or
another AIP.

{% endtabs %}

### Long-running undelete

Some resources take longer to undelete a resource than is reasonable for a
regular API request. In this situation, the API **should** follow the
long-running request pattern (AIP-151).

### List and Get

Soft-deleted resources **should not** be returned in `List` (AIP-132) responses
by default (unless `bool show_deleted` is true).

A `Get` (AIP-131) request for a soft deleted resource **should** error with
`410 Gone` unless `bool show_deleted` is true, in which case soft-deleted
resources **must** return the resource.

Services that soft delete resources **may** choose a reasonable strategy for
purging those resources, including automatic purging after a reasonable time
(such as 30 days), allowing users to set an expiry time (AIP-214), or retaining
the resources indefinitely. Regardless of what strategy is selected, the
service **should** document when soft deleted resources will be completely
removed.

### Declarative-friendly resources

A resource that is declarative-friendly (AIP-128) **should** support soft
delete and undelete.

**Important:** There is an ambiguity in declarative tooling between "create"
and "undelete". When given an alias which was previously deleted and a
directive to make it exist, tooling usually does not know if the intent is to
restore the previously-deleted resource, or create a new one with the same
alias. Declarative tools **should** resolve this ambiguity in favor of creating
a new resource: the only way to undelete is to explicitly use the undelete RPC
(an imperative operation), and declarative tools **may** elect not to map
anything to undelete at all.

Declarative-friendly resources **must** use long-running operations for both
soft delete and undelete. The service **may** return an LRO that is already set
to done if the request is effectively immediate.

Declarative-friendly resources **must** include `validate_only` (AIP-163) and
`etag` (AIP-154) in their `Undelete` methods.

### Errors

If the user does not have permission to access the resource, regardless of
whether or not it exists, the service **must** error with `403 Forbidden`.
Permission **must** be checked prior to checking if the resource exists.

If the user does have proper permission, but the requested resource does not
exist (either it was never created or already expunged), the service **must**
error with `404 Not Found`.

If the user calling a soft `Delete` has proper permission, but the requested
resource is already deleted, the service **must** succeed if `allow_missing` is
`true`, and **should** error with `404 Not Found` if `allow_missing` is
`false`.

If the user calling `Undelete` has proper permission, but the requested
resource is not deleted, the service **must** error with `409 Conflict`.

## Further reading

- For the `Delete` standard method, see AIP-135.
- For long-running operations, see AIP-151.
- For resource freshness validation (`etag`), see AIP-154.
- For change validation (`validate_only`), see AIP-163.
7 changes: 7 additions & 0 deletions aip/general/0164/aip.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
id: 164
state: approved
created: 2020-10-06
placement:
category: design-patterns
order: 95
42 changes: 42 additions & 0 deletions aip/general/0164/undelete.oas.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
openapi: 3.0.3
info:
title: Library
version: 1.0.0
paths:
/publishers/{publisherId}/books/{bookId}:undelete:
post:
operationId: undeleteBook
description: Undelete a single book.
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Book'
components:
schema:
Book:
description: A representation of a single book.
properties:
name:
type: string
description: |
The name of the book.
Format: publishers/{publisher}/books/{book}
isbn:
type: string
description: |
The ISBN (International Standard Book Number) for this book.
title:
type: string
description: The title of the book.
authors:
type: array
items:
type: string
description: The author or authors of the book.
rating:
type: float
description: The rating assigned to the book.
66 changes: 66 additions & 0 deletions aip/general/0164/undelete.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/resource.proto";

service Library {
// Undelete a single book.
rpc UndeleteBook(UndeleteBookRequest) returns (Book) {
option (google.api.http) = {
post: "/v1/{name=publishers/*/books/*}:undelete"
body: "*"
};
option (google.api.method_signature) = "name";
}
}

// Request message to undelete a single book.
message UndeleteBookRequest {
// The name of the book to undelete.
// The book must exist and currently be deleted (but not expunged).
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {
type: "library.googleapis.com/Book"
}];
}

// A representation of a single book.
message Book {
option (google.api.resource) = {
type: "library.googleapis.com/Book"
pattern: "publishers/{publisher}/books/{book}"
};

// The name of the book.
// Format: publishers/{publisher}/books/{book}
string name = 1;

// The ISBN (International Standard Book Number) for this book.
string isbn = 2;

// The title of the book.
string title = 3;

// The author or authors of the book.
repeated string authors = 4;

// The rating assigned to the book.
float rating = 5;
}

0 comments on commit 8977b65

Please sign in to comment.