Skip to content

Commit 0e222ac

Browse files
committed
feat(301): add undelete pattern, replace soft-delete
The current soft-delete pattern has the undesirable side effect of adding special-casing and additional query parameters to the standard methods, as well as possibly impacting their behavior. Introducing undelete, which is a resource oriented solution that achieves much of the same user journeys: - Restoring resources after being deleted. - Listing / getting deleted resources to figure out what is restorable. fixes #111.
1 parent 322db5c commit 0e222ac

File tree

6 files changed

+283
-1
lines changed

6 files changed

+283
-1
lines changed

aep/general/0164/aep.md.j2

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Soft delete
22

3+
**NOTE: this pattern is now deprecated, and is no longer valid. Please use
4+
[undelete](/undelete) instead.**
5+
36
There are several reasons why a client could desire soft delete and undelete
47
functionality, but one over-arching reason stands out: recovery from mistakes.
58
A service that supports undelete makes it possible for users to recover

aep/general/0164/aep.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
id: 164
3-
state: approved
3+
state: deprecated
44
slug: soft-delete
55
created: 2020-10-06
66
placement:

aep/general/0301/aep.md.j2

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Undelete
2+
3+
There are several reasons why a client could desire undelete functionality, but
4+
one over-arching reason stands out: recovery from mistakes. A service that
5+
supports undelete makes it possible for users to recover resources that were
6+
deleted by accident.
7+
8+
## Guidance
9+
10+
Services **may** support the ability to "undelete", to allow for situations
11+
where users mistakenly delete resources and need the ability to recover.
12+
13+
These resources **must** be stored in a separate sibling collection, prefixed
14+
with `deleted-`. (e.g. `deleted-books`). Resources deleted will remain in this
15+
sibling collection until they expire, or until they are undeleted into the
16+
original collection.
17+
18+
Resources that support soft delete **should** have an `expire_time` field on
19+
the deleted version of the resource, as described in AEP-148.
20+
21+
### Sibling collection
22+
23+
To implement the undelete pattern, a sibling collection,
24+
`deleted-{resource_plural}`, **should** be created where all undeletable
25+
resources can be listed and retrieved, as well as undeleted via the `Undelete`
26+
custom method.
27+
28+
### Undelete
29+
30+
The `Undelete` custom method **should** be available. A successful call to this
31+
method will:
32+
33+
1. remove the resource from the deleted collection.
34+
2. restore the resource back into the original collection.
35+
36+
{% tab proto %}
37+
38+
{% sample '../example.proto', 'rpc UndeleteDeletedBook', 'message UndeleteDeletedBookRequest' %}
39+
40+
- The HTTP method **must** be `POST`.
41+
- The `body` clause **must** be `"*"`.
42+
- The response message **must** be the resource itself. There is no
43+
`UndeleteDeletedBookResponse`.
44+
- The response **may** include the fully-populated resource or an empty
45+
response.
46+
- A `path` field **must** be included in the request message; it **should** be
47+
called `path`.
48+
- The field **should** be [annotated as required][aep-203].
49+
- The field **should** identify the [resource type][aep-4] that it
50+
references.
51+
- The comment for the field **should** document the resource pattern.
52+
- The request message **must not** contain any other required fields, and
53+
**should not** contain other optional fields except those described in this
54+
or another AEP.
55+
56+
{% tab oas %}
57+
58+
% sample 'undelete.oas.yaml',
59+
'$.paths./deleted-publishers/{deleted_publisher_id}:undelete' %}
60+
61+
- The HTTP method **must** be `POST`.
62+
- The response message **must** be the resource itself.
63+
- The response **may** include the fully-populated resource or an empty
64+
response.
65+
- The operation **must not** require any other fields, and **should not**
66+
contain other optional query parameters except those described in this or
67+
another AEP.
68+
69+
{% endtabs %}
70+
71+
### Long-running undelete
72+
73+
Some resources take longer to undelete a resource than is reasonable for a
74+
regular API request. In this situation, the API **should** follow the
75+
long-running request pattern AEP-151.
76+
77+
### Errors
78+
79+
If the user calling `Undelete` has proper permission, but the requested
80+
resource is not deleted, the service **must** error with `409 Conflict`.
81+
82+
For additional guiance, see [errors](/errors).
83+
84+
## Further reading
85+
86+
- For the `Delete` standard method, see AEP-135.
87+
- For long-running operations, see AEP-151.
88+
- For resource freshness validation (`etag`), see AEP-154.
89+
- For change validation (`validate_only`), see AEP-163.

