Skip to content

Commit 1175508

Browse files
authored
feat: adds trash support to the count operation (#13304)
### What? - Updated the `countOperation` to respect the `trash` argument. ### Why? - Previously, `count` would incorrectly include trashed documents even when `trash` was not specified. - This change aligns `count` behavior with `find` and other operations, providing accurate counts for normal and trashed documents. ### How? - Applied `appendNonTrashedFilter` in `countOperation` to automatically exclude soft-deleted docs when `trash: false` (default). - Added `trash` argument support in Local API, REST API (`/count` endpoints), and GraphQL (`count<Collection>` queries).
1 parent a8b6983 commit 1175508

File tree

6 files changed

+134
-3
lines changed

6 files changed

+134
-3
lines changed

packages/graphql/src/resolvers/collections/count.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type Resolver = (
99
args: {
1010
data: Record<string, unknown>
1111
locale?: string
12+
trash?: boolean
1213
where?: Where
1314
},
1415
context: {
@@ -30,6 +31,7 @@ export function countResolver(collection: Collection): Resolver {
3031
const options = {
3132
collection,
3233
req: isolateObjectProperty(req, 'transactionID'),
34+
trash: args.trash,
3335
where: args.where,
3436
}
3537

packages/graphql/src/schema/initCollections.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
239239
}),
240240
args: {
241241
draft: { type: GraphQLBoolean },
242+
trash: { type: GraphQLBoolean },
242243
where: { type: collection.graphQL.whereInputType },
243244
...(config.localization
244245
? {

packages/payload/src/collections/endpoints/count.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { countOperation } from '../operations/count.js'
88

99
export const countHandler: PayloadHandler = async (req) => {
1010
const collection = getRequestCollection(req)
11-
const { where } = req.query as {
11+
const { trash, where } = req.query as {
12+
trash?: string
1213
where?: Where
1314
}
1415

1516
const result = await countOperation({
1617
collection,
1718
req,
19+
trash: trash === 'true',
1820
where,
1921
})
2022

packages/payload/src/collections/operations/count.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { executeAccess } from '../../auth/executeAccess.js'
77
import { combineQueries } from '../../database/combineQueries.js'
88
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
99
import { sanitizeWhereQuery } from '../../database/sanitizeWhereQuery.js'
10+
import { appendNonTrashedFilter } from '../../utilities/appendNonTrashedFilter.js'
1011
import { killTransaction } from '../../utilities/killTransaction.js'
1112
import { buildAfterOperation } from './utils.js'
1213

@@ -15,6 +16,7 @@ export type Arguments = {
1516
disableErrors?: boolean
1617
overrideAccess?: boolean
1718
req?: PayloadRequest
19+
trash?: boolean
1820
where?: Where
1921
}
2022

@@ -47,6 +49,7 @@ export const countOperation = async <TSlug extends CollectionSlug>(
4749
disableErrors,
4850
overrideAccess,
4951
req,
52+
trash = false,
5053
where,
5154
} = args
5255

@@ -71,9 +74,16 @@ export const countOperation = async <TSlug extends CollectionSlug>(
7174

7275
let result: { totalDocs: number }
7376

74-
const fullWhere = combineQueries(where!, accessResult!)
77+
let fullWhere = combineQueries(where!, accessResult!)
7578
sanitizeWhereQuery({ fields: collectionConfig.flattenedFields, payload, where: fullWhere })
7679

80+
// Exclude trashed documents when trash: false
81+
fullWhere = appendNonTrashedFilter({
82+
enableTrash: collectionConfig.trash,
83+
trash,
84+
where: fullWhere,
85+
})
86+
7787
await validateQueryPaths({
7888
collectionConfig,
7989
overrideAccess: overrideAccess!,

packages/payload/src/collections/operations/local/count.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ export type Options<TSlug extends CollectionSlug> = {
4141
* Recommended to pass when using the Local API from hooks, as usually you want to execute the operation within the current transaction.
4242
*/
4343
req?: Partial<PayloadRequest>
44+
/**
45+
* When set to `true`, the query will include both normal and trashed documents.
46+
* To query only trashed documents, pass `trash: true` and combine with a `where` clause filtering by `deletedAt`.
47+
* By default (`false`), the query will only include normal documents and exclude those with a `deletedAt` field.
48+
*
49+
* This argument has no effect unless `trash` is enabled on the collection.
50+
* @default false
51+
*/
52+
trash?: boolean
4453
/**
4554
* If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks.
4655
*/
@@ -55,7 +64,13 @@ export async function countLocal<TSlug extends CollectionSlug>(
5564
payload: Payload,
5665
options: Options<TSlug>,
5766
): Promise<{ totalDocs: number }> {
58-
const { collection: collectionSlug, disableErrors, overrideAccess = true, where } = options
67+
const {
68+
collection: collectionSlug,
69+
disableErrors,
70+
overrideAccess = true,
71+
trash = false,
72+
where,
73+
} = options
5974

6075
const collection = payload.collections[collectionSlug]
6176

@@ -70,6 +85,7 @@ export async function countLocal<TSlug extends CollectionSlug>(
7085
disableErrors,
7186
overrideAccess,
7287
req: await createLocalReq(options as CreateLocalReqOptions, payload),
88+
trash,
7389
where,
7490
})
7591
}

test/trash/int.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,35 @@ describe('trash', () => {
709709
).rejects.toThrow(/Cannot restore a version of a trashed document/i)
710710
})
711711
})
712+
713+
describe('count operation', () => {
714+
it('should return total count of non-soft-deleted documents by default (trash: false)', async () => {
715+
const result = await payload.count({
716+
collection: postsSlug,
717+
})
718+
719+
expect(result.totalDocs).toEqual(1) // Only postsDocOne
720+
})
721+
722+
it('should return total count of all documents including soft-deleted when trash: true', async () => {
723+
const result = await payload.count({
724+
collection: postsSlug,
725+
trash: true,
726+
})
727+
728+
expect(result.totalDocs).toEqual(2)
729+
})
730+
731+
it('should return count of only soft-deleted documents when where[deletedAt][exists]=true', async () => {
732+
const result = await payload.count({
733+
collection: postsSlug,
734+
trash: true,
735+
where: { deletedAt: { exists: true } },
736+
})
737+
738+
expect(result.totalDocs).toEqual(1) // Only postsDocTwo
739+
})
740+
})
712741
})
713742

714743
describe('REST API', () => {
@@ -1055,6 +1084,30 @@ describe('trash', () => {
10551084
)
10561085
})
10571086
})
1087+
1088+
describe('count endpoint', () => {
1089+
it('should return count of non-soft-deleted docs by default (trash=false)', async () => {
1090+
const res = await restClient.GET(`/${postsSlug}/count`)
1091+
expect(res.status).toBe(200)
1092+
const data = await res.json()
1093+
expect(data.totalDocs).toEqual(1)
1094+
})
1095+
1096+
it('should return count of all docs including soft-deleted when trash=true', async () => {
1097+
const res = await restClient.GET(`/${postsSlug}/count?trash=true`)
1098+
expect(res.status).toBe(200)
1099+
const data = await res.json()
1100+
expect(data.totalDocs).toEqual(2)
1101+
})
1102+
1103+
it('should return count of only soft-deleted docs with trash=true & where[deletedAt][exists]=true', async () => {
1104+
const res = await restClient.GET(
1105+
`/${postsSlug}/count?trash=true&where[deletedAt][exists]=true`,
1106+
)
1107+
const data = await res.json()
1108+
expect(data.totalDocs).toEqual(1)
1109+
})
1110+
})
10581111
})
10591112

10601113
describe('GRAPHQL API', () => {
@@ -1632,5 +1685,52 @@ describe('trash', () => {
16321685
)
16331686
})
16341687
})
1688+
1689+
describe('count query', () => {
1690+
it('should return count of non-soft-deleted documents by default (trash=false)', async () => {
1691+
const query = `
1692+
query {
1693+
countPosts {
1694+
totalDocs
1695+
}
1696+
}
1697+
`
1698+
const res = await restClient
1699+
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
1700+
.then((r) => r.json())
1701+
1702+
expect(res.data.countPosts.totalDocs).toBe(1)
1703+
})
1704+
1705+
it('should return count of all documents including soft-deleted when trash=true', async () => {
1706+
const query = `
1707+
query {
1708+
countPosts(trash: true) {
1709+
totalDocs
1710+
}
1711+
}
1712+
`
1713+
const res = await restClient
1714+
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
1715+
.then((r) => r.json())
1716+
1717+
expect(res.data.countPosts.totalDocs).toBe(2)
1718+
})
1719+
1720+
it('should return count of only soft-deleted docs with where[deletedAt][exists]=true', async () => {
1721+
const query = `
1722+
query {
1723+
countPosts(trash: true, where: { deletedAt: { exists: true } }) {
1724+
totalDocs
1725+
}
1726+
}
1727+
`
1728+
const res = await restClient
1729+
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
1730+
.then((r) => r.json())
1731+
1732+
expect(res.data.countPosts.totalDocs).toBe(1)
1733+
})
1734+
})
16351735
})
16361736
})

0 commit comments

Comments
 (0)