feat: sort by multiple fields (#8799)
This change adds support for sort with multiple fields in local API and REST API. Related discussion #2089 Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
committed by
GitHub
parent
6e919cc83a
commit
4d44c378ed
@@ -58,13 +58,13 @@ export const Posts: CollectionConfig = {
|
|||||||
The following options are available:
|
The following options are available:
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/collections). |
|
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/collections). |
|
||||||
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||||
| **`auth`** | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
| **`auth`** | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||||
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||||
| **`defaultSort`** | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
| **`defaultSort`** | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||||
| **`dbName`** | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
| **`dbName`** | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||||
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ desc: Payload sort allows you to order your documents by a field in ascending or
|
|||||||
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||||
---
|
---
|
||||||
|
|
||||||
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order. If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order.
|
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order. If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specificed by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields.
|
||||||
|
|
||||||
Because sorting is handled by the database, the field cannot be a [Virtual Field](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges). It must be stored in the database to be searchable.
|
Because sorting is handled by the database, the field cannot be a [Virtual Field](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges). It must be stored in the database to be searchable.
|
||||||
|
|
||||||
@@ -30,6 +30,19 @@ const getPosts = async () => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To sort by multiple fields, you can use the `sort` option with fields in an array:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const getPosts = async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: ['priority', '-createdAt'], // highlight-line
|
||||||
|
})
|
||||||
|
|
||||||
|
return posts
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
To sort in the [REST API](../rest-api/overview), you can use the `sort` parameter in your query:
|
To sort in the [REST API](../rest-api/overview), you can use the `sort` parameter in your query:
|
||||||
@@ -40,6 +53,14 @@ fetch('https://localhost:3000/api/posts?sort=-createdAt') // highlight-line
|
|||||||
.then((data) => console.log(data))
|
.then((data) => console.log(data))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To sort by multiple fields, you can use the `sort` parameter with fields separated by comma:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
fetch('https://localhost:3000/api/posts?sort=priority,-createdAt') // highlight-line
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => console.log(data))
|
||||||
|
```
|
||||||
|
|
||||||
## GraphQL API
|
## GraphQL API
|
||||||
|
|
||||||
To sort in the [GraphQL API](../graphql/overview), you can use the `sort` parameter in your query:
|
To sort in the [GraphQL API](../graphql/overview), you can use the `sort` parameter in your query:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PaginateOptions } from 'mongoose'
|
import type { PaginateOptions } from 'mongoose'
|
||||||
import type { Field, SanitizedConfig } from 'payload'
|
import type { Field, SanitizedConfig, Sort } from 'payload'
|
||||||
|
|
||||||
import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'
|
import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ type Args = {
|
|||||||
config: SanitizedConfig
|
config: SanitizedConfig
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
locale: string
|
locale: string
|
||||||
sort: string
|
sort: Sort
|
||||||
timestamps: boolean
|
timestamps: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,32 +25,41 @@ export const buildSortParam = ({
|
|||||||
sort,
|
sort,
|
||||||
timestamps,
|
timestamps,
|
||||||
}: Args): PaginateOptions['sort'] => {
|
}: Args): PaginateOptions['sort'] => {
|
||||||
let sortProperty: string
|
|
||||||
let sortDirection: SortDirection = 'desc'
|
|
||||||
|
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
if (timestamps) {
|
if (timestamps) {
|
||||||
sortProperty = 'createdAt'
|
sort = '-createdAt'
|
||||||
} else {
|
} else {
|
||||||
sortProperty = '_id'
|
sort = '-id'
|
||||||
}
|
}
|
||||||
} else if (sort.indexOf('-') === 0) {
|
|
||||||
sortProperty = sort.substring(1)
|
|
||||||
} else {
|
|
||||||
sortProperty = sort
|
|
||||||
sortDirection = 'asc'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortProperty === 'id') {
|
if (typeof sort === 'string') {
|
||||||
sortProperty = '_id'
|
sort = [sort]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorting = sort.reduce<PaginateOptions['sort']>((acc, item) => {
|
||||||
|
let sortProperty: string
|
||||||
|
let sortDirection: SortDirection
|
||||||
|
if (item.indexOf('-') === 0) {
|
||||||
|
sortProperty = item.substring(1)
|
||||||
|
sortDirection = 'desc'
|
||||||
} else {
|
} else {
|
||||||
sortProperty = getLocalizedSortProperty({
|
sortProperty = item
|
||||||
|
sortDirection = 'asc'
|
||||||
|
}
|
||||||
|
if (sortProperty === 'id') {
|
||||||
|
acc['_id'] = sortDirection
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
const localizedProperty = getLocalizedSortProperty({
|
||||||
config,
|
config,
|
||||||
fields,
|
fields,
|
||||||
locale,
|
locale,
|
||||||
segments: sortProperty.split('.'),
|
segments: sortProperty.split('.'),
|
||||||
})
|
})
|
||||||
}
|
acc[localizedProperty] = sortDirection
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
return { [sortProperty]: sortDirection }
|
return sorting
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const find: Find = async function find(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||||
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
|
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collectionConfig.defaultSort
|
||||||
|
|
||||||
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
|
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
|
||||||
|
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ export const findMany = async function find({
|
|||||||
|
|
||||||
const selectDistinctMethods: ChainedMethods = []
|
const selectDistinctMethods: ChainedMethods = []
|
||||||
|
|
||||||
if (orderBy?.order && orderBy?.column) {
|
if (orderBy) {
|
||||||
selectDistinctMethods.push({
|
selectDistinctMethods.push({
|
||||||
args: [orderBy.order(orderBy.column)],
|
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||||
method: 'orderBy',
|
method: 'orderBy',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ export const findMany = async function find({
|
|||||||
} else {
|
} else {
|
||||||
findManyArgs.limit = limit
|
findManyArgs.limit = limit
|
||||||
findManyArgs.offset = offset
|
findManyArgs.offset = offset
|
||||||
findManyArgs.orderBy = orderBy.order(orderBy.column)
|
findManyArgs.orderBy = () => orderBy.map(({ column, order }) => order(column))
|
||||||
|
|
||||||
if (where) {
|
if (where) {
|
||||||
findManyArgs.where = where
|
findManyArgs.where = where
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ export const traverseFields = ({
|
|||||||
})
|
})
|
||||||
.from(adapter.tables[joinCollectionTableName])
|
.from(adapter.tables[joinCollectionTableName])
|
||||||
.where(subQueryWhere)
|
.where(subQueryWhere)
|
||||||
.orderBy(orderBy.order(orderBy.column)),
|
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
||||||
})
|
})
|
||||||
|
|
||||||
const columnName = `${path.replaceAll('.', '_')}${field.name}`
|
const columnName = `${path.replaceAll('.', '_')}${field.name}`
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
|||||||
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
|
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
|
||||||
({ slug }) => slug === global,
|
({ slug }) => slug === global,
|
||||||
)
|
)
|
||||||
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
|
const sort = sortArg !== undefined && sortArg !== null ? sortArg : '-createdAt'
|
||||||
|
|
||||||
const tableName = this.tableNameMap.get(
|
const tableName = this.tableNameMap.get(
|
||||||
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
|
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const findVersions: FindVersions = async function findVersions(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||||
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
|
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collectionConfig.defaultSort
|
||||||
|
|
||||||
const tableName = this.tableNameMap.get(
|
const tableName = this.tableNameMap.get(
|
||||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Field } from 'payload'
|
import type { Field, Sort } from 'payload'
|
||||||
|
|
||||||
import { asc, desc } from 'drizzle-orm'
|
import { asc, desc } from 'drizzle-orm'
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ type Args = {
|
|||||||
joins: BuildQueryJoinAliases
|
joins: BuildQueryJoinAliases
|
||||||
locale?: string
|
locale?: string
|
||||||
selectFields: Record<string, GenericColumn>
|
selectFields: Record<string, GenericColumn>
|
||||||
sort?: string
|
sort?: Sort
|
||||||
tableName: string
|
tableName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,54 +29,55 @@ export const buildOrderBy = ({
|
|||||||
sort,
|
sort,
|
||||||
tableName,
|
tableName,
|
||||||
}: Args): BuildQueryResult['orderBy'] => {
|
}: Args): BuildQueryResult['orderBy'] => {
|
||||||
const orderBy: BuildQueryResult['orderBy'] = {
|
const orderBy: BuildQueryResult['orderBy'] = []
|
||||||
column: null,
|
|
||||||
order: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sort) {
|
if (!sort) {
|
||||||
let sortPath
|
const createdAt = adapter.tables[tableName]?.createdAt
|
||||||
|
if (createdAt) {
|
||||||
if (sort[0] === '-') {
|
sort = '-createdAt'
|
||||||
sortPath = sort.substring(1)
|
|
||||||
orderBy.order = desc
|
|
||||||
} else {
|
} else {
|
||||||
sortPath = sort
|
sort = '-id'
|
||||||
orderBy.order = asc
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof sort === 'string') {
|
||||||
|
sort = [sort]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sortItem of sort) {
|
||||||
|
let sortProperty: string
|
||||||
|
let sortDirection: 'asc' | 'desc'
|
||||||
|
if (sortItem[0] === '-') {
|
||||||
|
sortProperty = sortItem.substring(1)
|
||||||
|
sortDirection = 'desc'
|
||||||
|
} else {
|
||||||
|
sortProperty = sortItem
|
||||||
|
sortDirection = 'asc'
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
|
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
|
||||||
adapter,
|
adapter,
|
||||||
collectionPath: sortPath,
|
collectionPath: sortProperty,
|
||||||
fields,
|
fields,
|
||||||
joins,
|
joins,
|
||||||
locale,
|
locale,
|
||||||
pathSegments: sortPath.replace(/__/g, '.').split('.'),
|
pathSegments: sortProperty.replace(/__/g, '.').split('.'),
|
||||||
selectFields,
|
selectFields,
|
||||||
tableName,
|
tableName,
|
||||||
value: sortPath,
|
value: sortProperty,
|
||||||
})
|
})
|
||||||
orderBy.column = sortTable?.[sortTableColumnName]
|
if (sortTable?.[sortTableColumnName]) {
|
||||||
|
orderBy.push({
|
||||||
|
column: sortTable[sortTableColumnName],
|
||||||
|
order: sortDirection === 'asc' ? asc : desc,
|
||||||
|
})
|
||||||
|
|
||||||
|
selectFields[sortTableColumnName] = sortTable[sortTableColumnName]
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// continue
|
// continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!orderBy?.column) {
|
|
||||||
orderBy.order = desc
|
|
||||||
const createdAt = adapter.tables[tableName]?.createdAt
|
|
||||||
|
|
||||||
if (createdAt) {
|
|
||||||
orderBy.column = createdAt
|
|
||||||
} else {
|
|
||||||
orderBy.column = adapter.tables[tableName].id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderBy.column) {
|
|
||||||
selectFields.sort = orderBy.column
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderBy
|
return orderBy
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { asc, desc, SQL } from 'drizzle-orm'
|
import type { asc, desc, SQL } from 'drizzle-orm'
|
||||||
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
|
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||||
import type { Field, Where } from 'payload'
|
import type { Field, Sort, Where } from 'payload'
|
||||||
|
|
||||||
import type { DrizzleAdapter, GenericColumn, GenericTable } from '../types.js'
|
import type { DrizzleAdapter, GenericColumn, GenericTable } from '../types.js'
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ type BuildQueryArgs = {
|
|||||||
fields: Field[]
|
fields: Field[]
|
||||||
joins?: BuildQueryJoinAliases
|
joins?: BuildQueryJoinAliases
|
||||||
locale?: string
|
locale?: string
|
||||||
sort?: string
|
sort?: Sort
|
||||||
tableName: string
|
tableName: string
|
||||||
where: Where
|
where: Where
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ export type BuildQueryResult = {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
column: GenericColumn
|
column: GenericColumn
|
||||||
order: typeof asc | typeof desc
|
order: typeof asc | typeof desc
|
||||||
}
|
}[]
|
||||||
selectFields: Record<string, GenericColumn>
|
selectFields: Record<string, GenericColumn>
|
||||||
where: SQL
|
where: SQL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
|||||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||||
page: isNumber(page) ? Number(page) : undefined,
|
page: isNumber(page) ? Number(page) : undefined,
|
||||||
req,
|
req,
|
||||||
sort,
|
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const findVersions: CollectionRouteHandler = async ({ collection, req })
|
|||||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||||
page: isNumber(page) ? Number(page) : undefined,
|
page: isNumber(page) ? Number(page) : undefined,
|
||||||
req,
|
req,
|
||||||
sort,
|
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) =>
|
|||||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||||
page: isNumber(page) ? Number(page) : undefined,
|
page: isNumber(page) ? Number(page) : undefined,
|
||||||
req,
|
req,
|
||||||
sort,
|
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||||
where,
|
where,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
const sort =
|
const sort =
|
||||||
query?.sort && typeof query.sort === 'string'
|
query?.sort && typeof query.sort === 'string'
|
||||||
? query.sort
|
? query.sort
|
||||||
: listPreferences?.sort || collectionConfig.defaultSort || undefined
|
: listPreferences?.sort ||
|
||||||
|
(typeof collectionConfig.defaultSort === 'string'
|
||||||
|
? collectionConfig.defaultSort
|
||||||
|
: undefined)
|
||||||
|
|
||||||
const data = await payload.find({
|
const data = await payload.find({
|
||||||
collection: collectionSlug,
|
collection: collectionSlug,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import type {
|
|||||||
TypedAuthOperations,
|
TypedAuthOperations,
|
||||||
TypedCollection,
|
TypedCollection,
|
||||||
} from '../../index.js'
|
} from '../../index.js'
|
||||||
import type { PayloadRequest, RequestContext } from '../../types/index.js'
|
import type { PayloadRequest, RequestContext, Sort } from '../../types/index.js'
|
||||||
import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js'
|
import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js'
|
||||||
import type {
|
import type {
|
||||||
IncomingCollectionVersions,
|
IncomingCollectionVersions,
|
||||||
@@ -375,7 +375,7 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
|
|||||||
/**
|
/**
|
||||||
* Default field to sort by in collection list view
|
* Default field to sort by in collection list view
|
||||||
*/
|
*/
|
||||||
defaultSort?: string
|
defaultSort?: Sort
|
||||||
/**
|
/**
|
||||||
* When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs
|
* When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AccessResult } from '../../config/types.js'
|
import type { AccessResult } from '../../config/types.js'
|
||||||
import type { PaginatedDocs } from '../../database/types.js'
|
import type { PaginatedDocs } from '../../database/types.js'
|
||||||
import type { CollectionSlug, JoinQuery } from '../../index.js'
|
import type { CollectionSlug, JoinQuery } from '../../index.js'
|
||||||
import type { PayloadRequest, Where } from '../../types/index.js'
|
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
|
||||||
import type { Collection, DataFromCollectionSlug } from '../config/types.js'
|
import type { Collection, DataFromCollectionSlug } from '../config/types.js'
|
||||||
|
|
||||||
import executeAccess from '../../auth/executeAccess.js'
|
import executeAccess from '../../auth/executeAccess.js'
|
||||||
@@ -28,7 +28,7 @@ export type Arguments = {
|
|||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
req?: PayloadRequest
|
req?: PayloadRequest
|
||||||
showHiddenFields?: boolean
|
showHiddenFields?: boolean
|
||||||
sort?: string
|
sort?: Sort
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PaginatedDocs } from '../../database/types.js'
|
import type { PaginatedDocs } from '../../database/types.js'
|
||||||
import type { PayloadRequest, Where } from '../../types/index.js'
|
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
|
||||||
import type { TypeWithVersion } from '../../versions/types.js'
|
import type { TypeWithVersion } from '../../versions/types.js'
|
||||||
import type { Collection } from '../config/types.js'
|
import type { Collection } from '../config/types.js'
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export type Arguments = {
|
|||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
req?: PayloadRequest
|
req?: PayloadRequest
|
||||||
showHiddenFields?: boolean
|
showHiddenFields?: boolean
|
||||||
sort?: string
|
sort?: Sort
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PaginatedDocs } from '../../../database/types.js'
|
import type { PaginatedDocs } from '../../../database/types.js'
|
||||||
import type { CollectionSlug, JoinQuery, Payload, TypedLocale } from '../../../index.js'
|
import type { CollectionSlug, JoinQuery, Payload, TypedLocale } from '../../../index.js'
|
||||||
import type { Document, PayloadRequest, RequestContext, Where } from '../../../types/index.js'
|
import type { Document, PayloadRequest, RequestContext, Sort, Where } from '../../../types/index.js'
|
||||||
import type { DataFromCollectionSlug } from '../../config/types.js'
|
import type { DataFromCollectionSlug } from '../../config/types.js'
|
||||||
|
|
||||||
import { APIError } from '../../../errors/index.js'
|
import { APIError } from '../../../errors/index.js'
|
||||||
@@ -27,7 +27,7 @@ export type Options<TSlug extends CollectionSlug> = {
|
|||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
req?: PayloadRequest
|
req?: PayloadRequest
|
||||||
showHiddenFields?: boolean
|
showHiddenFields?: boolean
|
||||||
sort?: string
|
sort?: Sort
|
||||||
user?: Document
|
user?: Document
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PaginatedDocs } from '../../../database/types.js'
|
import type { PaginatedDocs } from '../../../database/types.js'
|
||||||
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
|
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
|
||||||
import type { Document, PayloadRequest, RequestContext, Where } from '../../../types/index.js'
|
import type { Document, PayloadRequest, RequestContext, Sort, Where } from '../../../types/index.js'
|
||||||
import type { TypeWithVersion } from '../../../versions/types.js'
|
import type { TypeWithVersion } from '../../../versions/types.js'
|
||||||
import type { DataFromCollectionSlug } from '../../config/types.js'
|
import type { DataFromCollectionSlug } from '../../config/types.js'
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export type Options<TSlug extends CollectionSlug> = {
|
|||||||
page?: number
|
page?: number
|
||||||
req?: PayloadRequest
|
req?: PayloadRequest
|
||||||
showHiddenFields?: boolean
|
showHiddenFields?: boolean
|
||||||
sort?: string
|
sort?: Sort
|
||||||
user?: Document
|
user?: Document
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TypeWithID } from '../collections/config/types.js'
|
import type { TypeWithID } from '../collections/config/types.js'
|
||||||
import type { Document, JoinQuery, Payload, PayloadRequest, Where } from '../types/index.js'
|
import type { Document, JoinQuery, Payload, PayloadRequest, Sort, Where } from '../types/index.js'
|
||||||
import type { TypeWithVersion } from '../versions/types.js'
|
import type { TypeWithVersion } from '../versions/types.js'
|
||||||
|
|
||||||
export type { TypeWithVersion }
|
export type { TypeWithVersion }
|
||||||
@@ -180,7 +180,7 @@ export type QueryDraftsArgs = {
|
|||||||
page?: number
|
page?: number
|
||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
sort?: string
|
sort?: Sort
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ export type FindArgs = {
|
|||||||
projection?: Record<string, unknown>
|
projection?: Record<string, unknown>
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
skip?: number
|
skip?: number
|
||||||
sort?: string
|
sort?: Sort
|
||||||
versions?: boolean
|
versions?: boolean
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ type BaseVersionArgs = {
|
|||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
skip?: number
|
skip?: number
|
||||||
sort?: string
|
sort?: Sort
|
||||||
versions?: boolean
|
versions?: boolean
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PaginatedDocs } from '../../database/types.js'
|
import type { PaginatedDocs } from '../../database/types.js'
|
||||||
import type { PayloadRequest, Where } from '../../types/index.js'
|
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
|
||||||
import type { TypeWithVersion } from '../../versions/types.js'
|
import type { TypeWithVersion } from '../../versions/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../config/types.js'
|
import type { SanitizedGlobalConfig } from '../config/types.js'
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export type Arguments = {
|
|||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
req?: PayloadRequest
|
req?: PayloadRequest
|
||||||
showHiddenFields?: boolean
|
showHiddenFields?: boolean
|
||||||
sort?: string
|
sort?: Sort
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PaginatedDocs } from '../../../database/types.js'
|
import type { PaginatedDocs } from '../../../database/types.js'
|
||||||
import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
|
import type { GlobalSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
|
||||||
import type { Document, PayloadRequest, Where } from '../../../types/index.js'
|
import type { Document, PayloadRequest, Sort, Where } from '../../../types/index.js'
|
||||||
import type { TypeWithVersion } from '../../../versions/types.js'
|
import type { TypeWithVersion } from '../../../versions/types.js'
|
||||||
import type { DataFromGlobalSlug } from '../../config/types.js'
|
import type { DataFromGlobalSlug } from '../../config/types.js'
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export type Options<TSlug extends GlobalSlug> = {
|
|||||||
req?: PayloadRequest
|
req?: PayloadRequest
|
||||||
showHiddenFields?: boolean
|
showHiddenFields?: boolean
|
||||||
slug: TSlug
|
slug: TSlug
|
||||||
sort?: string
|
sort?: Sort
|
||||||
user?: Document
|
user?: Document
|
||||||
where?: Where
|
where?: Where
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ export type Where = {
|
|||||||
or?: Where[]
|
or?: Where[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Sort = Array<string> | string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies pagination for join fields for including collection relationships
|
* Applies pagination for join fields for including collection relationships
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||||
|
import type { Sort } from '../../types/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes the incoming sort argument and prefixes it with `versions.` and preserves any `-` prefixes for descending order
|
* Takes the incoming sort argument and prefixes it with `versions.` and preserves any `-` prefixes for descending order
|
||||||
@@ -9,8 +10,8 @@ export const getQueryDraftsSort = ({
|
|||||||
sort,
|
sort,
|
||||||
}: {
|
}: {
|
||||||
collectionConfig: SanitizedCollectionConfig
|
collectionConfig: SanitizedCollectionConfig
|
||||||
sort: string
|
sort?: Sort
|
||||||
}): string => {
|
}): Sort => {
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
if (collectionConfig.defaultSort) {
|
if (collectionConfig.defaultSort) {
|
||||||
sort = collectionConfig.defaultSort
|
sort = collectionConfig.defaultSort
|
||||||
@@ -19,12 +20,18 @@ export const getQueryDraftsSort = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let direction = ''
|
if (typeof sort === 'string') {
|
||||||
let orderBy = sort
|
sort = [sort]
|
||||||
|
}
|
||||||
|
|
||||||
if (sort[0] === '-') {
|
return sort.map((field: string) => {
|
||||||
|
let orderBy: string
|
||||||
|
let direction = ''
|
||||||
|
if (field[0] === '-') {
|
||||||
|
orderBy = field.substring(1)
|
||||||
direction = '-'
|
direction = '-'
|
||||||
orderBy = sort.substring(1)
|
} else {
|
||||||
|
orderBy = field
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orderBy === 'id') {
|
if (orderBy === 'id') {
|
||||||
@@ -32,4 +39,5 @@ export const getQueryDraftsSort = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `${direction}version.${orderBy}`
|
return `${direction}version.${orderBy}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
21
test/sort/collections/DefaultSort/index.ts
Normal file
21
test/sort/collections/DefaultSort/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const defaultSortSlug = 'default-sort'
|
||||||
|
|
||||||
|
export const DefaultSortCollection: CollectionConfig = {
|
||||||
|
slug: defaultSortSlug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'text',
|
||||||
|
},
|
||||||
|
defaultSort: ['number', '-text'],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
27
test/sort/collections/Drafts/index.ts
Normal file
27
test/sort/collections/Drafts/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const draftsSlug = 'drafts'
|
||||||
|
|
||||||
|
export const DraftsCollection: CollectionConfig = {
|
||||||
|
slug: draftsSlug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'text',
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
drafts: true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number2',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
41
test/sort/collections/Localized/index.ts
Normal file
41
test/sort/collections/Localized/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const localiedSlug = 'localized'
|
||||||
|
|
||||||
|
export const LocalizedCollection: CollectionConfig = {
|
||||||
|
slug: localiedSlug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'text',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
type: 'number',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number2',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'group',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
type: 'number',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
38
test/sort/collections/Posts/index.ts
Normal file
38
test/sort/collections/Posts/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const postsSlug = 'posts'
|
||||||
|
|
||||||
|
export const PostsCollection: CollectionConfig = {
|
||||||
|
slug: postsSlug,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'text',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number2',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'group',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'number',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
37
test/sort/config.ts
Normal file
37
test/sort/config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { DefaultSortCollection } from './collections/DefaultSort/index.js'
|
||||||
|
import { DraftsCollection } from './collections/Drafts/index.js'
|
||||||
|
import { LocalizedCollection } from './collections/Localized/index.js'
|
||||||
|
import { PostsCollection } from './collections/Posts/index.js'
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
collections: [PostsCollection, DraftsCollection, DefaultSortCollection, LocalizedCollection],
|
||||||
|
admin: {
|
||||||
|
importMap: {
|
||||||
|
baseDir: path.resolve(dirname),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
||||||
|
localization: {
|
||||||
|
locales: ['en', 'nb'],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
},
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
|
},
|
||||||
|
})
|
||||||
19
test/sort/eslint.config.js
Normal file
19
test/sort/eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { rootParserOptions } from '../../eslint.config.js'
|
||||||
|
import { testEslintConfig } from '../eslint.config.js'
|
||||||
|
|
||||||
|
/** @typedef {import('eslint').Linter.Config} Config */
|
||||||
|
|
||||||
|
/** @type {Config[]} */
|
||||||
|
export const index = [
|
||||||
|
...testEslintConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
...rootParserOptions,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default index
|
||||||
471
test/sort/int.spec.ts
Normal file
471
test/sort/int.spec.ts
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
import type { CollectionSlug, Payload } from 'payload'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||||
|
|
||||||
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
|
|
||||||
|
let payload: Payload
|
||||||
|
let restClient: NextRESTClient
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
describe('Sort', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const initialized = await initPayloadInt(dirname)
|
||||||
|
;({ payload, restClient } = initialized)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (typeof payload.db.destroy === 'function') {
|
||||||
|
await payload.db.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Local API', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await createData('posts', [
|
||||||
|
{ text: 'Post 1', number: 1, number2: 10, group: { number: 100 } },
|
||||||
|
{ text: 'Post 2', number: 2, number2: 10, group: { number: 200 } },
|
||||||
|
{ text: 'Post 3', number: 3, number2: 5, group: { number: 150 } },
|
||||||
|
{ text: 'Post 10', number: 10, number2: 5, group: { number: 200 } },
|
||||||
|
{ text: 'Post 11', number: 11, number2: 20, group: { number: 150 } },
|
||||||
|
{ text: 'Post 12', number: 12, number2: 20, group: { number: 100 } },
|
||||||
|
])
|
||||||
|
await createData('default-sort', [
|
||||||
|
{ text: 'Post default-5 b', number: 5 },
|
||||||
|
{ text: 'Post default-10', number: 10 },
|
||||||
|
{ text: 'Post default-5 a', number: 5 },
|
||||||
|
{ text: 'Post default-1', number: 1 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await payload.delete({ collection: 'posts', where: {} })
|
||||||
|
await payload.delete({ collection: 'default-sort', where: {} })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Default sort', () => {
|
||||||
|
it('should sort posts by default definition in collection', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'default-sort', // 'number,-text'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post default-1',
|
||||||
|
'Post default-5 b',
|
||||||
|
'Post default-5 a',
|
||||||
|
'Post default-10',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sinlge sort field', () => {
|
||||||
|
it('should sort posts by text field', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: 'text',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 1',
|
||||||
|
'Post 10',
|
||||||
|
'Post 11',
|
||||||
|
'Post 12',
|
||||||
|
'Post 2',
|
||||||
|
'Post 3',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by text field desc', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: '-text',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 3',
|
||||||
|
'Post 2',
|
||||||
|
'Post 12',
|
||||||
|
'Post 11',
|
||||||
|
'Post 10',
|
||||||
|
'Post 1',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by number field', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: 'number',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 1',
|
||||||
|
'Post 2',
|
||||||
|
'Post 3',
|
||||||
|
'Post 10',
|
||||||
|
'Post 11',
|
||||||
|
'Post 12',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by number field desc', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: '-number',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 12',
|
||||||
|
'Post 11',
|
||||||
|
'Post 10',
|
||||||
|
'Post 3',
|
||||||
|
'Post 2',
|
||||||
|
'Post 1',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sort by multiple fields', () => {
|
||||||
|
it('should sort posts by multiple fields', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: ['number2', 'number'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 3', // 5, 3
|
||||||
|
'Post 10', // 5, 10
|
||||||
|
'Post 1', // 10, 1
|
||||||
|
'Post 2', // 10, 2
|
||||||
|
'Post 11', // 20, 11
|
||||||
|
'Post 12', // 20, 12
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by multiple fields asc and desc', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: ['number2', '-number'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 10', // 5, 10
|
||||||
|
'Post 3', // 5, 3
|
||||||
|
'Post 2', // 10, 2
|
||||||
|
'Post 1', // 10, 1
|
||||||
|
'Post 12', // 20, 12
|
||||||
|
'Post 11', // 20, 11
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by multiple fields with group', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
sort: ['-group.number', '-number'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 10', // 200, 10
|
||||||
|
'Post 2', // 200, 2
|
||||||
|
'Post 11', // 150, 11
|
||||||
|
'Post 3', // 150, 3
|
||||||
|
'Post 12', // 100, 12
|
||||||
|
'Post 1', // 100, 1
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sort with drafts', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const testData1 = await payload.create({
|
||||||
|
collection: 'drafts',
|
||||||
|
data: { text: 'Post 1 draft', number: 10 },
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
await payload.update({
|
||||||
|
collection: 'drafts',
|
||||||
|
id: testData1.id,
|
||||||
|
data: { text: 'Post 1 draft updated', number: 20 },
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
await payload.update({
|
||||||
|
collection: 'drafts',
|
||||||
|
id: testData1.id,
|
||||||
|
data: { text: 'Post 1 draft updated', number: 30 },
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
await payload.update({
|
||||||
|
collection: 'drafts',
|
||||||
|
id: testData1.id,
|
||||||
|
data: { text: 'Post 1 published', number: 15 },
|
||||||
|
draft: false,
|
||||||
|
})
|
||||||
|
const testData2 = await payload.create({
|
||||||
|
collection: 'drafts',
|
||||||
|
data: { text: 'Post 2 draft', number: 1 },
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
await payload.update({
|
||||||
|
collection: 'drafts',
|
||||||
|
id: testData2.id,
|
||||||
|
data: { text: 'Post 2 published', number: 2 },
|
||||||
|
draft: false,
|
||||||
|
})
|
||||||
|
await payload.update({
|
||||||
|
collection: 'drafts',
|
||||||
|
id: testData2.id,
|
||||||
|
data: { text: 'Post 2 newdraft', number: 100 },
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
await payload.create({
|
||||||
|
collection: 'drafts',
|
||||||
|
data: { text: 'Post 3 draft', number: 3 },
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort latest without draft', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'drafts',
|
||||||
|
sort: 'number',
|
||||||
|
draft: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 2 published', // 2
|
||||||
|
'Post 3 draft', // 3
|
||||||
|
'Post 1 published', // 15
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort latest with draft', async () => {
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'drafts',
|
||||||
|
sort: 'number',
|
||||||
|
draft: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 3 draft', // 3
|
||||||
|
'Post 1 published', // 15
|
||||||
|
'Post 2 newdraft', // 100
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort versions', async () => {
|
||||||
|
const posts = await payload.findVersions({
|
||||||
|
collection: 'drafts',
|
||||||
|
sort: 'version.number',
|
||||||
|
draft: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(posts.docs.map((post) => post.version.text)).toEqual([
|
||||||
|
'Post 2 draft', // 1
|
||||||
|
'Post 2 published', // 2
|
||||||
|
'Post 3 draft', // 3
|
||||||
|
'Post 1 draft', // 10
|
||||||
|
'Post 1 published', // 15
|
||||||
|
'Post 1 draft updated', // 20
|
||||||
|
'Post 1 draft updated', // 30
|
||||||
|
'Post 2 newdraft', // 100
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Localized sort', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const testData1 = await payload.create({
|
||||||
|
collection: 'localized',
|
||||||
|
data: { text: 'Post 1 english', number: 10 },
|
||||||
|
locale: 'en',
|
||||||
|
})
|
||||||
|
await payload.update({
|
||||||
|
collection: 'localized',
|
||||||
|
id: testData1.id,
|
||||||
|
data: { text: 'Post 1 norsk', number: 20 },
|
||||||
|
locale: 'nb',
|
||||||
|
})
|
||||||
|
const testData2 = await payload.create({
|
||||||
|
collection: 'localized',
|
||||||
|
data: { text: 'Post 2 english', number: 25 },
|
||||||
|
locale: 'en',
|
||||||
|
})
|
||||||
|
await payload.update({
|
||||||
|
collection: 'localized',
|
||||||
|
id: testData2.id,
|
||||||
|
data: { text: 'Post 2 norsk', number: 5 },
|
||||||
|
locale: 'nb',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort localized field', async () => {
|
||||||
|
const englishPosts = await payload.find({
|
||||||
|
collection: 'localized',
|
||||||
|
sort: 'number',
|
||||||
|
locale: 'en',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(englishPosts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 1 english', // 10
|
||||||
|
'Post 2 english', // 20
|
||||||
|
])
|
||||||
|
|
||||||
|
const norwegianPosts = await payload.find({
|
||||||
|
collection: 'localized',
|
||||||
|
sort: 'number',
|
||||||
|
locale: 'nb',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(norwegianPosts.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 2 norsk', // 5
|
||||||
|
'Post 1 norsk', // 25
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('REST API', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await createData('posts', [
|
||||||
|
{ text: 'Post 1', number: 1, number2: 10 },
|
||||||
|
{ text: 'Post 2', number: 2, number2: 10 },
|
||||||
|
{ text: 'Post 3', number: 3, number2: 5 },
|
||||||
|
{ text: 'Post 10', number: 10, number2: 5 },
|
||||||
|
{ text: 'Post 11', number: 11, number2: 20 },
|
||||||
|
{ text: 'Post 12', number: 12, number2: 20 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await payload.delete({ collection: 'posts', where: {} })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sinlge sort field', () => {
|
||||||
|
it('should sort posts by text field', async () => {
|
||||||
|
const res = await restClient
|
||||||
|
.GET(`/posts`, {
|
||||||
|
query: {
|
||||||
|
sort: 'text',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
expect(res.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 1',
|
||||||
|
'Post 10',
|
||||||
|
'Post 11',
|
||||||
|
'Post 12',
|
||||||
|
'Post 2',
|
||||||
|
'Post 3',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by text field desc', async () => {
|
||||||
|
const res = await restClient
|
||||||
|
.GET(`/posts`, {
|
||||||
|
query: {
|
||||||
|
sort: '-text',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
expect(res.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 3',
|
||||||
|
'Post 2',
|
||||||
|
'Post 12',
|
||||||
|
'Post 11',
|
||||||
|
'Post 10',
|
||||||
|
'Post 1',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by number field', async () => {
|
||||||
|
const res = await restClient
|
||||||
|
.GET(`/posts`, {
|
||||||
|
query: {
|
||||||
|
sort: 'number',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
expect(res.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 1',
|
||||||
|
'Post 2',
|
||||||
|
'Post 3',
|
||||||
|
'Post 10',
|
||||||
|
'Post 11',
|
||||||
|
'Post 12',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by number field desc', async () => {
|
||||||
|
const res = await restClient
|
||||||
|
.GET(`/posts`, {
|
||||||
|
query: {
|
||||||
|
sort: '-number',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
expect(res.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 12',
|
||||||
|
'Post 11',
|
||||||
|
'Post 10',
|
||||||
|
'Post 3',
|
||||||
|
'Post 2',
|
||||||
|
'Post 1',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Sort by multiple fields', () => {
|
||||||
|
it('should sort posts by multiple fields', async () => {
|
||||||
|
const res = await restClient
|
||||||
|
.GET(`/posts`, {
|
||||||
|
query: {
|
||||||
|
sort: 'number2,number',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
expect(res.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 3', // 5, 3
|
||||||
|
'Post 10', // 5, 10
|
||||||
|
'Post 1', // 10, 1
|
||||||
|
'Post 2', // 10, 2
|
||||||
|
'Post 11', // 20, 11
|
||||||
|
'Post 12', // 20, 12
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should sort posts by multiple fields asc and desc', async () => {
|
||||||
|
const res = await restClient
|
||||||
|
.GET(`/posts`, {
|
||||||
|
query: {
|
||||||
|
sort: 'number2,-number',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
expect(res.docs.map((post) => post.text)).toEqual([
|
||||||
|
'Post 10', // 5, 10
|
||||||
|
'Post 3', // 5, 3
|
||||||
|
'Post 2', // 10, 2
|
||||||
|
'Post 1', // 10, 1
|
||||||
|
'Post 12', // 20, 12
|
||||||
|
'Post 11', // 20, 11
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createData(collection: CollectionSlug, data: Record<string, any>[]) {
|
||||||
|
for (const item of data) {
|
||||||
|
await payload.create({ collection, data: item })
|
||||||
|
}
|
||||||
|
}
|
||||||
204
test/sort/payload-types.ts
Normal file
204
test/sort/payload-types.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
auth: {
|
||||||
|
users: UserAuthOperations;
|
||||||
|
};
|
||||||
|
collections: {
|
||||||
|
posts: Post;
|
||||||
|
drafts: Draft;
|
||||||
|
'default-sort': DefaultSort;
|
||||||
|
localized: Localized;
|
||||||
|
users: User;
|
||||||
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
'payload-preferences': PayloadPreference;
|
||||||
|
'payload-migrations': PayloadMigration;
|
||||||
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: string;
|
||||||
|
};
|
||||||
|
globals: {};
|
||||||
|
locale: 'en' | 'nb';
|
||||||
|
user: User & {
|
||||||
|
collection: 'users';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface UserAuthOperations {
|
||||||
|
forgotPassword: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
login: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
registerFirstUser: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
unlock: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "posts".
|
||||||
|
*/
|
||||||
|
export interface Post {
|
||||||
|
id: string;
|
||||||
|
text?: string | null;
|
||||||
|
number?: number | null;
|
||||||
|
number2?: number | null;
|
||||||
|
group?: {
|
||||||
|
text?: string | null;
|
||||||
|
number?: number | null;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "drafts".
|
||||||
|
*/
|
||||||
|
export interface Draft {
|
||||||
|
id: string;
|
||||||
|
text?: string | null;
|
||||||
|
number?: number | null;
|
||||||
|
number2?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "default-sort".
|
||||||
|
*/
|
||||||
|
export interface DefaultSort {
|
||||||
|
id: string;
|
||||||
|
text?: string | null;
|
||||||
|
number?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "localized".
|
||||||
|
*/
|
||||||
|
export interface Localized {
|
||||||
|
id: string;
|
||||||
|
text?: string | null;
|
||||||
|
number?: number | null;
|
||||||
|
number2?: number | null;
|
||||||
|
group?: {
|
||||||
|
text?: string | null;
|
||||||
|
number?: number | null;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users".
|
||||||
|
*/
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
resetPasswordToken?: string | null;
|
||||||
|
resetPasswordExpiration?: string | null;
|
||||||
|
salt?: string | null;
|
||||||
|
hash?: string | null;
|
||||||
|
loginAttempts?: number | null;
|
||||||
|
lockUntil?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocument {
|
||||||
|
id: string;
|
||||||
|
document?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'posts';
|
||||||
|
value: string | Post;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'drafts';
|
||||||
|
value: string | Draft;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'default-sort';
|
||||||
|
value: string | DefaultSort;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'localized';
|
||||||
|
value: string | Localized;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
} | null);
|
||||||
|
globalSlug?: string | null;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreference {
|
||||||
|
id: string;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
};
|
||||||
|
key?: string | null;
|
||||||
|
value?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigration {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
batch?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "auth".
|
||||||
|
*/
|
||||||
|
export interface Auth {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare module 'payload' {
|
||||||
|
// @ts-ignore
|
||||||
|
export interface GeneratedTypes extends Config {}
|
||||||
|
}
|
||||||
13
test/sort/tsconfig.eslint.json
Normal file
13
test/sort/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
test/sort/tsconfig.json
Normal file
3
test/sort/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user