aep/general/0301/aep.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
id: 301
3+
state: approved
4+
slug: undelete
5+
created: 2020-10-06
6+
placement:
7+
category: design-patterns
8+
order: 95

aep/general/example.oas.yaml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,27 @@ components:
6262
- publishers/{publisher_id}/books/{book_id}/editions/{book_edition_id}
6363
plural: book-editions
6464
singular: book-edition
65+
deleted_publisher:
66+
properties:
67+
description:
68+
type: string
69+
expire_time:
70+
description:
71+
The time when this deleted publisher will expire and be permanently
72+
removed
73+
format: date-time
74+
type: string
75+
path:
76+
description:
77+
The server-assigned path of the resource, which is unique within
78+
the service.
79+
type: string
80+
type: object
81+
x-aep-resource:
82+
patterns:
83+
- deleted-publishers/{deleted_publisher_id}
84+
plural: deleted_publishers
85+
singular: deleted_publisher
6586
isbn:
6687
properties:
6788
path:
@@ -144,6 +165,73 @@ info:
144165
version: version not set
145166
openapi: 3.1.0
146167
paths:
168+
/deleted-publishers:
169+
get:
170+
description: List method for deleted_publisher
171+
operationId: ListDeletedPublisher
172+
parameters:
173+
- in: query
174+
name: max_page_size
175+
schema:
176+
type: integer
177+
- in: query
178+
name: page_token
179+
schema:
180+
type: string
181+
responses:
182+
'200':
183+
content:
184+
application/json:
185+
schema:
186+
properties:
187+
next_page_token:
188+
type: string
189+
results:
190+
items:
191+
$ref: '#/components/schemas/deleted_publisher'
192+
type: array
193+
type: object
194+
description: Successful response
195+
/deleted-publishers/{deleted_publisher_id}:
196+
get:
197+
description: Get method for deleted_publisher
198+
operationId: GetDeletedPublisher
199+
parameters:
200+
- in: path
201+
name: deleted_publisher_id
202+
required: true
203+
schema:
204+
type: string
205+
responses:
206+
'200':
207+
content:
208+
application/json:
209+
schema:
210+
$ref: '#/components/schemas/deleted_publisher'
211+
description: Successful response
212+
/deleted-publishers/{deleted_publisher_id}:undelete:
213+
post:
214+
description: Custom method undelete for deleted_publisher
215+
operationId: :UndeleteDeletedPublisher
216+
parameters:
217+
- in: path
218+
name: deleted_publisher_id
219+
required: true
220+
schema:
221+
type: string
222+
requestBody:
223+
content:
224+
application/json:
225+
schema:
226+
type: object
227+
required: true
228+
responses:
229+
'200':
230+
content:
231+
application/json:
232+
schema:
233+
type: object
234+
description: Successful response
147235
/isbns:
148236
get:
149237
description: List method for isbn

aep/general/example.proto

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,28 @@ service Bookstore {
106106
option (google.api.method_signature) = "parent";
107107
}
108108

