Skip to content

Commit 2ccf898

Browse files
feat: support qs-esm sort arrays in REST API (#15065)
### What? Support qs-esm array in sort parameter to sort by multiple columns in REST API ### Why? To align input of Local API and REST API so that REST API also accepts array like Local API do. ### How? Extract parsing/sanitizing of sort input to it's own method which also accept array. Added unit and integration test. Updated docs with example. Fixes #15052
1 parent 5cbdca1 commit 2ccf898

File tree

6 files changed

+75
-5
lines changed

6 files changed

+75
-5
lines changed

docs/queries/sort.mdx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ fetch('https://localhost:3000/api/posts?sort=priority,-createdAt') // highlight-
6767
.then((data) => console.log(data))
6868
```
6969

70+
You can also pass `sort` as an array so each item is a single field when using query string builder:
71+
72+
```ts
73+
import { stringify } from 'qs-esm'
74+
75+
const getPosts = async () => {
76+
const stringifiedQuery = stringify(
77+
{
78+
sort: ['priority', '-createdAt'],
79+
},
80+
{ addQueryPrefix: true },
81+
)
82+
83+
const response = await fetch(
84+
`https://localhost:3000/api/posts${stringifiedQuery}`, // highlight-line
85+
)
86+
const data = await response.json()
87+
return data
88+
}
89+
```
90+
7091
## GraphQL API
7192

7293
To sort in the [GraphQL API](../graphql/overview), you can use the `sort` parameter in your query:

packages/payload/src/globals/endpoints/findVersions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { headersWithCors } from '../../utilities/headersWithCors.js'
88
import { isNumber } from '../../utilities/isNumber.js'
99
import { sanitizePopulateParam } from '../../utilities/sanitizePopulateParam.js'
1010
import { sanitizeSelectParam } from '../../utilities/sanitizeSelectParam.js'
11+
import { sanitizeSortParams } from '../../utilities/sanitizeSortParams.js'
1112
import { findVersionsOperation } from '../operations/findVersions.js'
1213

1314
export const findVersionsHandler: PayloadHandler = async (req) => {
@@ -19,7 +20,7 @@ export const findVersionsHandler: PayloadHandler = async (req) => {
1920
pagination?: string
2021
populate?: Record<string, unknown>
2122
select?: Record<string, unknown>
22-
sort?: string
23+
sort?: string | string[]
2324
where?: Where
2425
}
2526

@@ -32,7 +33,7 @@ export const findVersionsHandler: PayloadHandler = async (req) => {
3233
populate: sanitizePopulateParam(populate),
3334
req,
3435
select: sanitizeSelectParam(select),
35-
sort: typeof sort === 'string' ? sort.split(',') : undefined,
36+
sort: sanitizeSortParams(sort),
3637
where,
3738
})
3839

packages/payload/src/utilities/parseParams/index.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as qs from 'qs-esm'
12
import { describe, it, expect } from 'vitest'
23
import { parseParams, booleanParams, numberParams } from './index.js'
34

@@ -105,11 +106,28 @@ describe('parseParams', () => {
105106
expect(result.sort).toEqual(['name', ' createdAt ', ' -updatedAt'])
106107
})
107108

109+
it('should parse array of strings', () => {
110+
const result = parseParams({ sort: ['name', '-createdAt'] })
111+
expect(result.sort).toEqual(['name', '-createdAt'])
112+
})
113+
108114
it('should return undefined for non-string sort values', () => {
109115
const result = parseParams({ sort: 123 as any })
110116
expect(result.sort).toBeUndefined()
111117
})
112118

119+
it('should return undefined for array with non-string sort values', () => {
120+
const result = parseParams({ sort: ['name', 123] as any })
121+
expect(result.sort).toBeUndefined()
122+
})
123+
124+
it('should handle qs-esm array sort parsing', () => {
125+
const query = qs.stringify({ sort: ['title', '-createdAt'] })
126+
const parsed = qs.parse(query)
127+
const result = parseParams(parsed)
128+
expect(result.sort).toEqual(['title', '-createdAt'])
129+
})
130+
113131
it('should return undefined for null sort values', () => {
114132
const result = parseParams({ sort: null as any })
115133
expect(result.sort).toBeUndefined()

packages/payload/src/utilities/parseParams/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parseBooleanString } from '../parseBooleanString.js'
66
import { sanitizeJoinParams } from '../sanitizeJoinParams.js'
77
import { sanitizePopulateParam } from '../sanitizePopulateParam.js'
88
import { sanitizeSelectParam } from '../sanitizeSelectParam.js'
9+
import { sanitizeSortParams } from '../sanitizeSortParams.js'
910

1011
type ParsedParams = {
1112
autosave?: boolean
@@ -45,7 +46,7 @@ type RawParams = {
4546
publishSpecificLocale?: string
4647
select?: unknown
4748
selectedLocales?: string
48-
sort?: string
49+
sort?: string | string[]
4950
trash?: string
5051
where?: Where
5152
}
@@ -66,7 +67,7 @@ export const numberParams = ['depth', 'limit', 'page']
6667
* Examples:
6768
* a. `draft` provided as a string of "true" is converted to a boolean
6869
* b. `depth` provided as a string of "0" is converted to a number
69-
* c. `sort` provided as a comma-separated string is converted to an array of strings
70+
* c. `sort` provided as a comma-separated string or array is converted to an array of strings
7071
*/
7172
export const parseParams = (params: RawParams): ParsedParams => {
7273
const parsedParams = (params || {}) as ParsedParams
@@ -99,7 +100,7 @@ export const parseParams = (params: RawParams): ParsedParams => {
99100
}
100101

101102
if ('sort' in params) {
102-
parsedParams.sort = typeof params.sort === 'string' ? params.sort.split(',') : undefined
103+
parsedParams.sort = sanitizeSortParams(params.sort)
103104
}
104105

105106
if ('data' in params && typeof params.data === 'string' && params.data.length > 0) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const sanitizeSortParams = (sort: unknown): string[] | undefined => {
2+
if (typeof sort === 'string') {
3+
return sort.split(',')
4+
}
5+
6+
if (Array.isArray(sort) && sort.every((value) => typeof value === 'string')) {
7+
return sort
8+
}
9+
10+
return undefined
11+
}

test/sort/int.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CollectionSlug, Payload } from 'payload'
22

33
import path from 'path'
4+
import * as qs from 'qs-esm'
45
import { fileURLToPath } from 'url'
56
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
67

@@ -915,6 +916,23 @@ describe('Sort', () => {
915916
])
916917
})
917918
})
919+
920+
describe('Sort by multiple fields as array', () => {
921+
it('should sort posts by multiple fields using qs-esm array params', async () => {
922+
const query = qs.stringify({ sort: ['number2', '-number'] })
923+
924+
const res = await restClient.GET(`/posts?${query}`).then((res) => res.json())
925+
926+
expect(res.docs.map((post) => post.text)).toEqual([
927+
'Post 10', // 5, 10
928+
'Post 3', // 5, 3
929+
'Post 2', // 10, 2
930+
'Post 1', // 10, 1
931+
'Post 12', // 20, 12
932+
'Post 11', // 20, 11
933+
])
934+
})
935+
})
918936
})
919937
})
920938

0 commit comments

Comments
 (0)