Skip to content

Commit 3c9f56e

Browse files
authored
chore: standardize query param parsing (#14498)
The way we parse query params within endpoint handlers is currently non-standard. This leads to subtle differences in how they are handled across endpoints, e.g. using searchParams vs req.query, allowing values to fallback to undefined, etc. For background, each endpoint accepts arbitrary query params and must transform them into what Payload expects. For example: - `draft` provided as a string of "true" is converted to a boolean - `depth` provided as a string of "0" is converted to a number - `sort` provided as a comma-separated string is converted to an array of strings It is also easy to overlook params that should fallback to `undefined`. Not doing so will prevent the underlying operation from using its defaults, if any: ```ts await update({ // ... trash: trash === 'true' // This is never `undefined` so the default arg will not used (if any) }) ``` Parsing these params is now shared via a single `parseParams` utility, which takes in raw query parameters and returns them typed and properly formatted. ```ts const { depth, // number | undefined draft // number | undefined } = parseParams(req.query) ```
1 parent a3c0e84 commit 3c9f56e

File tree

19 files changed

+516
-205
lines changed

19 files changed

+516
-205
lines changed

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
import { status as httpStatus } from 'http-status'
22

33
import type { PayloadHandler } from '../../config/types.js'
4-
import type { Where } from '../../types/index.js'
54

65
import { getRequestCollection } from '../../utilities/getRequestEntity.js'
6+
import { parseParams } from '../../utilities/parseParams/index.js'
77
import { countOperation } from '../operations/count.js'
88

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

1614
const result = await countOperation({
1715
collection,
1816
req,
19-
trash: trash === 'true',
17+
trash,
2018
where,
2119
})
2220

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,26 @@ import type { PayloadHandler } from '../../config/types.js'
55

66
import { getRequestCollection } from '../../utilities/getRequestEntity.js'
77
import { headersWithCors } from '../../utilities/headersWithCors.js'
8-
import { isNumber } from '../../utilities/isNumber.js'
9-
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
10-
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
8+
import { parseParams } from '../../utilities/parseParams/index.js'
119
import { createOperation } from '../operations/create.js'
1210

1311
export const createHandler: PayloadHandler = async (req) => {
1412
const collection = getRequestCollection(req)
15-
const { searchParams } = req
16-
const autosave = searchParams.get('autosave') === 'true'
17-
const draft = searchParams.get('draft') === 'true'
18-
const depth = searchParams.get('depth')
13+
14+
const { autosave, depth, draft, populate, select } = parseParams(req.query)
15+
1916
const publishSpecificLocale = req.query.publishSpecificLocale as string | undefined
2017

2118
const doc = await createOperation({
2219
autosave,
2320
collection,
2421
data: req.data!,
25-
depth: isNumber(depth) ? depth : undefined,
22+
depth,
2623
draft,
27-
populate: sanitizePopulateParam(req.query.populate),
24+
populate,
2825
publishSpecificLocale,
2926
req,
30-
select: sanitizeSelectParam(req.query.select),
27+
select,
3128
})
3229

3330
return Response.json(

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

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,25 @@ import { getTranslation } from '@payloadcms/translations'
22
import { status as httpStatus } from 'http-status'
33

44
import type { PayloadHandler } from '../../config/types.js'
5-
import type { Where } from '../../types/index.js'
65

76
import { getRequestCollection } from '../../utilities/getRequestEntity.js'
87
import { headersWithCors } from '../../utilities/headersWithCors.js'
9-
import { isNumber } from '../../utilities/isNumber.js'
10-
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
11-
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
8+
import { parseParams } from '../../utilities/parseParams/index.js'
129
import { deleteOperation } from '../operations/delete.js'
1310

1411
export const deleteHandler: PayloadHandler = async (req) => {
1512
const collection = getRequestCollection(req)
16-
const { depth, overrideLock, populate, select, trash, where } = req.query as {
17-
depth?: string
18-
overrideLock?: string
19-
populate?: Record<string, unknown>
20-
select?: Record<string, unknown>
21-
trash?: string
22-
where?: Where
23-
}
13+
14+
const { depth, overrideLock, populate, select, trash, where } = parseParams(req.query)
2415

2516
const result = await deleteOperation({
2617
collection,
27-
depth: isNumber(depth) ? Number(depth) : undefined,
28-
overrideLock: Boolean(overrideLock === 'true'),
29-
populate: sanitizePopulateParam(populate),
18+
depth,
19+
overrideLock: overrideLock ?? false,
20+
populate,
3021
req,
31-
select: sanitizeSelectParam(select),
32-
trash: trash === 'true',
22+
select,
23+
trash,
3324
where: where!,
3425
})
3526

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,22 @@ import type { PayloadHandler } from '../../config/types.js'
44

55
import { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'
66
import { headersWithCors } from '../../utilities/headersWithCors.js'
7-
import { isNumber } from '../../utilities/isNumber.js'
8-
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
9-
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
7+
import { parseParams } from '../../utilities/parseParams/index.js'
108
import { deleteByIDOperation } from '../operations/deleteByID.js'
119

1210
export const deleteByIDHandler: PayloadHandler = async (req) => {
1311
const { id, collection } = getRequestCollectionWithID(req)
14-
const { searchParams } = req
15-
const depth = searchParams.get('depth')
16-
const overrideLock = searchParams.get('overrideLock')
17-
const trash = searchParams.get('trash') === 'true'
12+
13+
const { depth, overrideLock, populate, select, trash } = parseParams(req.query)
1814

1915
const doc = await deleteByIDOperation({
2016
id,
2117
collection,
22-
depth: isNumber(depth) ? depth : undefined,
23-
overrideLock: Boolean(overrideLock === 'true'),
24-
populate: sanitizePopulateParam(req.query.populate),
18+
depth,
19+
overrideLock: overrideLock ?? false,
20+
populate,
2521
req,
26-
select: sanitizeSelectParam(req.query.select),
22+
select,
2723
trash,
2824
})
2925

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { docAccessOperation } from '../operations/docAccess.js'
88

99
export const docAccessHandler: PayloadHandler = async (req) => {
1010
const { id, collection } = getRequestCollectionWithID(req, { optionalID: true })
11+
1112
const result = await docAccessOperation({
1213
id,
1314
collection,

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,23 @@ import type { PayloadHandler } from '../../config/types.js'
55

66
import { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'
77
import { headersWithCors } from '../../utilities/headersWithCors.js'
8-
import { isNumber } from '../../utilities/isNumber.js'
9-
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
10-
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
8+
import { parseParams } from '../../utilities/parseParams/index.js'
119
import { duplicateOperation } from '../operations/duplicate.js'
1210

1311
export const duplicateHandler: PayloadHandler = async (req) => {
1412
const { id, collection } = getRequestCollectionWithID(req)
15-
const { depth, draft, populate, select, selectedLocales } = req.query as {
16-
depth?: string
17-
draft?: string
18-
populate?: Record<string, unknown>
19-
select?: Record<string, unknown>
20-
selectedLocales?: string[]
21-
}
13+
14+
const { depth, draft, populate, select, selectedLocales } = parseParams(req.query)
2215

2316
const doc = await duplicateOperation({
2417
id,
2518
collection,
2619
data: req.data,
27-
depth: isNumber(depth) ? Number(depth) : undefined,
28-
draft: draft === 'true',
29-
populate: sanitizePopulateParam(populate),
20+
depth,
21+
draft,
22+
populate,
3023
req,
31-
select: sanitizeSelectParam(select),
24+
select,
3225
selectedLocales,
3326
})
3427

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

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,31 @@
11
import { status as httpStatus } from 'http-status'
22

33
import type { PayloadHandler } from '../../config/types.js'
4-
import type { Where } from '../../types/index.js'
5-
import type { JoinParams } from '../../utilities/sanitizeJoinParams.js'
64

75
import { getRequestCollection } from '../../utilities/getRequestEntity.js'
86
import { headersWithCors } from '../../utilities/headersWithCors.js'
9-
import { isNumber } from '../../utilities/isNumber.js'
10-
import { sanitizeJoinParams } from '../../utilities/sanitizeJoinParams.js'
11-
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
12-
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
7+
import { parseParams } from '../../utilities/parseParams/index.js'
138
import { findOperation } from '../operations/find.js'
149

1510
export const findHandler: PayloadHandler = async (req) => {
1611
const collection = getRequestCollection(req)
12+
1713
const { depth, draft, joins, limit, page, pagination, populate, select, sort, trash, where } =
18-
req.query as {
19-
depth?: string
20-
draft?: string
21-
joins?: JoinParams
22-
limit?: string
23-
page?: string
24-
pagination?: string
25-
populate?: Record<string, unknown>
26-
select?: Record<string, unknown>
27-
sort?: string
28-
trash?: string
29-
where?: Where
30-
}
14+
parseParams(req.query)
3115

3216
const result = await findOperation({
3317
collection,
34-
depth: isNumber(depth) ? Number(depth) : undefined,
35-
draft: draft === 'true',
36-
joins: sanitizeJoinParams(joins),
37-
limit: isNumber(limit) ? Number(limit) : undefined,
38-
page: isNumber(page) ? Number(page) : undefined,
39-
pagination: pagination === 'false' ? false : undefined,
40-
populate: sanitizePopulateParam(populate),
18+
depth,
19+
draft,
20+
joins,
21+
limit,
22+
page,
23+
pagination,
24+
populate,
4125
req,
42-
select: sanitizeSelectParam(select),
43-
sort: typeof sort === 'string' ? sort.split(',') : undefined,
44-
trash: trash === 'true',
26+
select,
27+
sort,
28+
trash,
4529
where,
4630
})
4731

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

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,29 @@ import type { PayloadHandler } from '../../config/types.js'
44

55
import { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'
66
import { headersWithCors } from '../../utilities/headersWithCors.js'
7-
import { isNumber } from '../../utilities/isNumber.js'
8-
import { type JoinParams, sanitizeJoinParams } from '../../utilities/sanitizeJoinParams.js'
9-
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
10-
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
7+
import { parseParams } from '../../utilities/parseParams/index.js'
118
import { findByIDOperation } from '../operations/findByID.js'
129

1310
export const findByIDHandler: PayloadHandler = async (req) => {
14-
const { data, searchParams } = req
11+
const { data: dataArg } = req
1512
const { id, collection } = getRequestCollectionWithID(req)
16-
const depth = data ? data.depth : searchParams.get('depth')
17-
const trash = data ? data.trash : searchParams.get('trash') === 'true'
18-
const flattenLocales = data
19-
? data.flattenLocales
20-
: searchParams.has('flattenLocales')
21-
? searchParams.get('flattenLocales') === 'true'
22-
: // flattenLocales should be undefined if not provided, so that the default (true) is applied in the operation
23-
undefined
13+
14+
const { data, depth, draft, flattenLocales, joins, populate, select, trash } = parseParams({
15+
...req.query,
16+
...dataArg,
17+
})
2418

2519
const result = await findByIDOperation({
2620
id,
2721
collection,
28-
data: data
29-
? data?.data
30-
: searchParams.get('data')
31-
? JSON.parse(searchParams.get('data') as string)
32-
: undefined,
33-
depth: isNumber(depth) ? Number(depth) : undefined,
34-
draft: data ? data.draft : searchParams.get('draft') === 'true',
22+
data,
23+
depth,
24+
draft,
3525
flattenLocales,
36-
joins: sanitizeJoinParams(req.query.joins as JoinParams),
37-
populate: sanitizePopulateParam(req.query.populate),
26+
joins,
27+
populate,
3828
req,
39-
select: sanitizeSelectParam(req.query.select),
29+
select,
4030
trash,
4131
})
4232

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

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,31 @@
11
import { status as httpStatus } from 'http-status'
22

33
import type { PayloadHandler } from '../../config/types.js'
4-
import type { Where } from '../../types/index.js'
54

65
import { APIError } from '../../errors/APIError.js'
76
import { getRequestCollection } from '../../utilities/getRequestEntity.js'
87
import { headersWithCors } from '../../utilities/headersWithCors.js'
9-
import { isNumber } from '../../utilities/isNumber.js'
8+
import { parseParams } from '../../utilities/parseParams/index.js'
109
import { findDistinctOperation } from '../operations/findDistinct.js'
1110

1211
export const findDistinctHandler: PayloadHandler = async (req) => {
1312
const collection = getRequestCollection(req)
14-
const { depth, field, limit, page, sort, trash, where } = req.query as {
15-
depth?: string
16-
field?: string
17-
limit?: string
18-
page?: string
19-
sort?: string
20-
sortOrder?: string
21-
trash?: string
22-
where?: Where
23-
}
13+
14+
const { depth, field, limit, page, sort, trash, where } = parseParams(req.query)
2415

2516
if (!field) {
2617
throw new APIError('field must be specified', httpStatus.BAD_REQUEST)
2718
}
2819

2920
const result = await findDistinctOperation({
3021
collection,
31-
depth: isNumber(depth) ? Number(depth) : undefined,
22+
depth,
3223
field,
33-
limit: isNumber(limit) ? Number(limit) : undefined,
34-
page: isNumber(page) ? Number(page) : undefined,
24+
limit,
25+
page,
3526
req,
36-
sort: typeof sort === 'string' ? sort.split(',') : undefined,
37-
trash: trash === 'true',
27+
sort,
28+
trash,
3829
where,
3930
})
4031

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,21 @@ import type { PayloadHandler } from '../../config/types.js'
44

55
import { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'
66
import { headersWithCors } from '../../utilities/headersWithCors.js'
7-
import { isNumber } from '../../utilities/isNumber.js'
8-
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
9-
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
7+
import { parseParams } from '../../utilities/parseParams/index.js'
108
import { findVersionByIDOperation } from '../operations/findVersionByID.js'
119

1210
export const findVersionByIDHandler: PayloadHandler = async (req) => {
13-
const { searchParams } = req
14-
const depth = searchParams.get('depth')
15-
const trash = searchParams.get('trash') === 'true'
11+
const { depth, populate, select, trash } = parseParams(req.query)
1612

1713
const { id, collection } = getRequestCollectionWithID(req)
1814

1915
const result = await findVersionByIDOperation({
2016
id,
2117
collection,
22-
depth: isNumber(depth) ? Number(depth) : undefined,
23-
populate: sanitizePopulateParam(req.query.populate),
18+
depth,
19+
populate,
2420
req,
25-
select: sanitizeSelectParam(req.query.select),
21+
select,
2622
trash,
2723
})
2824

0 commit comments

Comments
 (0)