Skip to content

Commit e0b3e81

Browse files
nathanbowangr1tsuu
andauthored
fix(sdk): pass trash to request (#16092)
### What? The REST SDK documents a `trash` option on `find`, `findVersions`, and `findVersionByID`, but that flag was never added to the request URL. The Local API and REST handlers already support `trash` via query params (`parseParams` / collection endpoints). ### Why? Users enabling [soft delete (trash)](https://payloadcms.com/docs/trash/overview) need `trash: true` on list/version queries to include trashed documents (or to query the trash view). Passing `trash` from the SDK had no effect because `buildSearchParams` omitted it, so the REST API always behaved as if `trash` was unset. ### How? - Extended `OperationArgs` in `packages/sdk/src/utilities/buildSearchParams.ts` with optional `trash?: boolean`. - When `trash` is a boolean, serialize it to the query string (same pattern as `draft`). - Added an integration test in `test/sdk/int.spec.ts` that uses a stub `fetch` and asserts the requested URL contains `trash=true` for `find`, `findVersions`, and `findVersionByID`. **Before:** `sdk.find({ collection: 'posts', trash: true })` → request had no `trash` query param. **After:** same call → `...?trash=true&...` (alongside other params). Fixes # --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1213864434332542 --------- Co-authored-by: Sasha Rakhmatulin <sasha@ritsuko.dev>
1 parent 727d74e commit e0b3e81

7 files changed

Lines changed: 160 additions & 13 deletions

File tree

packages/sdk/src/collections/count.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export type CountOptions<T extends PayloadTypesShape, TSlug extends CollectionSl
1111
* Specify [locale](https://payloadcms.com/docs/configuration/localization) for any returned documents.
1212
*/
1313
locale?: 'all' | TypedLocale<T>
14+
/**
15+
* When `true`, the count includes trashed documents (same semantics as `find`). No effect unless the collection has `trash` enabled.
16+
* @default false
17+
*/
18+
trash?: boolean
1419
/**
1520
* A filter [query](https://payloadcms.com/docs/queries/overview)
1621
*/

packages/sdk/src/collections/delete.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ export type DeleteBaseOptions<
3838
* Specify [select](https://payloadcms.com/docs/queries/select) to control which fields to include to the result.
3939
*/
4040
select?: TSelect
41+
/**
42+
* When the collection has `trash` enabled: use with the REST API semantics for permanent delete vs bulk operations on trashed docs.
43+
* @default false
44+
*/
45+
trash?: boolean
4146
}
4247

4348
export type DeleteByIDOptions<

packages/sdk/src/collections/findByID.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export type FindByIDOptions<
5959
* Specify [populate](https://payloadcms.com/docs/queries/select#populate) to control which fields to include to the result from populated documents.
6060
*/
6161
populate?: PopulateType<T>
62+
/**
63+
* When `true`, returns the document even if it is trashed. No effect unless the collection has `trash` enabled.
64+
* @default false
65+
*/
66+
trash?: boolean
6267
} & Pick<FindOptions<TSlug, SelectType & TSelect>, 'select'>
6368

6469
export async function findByID<

packages/sdk/src/collections/update.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export type UpdateBaseOptions<
6767
* Specify [select](https://payloadcms.com/docs/queries/select) to control which fields to include to the result.
6868
*/
6969
select?: TSelect
70+
/**
71+
* When `true`, the operation can target trashed (soft-deleted) documents. No effect unless the collection has `trash` enabled.
72+
* @default false
73+
*/
74+
trash?: boolean
7075
}
7176

7277
export type UpdateByIDOptions<

packages/sdk/src/utilities/buildSearchParams.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type OperationArgs = {
1414
populate?: Record<string, unknown>
1515
select?: unknown
1616
sort?: Sort
17+
trash?: boolean
1718
where?: Where
1819
}
1920

@@ -36,6 +37,10 @@ export const buildSearchParams = (args: OperationArgs): string => {
3637
search.draft = String(args.draft)
3738
}
3839

40+
if (typeof args.trash === 'boolean') {
41+
search.trash = String(args.trash)
42+
}
43+
3944
if (typeof args.pagination === 'boolean') {
4045
search.pagination = String(args.pagination)
4146
}

test/sdk/collections/Posts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ export const PostsCollection: CollectionConfig = {
3636
],
3737
},
3838
],
39+
trash: true,
3940
versions: true,
4041
}

