Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sort by multiple columns #8799

Merged
merged 6 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions docs/configuration/collections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,26 @@ export const Posts: CollectionConfig = {

The following options are available:

| Option | Description |
|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`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). |
| **`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) |
| **`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. |
| **`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). |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| Option | Description |
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`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). |
| **`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) |
| **`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. 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. |
| **`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). |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |

_\* An asterisk denotes that a property is required._

Expand Down
23 changes: 22 additions & 1 deletion docs/queries/sort.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

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.

Expand All @@ -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

To sort in the [REST API](../rest-api/overview), you can use the `sort` parameter in your query:
Expand All @@ -40,6 +53,14 @@ fetch('https://localhost:3000/api/posts?sort=-createdAt') // highlight-line
.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

To sort in the [GraphQL API](../graphql/overview), you can use the `sort` parameter in your query:
Expand Down
45 changes: 27 additions & 18 deletions packages/db-mongodb/src/queries/buildSortParam.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { PaginateOptions } from 'mongoose'
import type { Field, SanitizedConfig } from 'payload'
import type { Field, SanitizedConfig, Sort } from 'payload'

import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'

type Args = {
config: SanitizedConfig
fields: Field[]
locale: string
sort: string
sort: Sort
timestamps: boolean
}

Expand All @@ -25,32 +25,41 @@ export const buildSortParam = ({
sort,
timestamps,
}: Args): PaginateOptions['sort'] => {
let sortProperty: string
let sortDirection: SortDirection = 'desc'

if (!sort) {
if (timestamps) {
sortProperty = 'createdAt'
sort = '-createdAt'
} else {
sortProperty = '_id'
sort = '-id'
DanRibbens marked this conversation as resolved.
Show resolved Hide resolved
}
} else if (sort.indexOf('-') === 0) {
sortProperty = sort.substring(1)
} else {
sortProperty = sort
sortDirection = 'asc'
}

if (sortProperty === 'id') {
sortProperty = '_id'
} else {
sortProperty = getLocalizedSortProperty({
if (typeof sort === 'string') {
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 {
sortProperty = item
sortDirection = 'asc'
}
if (sortProperty === 'id') {
acc['_id'] = sortDirection
return acc
}
const localizedProperty = getLocalizedSortProperty({
config,
fields,
locale,
segments: sortProperty.split('.'),
})
}
acc[localizedProperty] = sortDirection
return acc
}, {})

return { [sortProperty]: sortDirection }
return sorting
}
2 changes: 1 addition & 1 deletion packages/drizzle/src/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const find: Find = async function find(
},
) {
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))

Expand Down
6 changes: 3 additions & 3 deletions packages/drizzle/src/find/findMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export const findMany = async function find({

const selectDistinctMethods: ChainedMethods = []

if (orderBy?.order && orderBy?.column) {
if (orderBy) {
selectDistinctMethods.push({
args: [orderBy.order(orderBy.column)],
args: [() => orderBy.map(({ column, order }) => order(column))],
method: 'orderBy',
})
}
Expand Down Expand Up @@ -114,7 +114,7 @@ export const findMany = async function find({
} else {
findManyArgs.limit = limit
findManyArgs.offset = offset
findManyArgs.orderBy = orderBy.order(orderBy.column)
findManyArgs.orderBy = () => orderBy.map(({ column, order }) => order(column))

if (where) {
findManyArgs.where = where
Expand Down
2 changes: 1 addition & 1 deletion packages/drizzle/src/find/traverseFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export const traverseFields = ({
})
.from(adapter.tables[joinCollectionTableName])
.where(subQueryWhere)
.orderBy(orderBy.order(orderBy.column)),
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
})

const columnName = `${path.replaceAll('.', '_')}${field.name}`
Expand Down
2 changes: 1 addition & 1 deletion packages/drizzle/src/findGlobalVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
({ slug }) => slug === global,
)
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const sort = sortArg !== undefined && sortArg !== null ? sortArg : '-createdAt'

const tableName = this.tableNameMap.get(
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
Expand Down
2 changes: 1 addition & 1 deletion packages/drizzle/src/findVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const findVersions: FindVersions = async function findVersions(
},
) {
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)}${this.versionsSuffix}`,
Expand Down
65 changes: 33 additions & 32 deletions packages/drizzle/src/queries/buildOrderBy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Field } from 'payload'
import type { Field, Sort } from 'payload'

import { asc, desc } from 'drizzle-orm'

Expand All @@ -13,7 +13,7 @@ type Args = {
joins: BuildQueryJoinAliases
locale?: string
selectFields: Record<string, GenericColumn>
sort?: string
sort?: Sort
tableName: string
}

Expand All @@ -29,54 +29,55 @@ export const buildOrderBy = ({
sort,
tableName,
}: Args): BuildQueryResult['orderBy'] => {
const orderBy: BuildQueryResult['orderBy'] = {
column: null,
order: null,
const orderBy: BuildQueryResult['orderBy'] = []

if (!sort) {
const createdAt = adapter.tables[tableName]?.createdAt
if (createdAt) {
sort = '-createdAt'
} else {
sort = '-id'
}
}

if (sort) {
let sortPath
if (typeof sort === 'string') {
sort = [sort]
}

if (sort[0] === '-') {
sortPath = sort.substring(1)
orderBy.order = desc
for (const sortItem of sort) {
let sortProperty: string
let sortDirection: 'asc' | 'desc'
if (sortItem[0] === '-') {
sortProperty = sortItem.substring(1)
sortDirection = 'desc'
} else {
sortPath = sort
orderBy.order = asc
sortProperty = sortItem
sortDirection = 'asc'
}

try {
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
adapter,
collectionPath: sortPath,
collectionPath: sortProperty,
fields,
joins,
locale,
pathSegments: sortPath.replace(/__/g, '.').split('.'),
pathSegments: sortProperty.replace(/__/g, '.').split('.'),
selectFields,
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) {
// 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
}
6 changes: 3 additions & 3 deletions packages/drizzle/src/queries/buildQuery.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { asc, desc, SQL } from 'drizzle-orm'
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'

Expand All @@ -18,7 +18,7 @@ type BuildQueryArgs = {
fields: Field[]
joins?: BuildQueryJoinAliases
locale?: string
sort?: string
sort?: Sort
tableName: string
where: Where
}
Expand All @@ -28,7 +28,7 @@ export type BuildQueryResult = {
orderBy: {
column: GenericColumn
order: typeof asc | typeof desc
}
}[]
selectFields: Record<string, GenericColumn>
where: SQL
}
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/routes/rest/collections/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => {
limit: isNumber(limit) ? Number(limit) : undefined,
page: isNumber(page) ? Number(page) : undefined,
req,
sort,
sort: typeof sort === 'string' ? sort.split(',') : undefined,
where,
})

Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/routes/rest/collections/findVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const findVersions: CollectionRouteHandler = async ({ collection, req })
limit: isNumber(limit) ? Number(limit) : undefined,
page: isNumber(page) ? Number(page) : undefined,
req,
sort,
sort: typeof sort === 'string' ? sort.split(',') : undefined,
where,
})

Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/routes/rest/globals/findVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) =>
limit: isNumber(limit) ? Number(limit) : undefined,
page: isNumber(page) ? Number(page) : undefined,
req,
sort,
sort: typeof sort === 'string' ? sort.split(',') : undefined,
where,
})

Expand Down
Loading