109+
// An aep-compliant Get method for deleted_publisher.
110+
rpc GetDeletedPublisher(GetDeletedPublisherRequest) returns (DeletedPublisher) {
111+
option (google.api.http) = {get: "/{path=deleted-publishers/*}"};
112+
113+
option (google.api.method_signature) = "path";
114+
}
115+
116+
// An aep-compliant List method for deleted_publishers.
117+
rpc ListDeletedPublishers(ListDeletedPublishersRequest) returns (ListDeletedPublishersResponse) {
118+
option (google.api.http) = {get: "/deleted_publishers"};
119+
120+
option (google.api.method_signature) = "parent";
121+
}
122+
123+
// undelete a deleted_publisher.
124+
rpc UndeleteDeletedPublisher(UndeleteDeletedPublisherRequest) returns (UndeleteDeletedPublisherResponse) {
125+
option (google.api.http) = {
126+
post: "/{path=deleted-publishers/*}:undelete"
127+
body: "*"
128+
};
129+
}
130+
109131
// An aep-compliant Create method for isbn.
110132
rpc CreateIsbn(CreateIsbnRequest) returns (Isbn) {
111133
option (google.api.http) = {
@@ -351,6 +373,25 @@ message BookEdition {
351373
string path = 10018;
352374
}
353375

376+
// A DeletedPublisher.
377+
message DeletedPublisher {
378+
option (google.api.resource) = {
379+
type: "bookstore.example.com/deleted_publisher"
380+
pattern: ["deleted-publishers/{deleted_publisher_id}"]
381+
plural: "deleted_publishers"
382+
singular: "deleted_publisher"
383+
};
384+
385+
// Field for description.
386+
string description = 1;
387+
388+
// Field for expire_time.
389+
string expire_time = 2 [json_name = "expire_time"];
390+
391+
// Field for path.
392+
string path = 10018;
393+
}
394+
354395
// A Isbn.
355396
message Isbn {
356397
option (google.api.resource) = {
@@ -650,6 +691,59 @@ message ListBookEditionsResponse {
650691
string next_page_token = 10011 [json_name = "next_page_token"];
651692
}
652693

694+
// Request message for the Getdeleted_publisher method
695+
message GetDeletedPublisherRequest {
696+
// The globally unique identifier for the resource
697+
string path = 10018 [
698+
(aep.api.field_info) = {
699+
resource_reference: ["bookstore.example.com/deleted_publisher"]
700+
field_behavior: [FIELD_BEHAVIOR_REQUIRED]
701+
},
702+
(google.api.field_behavior) = REQUIRED
703+
];
704+
}
705+
706+
// Request message for the Listdeleted_publisher method
707+
message ListDeletedPublishersRequest {
708+
// A field for the parent of deleted_publisher
709+
string parent = 10013 [
710+
(aep.api.field_info) = {
711+
field_behavior: [FIELD_BEHAVIOR_REQUIRED]
712+
},
713+
(google.api.field_behavior) = REQUIRED
714+
];
715+
716+
// The page token indicating the starting point of the page
717+
string page_token = 10010 [json_name = "page_token"];
718+
719+
// The maximum number of resources to return in a single page.
720+
int32 max_page_size = 10017 [json_name = "max_page_size"];
721+
}
722+
723+
// Response message for the Listdeleted_publisher method
724+
message ListDeletedPublishersResponse {
725+
// A list of deleted_publishers
726+
repeated DeletedPublisher results = 10016;
727+
728+
// The page token indicating the ending point of this response.
729+
string next_page_token = 10011 [json_name = "next_page_token"];
730+
}
731+
732+
// Response message for the undelete method
733+
message UndeleteDeletedPublisherResponse {}
734+
735+
// Request message for the undelete method
736+
message UndeleteDeletedPublisherRequest {
737+
// The globally unique identifier for the resource
738+
string path = 10018 [
739+
(aep.api.field_info) = {
740+
resource_reference: ["bookstore.example.com/deleted_publisher"]
741+
field_behavior: [FIELD_BEHAVIOR_REQUIRED]
742+
},
743+
(google.api.field_behavior) = REQUIRED
744+
];
745+
}
746+
653747
// A Create request for a isbn resource.
654748
message CreateIsbnRequest {
655749
// A field for the parent of isbn

0 commit comments

Comments
 (0)