test/sdk/int.spec.ts

Lines changed: 134 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { emailsSlug } from './collections/Emails.js'
1515

1616
let payload: Payload
1717
let post: Post
18+
let postTrash: Post
1819
let sdk: TypedPayloadSDK
1920

2021
const filename = fileURLToPath(import.meta.url)
@@ -30,6 +31,10 @@ describe('@payloadcms/sdk', () => {
3031
;({ payload, sdk } = await initPayloadInt(dirname))
3132

3233
post = await payload.create({ collection: 'posts', data: { number: 1, number2: 3 } })
34+
postTrash = await payload.create({
35+
collection: 'posts',
36+
data: { deletedAt: new Date().toISOString(), text: 'fixture-trash' },
37+
})
3338
await payload.create({
3439
collection: 'users',
3540
data: { ...testUserCredentials },
@@ -60,6 +65,15 @@ describe('@payloadcms/sdk', () => {
6065
await payload.delete({ collection: 'posts', where: { id: { in: ids } } })
6166
})
6267

68+
it('should execute find with trash', async () => {
69+
const pairWhere = { id: { in: [post.id, postTrash.id] } }
70+
71+
expect((await sdk.find({ collection: 'posts', where: pairWhere })).docs).toHaveLength(1)
72+
expect(
73+
(await sdk.find({ collection: 'posts', trash: true, where: pairWhere })).docs,
74+
).toHaveLength(2)
75+
})
76+
6377
it('should execute findVersions', async () => {
6478
const result = await sdk.findVersions({
6579
collection: 'posts',
@@ -69,12 +83,30 @@ describe('@payloadcms/sdk', () => {
6983
expect(result.docs[0].parent).toBe(post.id)
7084
})
7185

86+
it('should execute findVersions with trash', async () => {
87+
const pairWhere = { parent: { in: [post.id, postTrash.id] } }
88+
89+
expect((await sdk.findVersions({ collection: 'posts', where: pairWhere })).docs).toHaveLength(1)
90+
expect(
91+
(await sdk.findVersions({ collection: 'posts', trash: true, where: pairWhere })).docs,
92+
).toHaveLength(2)
93+
})
94+
7295
it('should execute findByID', async () => {
7396
const result = await sdk.findByID({ collection: 'posts', id: post.id })
7497

7598
expect(result.id).toBe(post.id)
7699
})
77100

101+
it('should execute findByID with trash', async () => {
102+
expect(
103+
await sdk.findByID({ collection: 'posts', id: postTrash.id, disableErrors: true }),
104+
).toBeNull()
105+
expect((await sdk.findByID({ collection: 'posts', id: postTrash.id, trash: true })).id).toEqual(
106+
postTrash.id,
107+
)
108+
})
109+
78110
it('should execute findByID with disableErrors: true', async () => {
79111
const result = await sdk.findByID({
80112
disableErrors: true,
@@ -95,6 +127,23 @@ describe('@payloadcms/sdk', () => {
95127
expect(result.id).toBe(version.id)
96128
})
97129

130+
it('should execute findVersionByID with trash', async () => {
131+
const {
132+
docs: [trashVersion],
133+
} = await payload.findVersions({
134+
collection: 'posts',
135+
trash: true,
136+
where: { parent: { equals: postTrash.id } },
137+
})
138+
139+
expect(
140+
await sdk.findVersionByID({ collection: 'posts', id: trashVersion.id, disableErrors: true }),
141+
).toBeNull()
142+
expect(
143+
(await sdk.findVersionByID({ collection: 'posts', id: trashVersion.id, trash: true })).id,
144+
).toBe(trashVersion.id)
145+
})
146+
98147
it('should execute create', async () => {
99148
const result = await sdk.create({ collection: 'posts', data: { text: 'text' } })
100149

@@ -115,6 +164,18 @@ describe('@payloadcms/sdk', () => {
115164
expect(result.totalDocs).toBe(1)
116165
})
117166

167+
it('should execute count with trash', async () => {
168+
expect(
169+
(
170+
await sdk.count({
171+
collection: 'posts',
172+
trash: true,
173+
where: { id: { in: [post.id, postTrash.id] } },
174+
})
175+
).totalDocs,
176+
).toBe(2)
177+
})
178+
118179
it('should execute update (by ID)', async () => {
119180
const result = await sdk.update({
120181
collection: 'posts',
@@ -125,34 +186,65 @@ describe('@payloadcms/sdk', () => {
125186
expect(result.text).toBe('updated-text')
126187
})
127188

189+
it('should execute update (by ID) with trash', async () => {
190+
const result = await sdk.update({
191+
collection: 'posts',
192+
id: postTrash.id,
193+
data: { text: 'updated-trash-by-id' },
194+
trash: true,
195+
})
196+
197+
expect(result.text).toBe('updated-trash-by-id')
198+
})
199+
128200
it('should execute update (bulk)', async () => {
129201
const result = await sdk.update({
130202
collection: 'posts',
131-
where: {
132-
id: {
133-
equals: post.id,
134-
},
135-
},
203+
where: { id: { equals: post.id } },
136204
data: { text: 'updated-text-bulk' },
137205
})
138206

139207
expect(result.docs[0].text).toBe('updated-text-bulk')
140208
})
141209

210+
it('should execute update (bulk) with trash', async () => {
211+
const result = await sdk.update({
212+
collection: 'posts',
213+
where: { id: { equals: postTrash.id } },
214+
data: { text: 'updated-trash-bulk' },
215+
trash: true,
216+
})
217+
218+
expect(result.docs[0].text).toBe('updated-trash-bulk')
219+
})
220+
142221
it('should execute delete (by ID)', async () => {
143222
const post = await payload.create({ collection: 'posts', data: {} })
144223

145224
const result = await sdk.delete({ id: post.id, collection: 'posts' })
146225

147226
expect(result.id).toBe(post.id)
227+
expect(
228+
await payload.findByID({ collection: 'posts', id: post.id, disableErrors: true }),
229+
).toBeNull()
230+
})
148231

149-
const resultLocal = await payload.findByID({
232+
it('should execute delete (by ID) with trash', async () => {
233+
const trashed = await payload.create({
150234
collection: 'posts',
151-
id: post.id,
152-
disableErrors: true,
235+
data: { deletedAt: new Date().toISOString() },
153236
})
154237

155-
expect(resultLocal).toBeNull()
238+
await sdk.delete({ collection: 'posts', id: trashed.id, trash: true })
239+
240+
expect(
241+
await payload.findByID({
242+
collection: 'posts',
243+
disableErrors: true,
244+
id: trashed.id,
245+
trash: true,
246+
}),
247+
).toBeNull()
156248
})
157249

158250
it('should execute delete (bulk)', async () => {
@@ -161,14 +253,43 @@ describe('@payloadcms/sdk', () => {
161253
const result = await sdk.delete({ where: { id: { equals: post.id } }, collection: 'posts' })
162254

163255
expect(result.docs[0].id).toBe(post.id)
256+
expect(
257+
await payload.findByID({ collection: 'posts', id: post.id, disableErrors: true }),
258+
).toBeNull()
259+
})
164260

165-
const resultLocal = await payload.findByID({
261+
it('should execute delete (bulk) with trash', async () => {
262+
const trashedA = await payload.create({
166263
collection: 'posts',
167-
id: post.id,
168-
disableErrors: true,
264+
data: { deletedAt: new Date().toISOString(), text: 'bulk-perma-a' },
265+
})
266+
const trashedB = await payload.create({
267+
collection: 'posts',
268+
data: { deletedAt: new Date().toISOString(), text: 'bulk-perma-b' },
269+
})
270+
271+
await sdk.delete({
272+
collection: 'posts',
273+
trash: true,
274+
where: { id: { in: [trashedA.id, trashedB.id] } },
169275
})
170276

171-
expect(resultLocal).toBeNull()
277+
expect(
278+
await payload.findByID({
279+
collection: 'posts',
280+
disableErrors: true,
281+
id: trashedA.id,
282+
trash: true,
283+
}),
284+
).toBeNull()
285+
expect(
286+
await payload.findByID({
287+
collection: 'posts',
288+
disableErrors: true,
289+
id: trashedB.id,
290+
trash: true,
291+
}),
292+
).toBeNull()
172293
})
173294

174295
it('should execute restoreVersion', async () => {

0 commit comments

Comments
 (0)