From 4d44c378ed61e552d5c2773e060b80cdab6f2c53 Mon Sep 17 00:00:00 2001 From: Anders Semb Hermansen Date: Thu, 24 Oct 2024 21:46:30 +0200 Subject: [PATCH] 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 --- docs/configuration/collections.mdx | 40 +- docs/queries/sort.mdx | 23 +- .../db-mongodb/src/queries/buildSortParam.ts | 45 +- packages/drizzle/src/find.ts | 2 +- packages/drizzle/src/find/findMany.ts | 6 +- packages/drizzle/src/find/traverseFields.ts | 2 +- packages/drizzle/src/findGlobalVersions.ts | 2 +- packages/drizzle/src/findVersions.ts | 2 +- packages/drizzle/src/queries/buildOrderBy.ts | 65 +-- packages/drizzle/src/queries/buildQuery.ts | 6 +- .../next/src/routes/rest/collections/find.ts | 2 +- .../routes/rest/collections/findVersions.ts | 2 +- .../src/routes/rest/globals/findVersions.ts | 2 +- packages/next/src/views/List/index.tsx | 5 +- .../payload/src/collections/config/types.ts | 4 +- .../src/collections/operations/find.ts | 4 +- .../collections/operations/findVersions.ts | 4 +- .../src/collections/operations/local/find.ts | 4 +- .../operations/local/findVersions.ts | 4 +- packages/payload/src/database/types.ts | 8 +- .../src/globals/operations/findVersions.ts | 4 +- .../globals/operations/local/findVersions.ts | 4 +- packages/payload/src/types/index.ts | 2 + .../src/versions/drafts/getQueryDraftsSort.ts | 32 +- test/sort/collections/DefaultSort/index.ts | 21 + test/sort/collections/Drafts/index.ts | 27 + test/sort/collections/Localized/index.ts | 41 ++ test/sort/collections/Posts/index.ts | 38 ++ test/sort/config.ts | 37 ++ test/sort/eslint.config.js | 19 + test/sort/int.spec.ts | 471 ++++++++++++++++++ test/sort/payload-types.ts | 204 ++++++++ test/sort/tsconfig.eslint.json | 13 + test/sort/tsconfig.json | 3 + 34 files changed, 1033 insertions(+), 115 deletions(-) create mode 100644 test/sort/collections/DefaultSort/index.ts create mode 100644 test/sort/collections/Drafts/index.ts create mode 100644 test/sort/collections/Localized/index.ts create mode 100644 test/sort/collections/Posts/index.ts create mode 100644 test/sort/config.ts create mode 100644 test/sort/eslint.config.js create mode 100644 test/sort/int.spec.ts create mode 100644 test/sort/payload-types.ts create mode 100644 test/sort/tsconfig.eslint.json create mode 100644 test/sort/tsconfig.json diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index 9c96785b3f..f20ea6df8d 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -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._ diff --git a/docs/queries/sort.mdx b/docs/queries/sort.mdx index 3340d21f98..60b46c3cd2 100644 --- a/docs/queries/sort.mdx +++ b/docs/queries/sort.mdx @@ -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. @@ -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: @@ -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: diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts index b539fe4812..de4d4860fe 100644 --- a/packages/db-mongodb/src/queries/buildSortParam.ts +++ b/packages/db-mongodb/src/queries/buildSortParam.ts @@ -1,5 +1,5 @@ import type { PaginateOptions } from 'mongoose' -import type { Field, SanitizedConfig } from 'payload' +import type { Field, SanitizedConfig, Sort } from 'payload' import { getLocalizedSortProperty } from './getLocalizedSortProperty.js' @@ -7,7 +7,7 @@ type Args = { config: SanitizedConfig fields: Field[] locale: string - sort: string + sort: Sort timestamps: boolean } @@ -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' } - } 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((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 } diff --git a/packages/drizzle/src/find.ts b/packages/drizzle/src/find.ts index 549ed6e13c..9ec5bcf692 100644 --- a/packages/drizzle/src/find.ts +++ b/packages/drizzle/src/find.ts @@ -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)) diff --git a/packages/drizzle/src/find/findMany.ts b/packages/drizzle/src/find/findMany.ts index a3a8a42ba7..442af7fd77 100644 --- a/packages/drizzle/src/find/findMany.ts +++ b/packages/drizzle/src/find/findMany.ts @@ -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', }) } @@ -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 diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts index 35f7a78750..8f64409577 100644 --- a/packages/drizzle/src/find/traverseFields.ts +++ b/packages/drizzle/src/find/traverseFields.ts @@ -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}` diff --git a/packages/drizzle/src/findGlobalVersions.ts b/packages/drizzle/src/findGlobalVersions.ts index f94095c40b..bb55ae3604 100644 --- a/packages/drizzle/src/findGlobalVersions.ts +++ b/packages/drizzle/src/findGlobalVersions.ts @@ -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}`, diff --git a/packages/drizzle/src/findVersions.ts b/packages/drizzle/src/findVersions.ts index eac2df63b7..ae0e280370 100644 --- a/packages/drizzle/src/findVersions.ts +++ b/packages/drizzle/src/findVersions.ts @@ -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}`, diff --git a/packages/drizzle/src/queries/buildOrderBy.ts b/packages/drizzle/src/queries/buildOrderBy.ts index e90c229a0d..30466735bf 100644 --- a/packages/drizzle/src/queries/buildOrderBy.ts +++ b/packages/drizzle/src/queries/buildOrderBy.ts @@ -1,4 +1,4 @@ -import type { Field } from 'payload' +import type { Field, Sort } from 'payload' import { asc, desc } from 'drizzle-orm' @@ -13,7 +13,7 @@ type Args = { joins: BuildQueryJoinAliases locale?: string selectFields: Record - sort?: string + sort?: Sort tableName: string } @@ -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 } diff --git a/packages/drizzle/src/queries/buildQuery.ts b/packages/drizzle/src/queries/buildQuery.ts index 9873b399a3..554142796b 100644 --- a/packages/drizzle/src/queries/buildQuery.ts +++ b/packages/drizzle/src/queries/buildQuery.ts @@ -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' @@ -18,7 +18,7 @@ type BuildQueryArgs = { fields: Field[] joins?: BuildQueryJoinAliases locale?: string - sort?: string + sort?: Sort tableName: string where: Where } @@ -28,7 +28,7 @@ export type BuildQueryResult = { orderBy: { column: GenericColumn order: typeof asc | typeof desc - } + }[] selectFields: Record where: SQL } diff --git a/packages/next/src/routes/rest/collections/find.ts b/packages/next/src/routes/rest/collections/find.ts index 4634629bff..ffa3eb19d9 100644 --- a/packages/next/src/routes/rest/collections/find.ts +++ b/packages/next/src/routes/rest/collections/find.ts @@ -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, }) diff --git a/packages/next/src/routes/rest/collections/findVersions.ts b/packages/next/src/routes/rest/collections/findVersions.ts index b58a380455..25cf18d241 100644 --- a/packages/next/src/routes/rest/collections/findVersions.ts +++ b/packages/next/src/routes/rest/collections/findVersions.ts @@ -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, }) diff --git a/packages/next/src/routes/rest/globals/findVersions.ts b/packages/next/src/routes/rest/globals/findVersions.ts index aa5260c3bd..d868cd44f3 100644 --- a/packages/next/src/routes/rest/globals/findVersions.ts +++ b/packages/next/src/routes/rest/globals/findVersions.ts @@ -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, }) diff --git a/packages/next/src/views/List/index.tsx b/packages/next/src/views/List/index.tsx index 7ff1bbbefb..940a6278f3 100644 --- a/packages/next/src/views/List/index.tsx +++ b/packages/next/src/views/List/index.tsx @@ -104,7 +104,10 @@ export const ListView: React.FC = async ({ const sort = query?.sort && typeof query.sort === 'string' ? query.sort - : listPreferences?.sort || collectionConfig.defaultSort || undefined + : listPreferences?.sort || + (typeof collectionConfig.defaultSort === 'string' + ? collectionConfig.defaultSort + : undefined) const data = await payload.find({ collection: collectionSlug, diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 5fe12a4f32..b0e1723d5b 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -38,7 +38,7 @@ import type { TypedAuthOperations, TypedCollection, } 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 { IncomingCollectionVersions, @@ -375,7 +375,7 @@ export type CollectionConfig = { /** * 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 */ diff --git a/packages/payload/src/collections/operations/find.ts b/packages/payload/src/collections/operations/find.ts index cb90b774e8..89ca48035a 100644 --- a/packages/payload/src/collections/operations/find.ts +++ b/packages/payload/src/collections/operations/find.ts @@ -1,7 +1,7 @@ import type { AccessResult } from '../../config/types.js' import type { PaginatedDocs } from '../../database/types.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 executeAccess from '../../auth/executeAccess.js' @@ -28,7 +28,7 @@ export type Arguments = { pagination?: boolean req?: PayloadRequest showHiddenFields?: boolean - sort?: string + sort?: Sort where?: Where } diff --git a/packages/payload/src/collections/operations/findVersions.ts b/packages/payload/src/collections/operations/findVersions.ts index 3426a32f1f..e0ee8d0b56 100644 --- a/packages/payload/src/collections/operations/findVersions.ts +++ b/packages/payload/src/collections/operations/findVersions.ts @@ -1,5 +1,5 @@ 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 { Collection } from '../config/types.js' @@ -20,7 +20,7 @@ export type Arguments = { pagination?: boolean req?: PayloadRequest showHiddenFields?: boolean - sort?: string + sort?: Sort where?: Where } diff --git a/packages/payload/src/collections/operations/local/find.ts b/packages/payload/src/collections/operations/local/find.ts index 09051ca6dc..8f5d545f8e 100644 --- a/packages/payload/src/collections/operations/local/find.ts +++ b/packages/payload/src/collections/operations/local/find.ts @@ -1,6 +1,6 @@ import type { PaginatedDocs } from '../../../database/types.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 { APIError } from '../../../errors/index.js' @@ -27,7 +27,7 @@ export type Options = { pagination?: boolean req?: PayloadRequest showHiddenFields?: boolean - sort?: string + sort?: Sort user?: Document where?: Where } diff --git a/packages/payload/src/collections/operations/local/findVersions.ts b/packages/payload/src/collections/operations/local/findVersions.ts index 9cb1caa86d..f5cbbe5a96 100644 --- a/packages/payload/src/collections/operations/local/findVersions.ts +++ b/packages/payload/src/collections/operations/local/findVersions.ts @@ -1,6 +1,6 @@ import type { PaginatedDocs } from '../../../database/types.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 { DataFromCollectionSlug } from '../../config/types.js' @@ -23,7 +23,7 @@ export type Options = { page?: number req?: PayloadRequest showHiddenFields?: boolean - sort?: string + sort?: Sort user?: Document where?: Where } diff --git a/packages/payload/src/database/types.ts b/packages/payload/src/database/types.ts index 03b0967ddb..d56c3dfd66 100644 --- a/packages/payload/src/database/types.ts +++ b/packages/payload/src/database/types.ts @@ -1,5 +1,5 @@ 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' export type { TypeWithVersion } @@ -180,7 +180,7 @@ export type QueryDraftsArgs = { page?: number pagination?: boolean req: PayloadRequest - sort?: string + sort?: Sort where?: Where } @@ -207,7 +207,7 @@ export type FindArgs = { projection?: Record req: PayloadRequest skip?: number - sort?: string + sort?: Sort versions?: boolean where?: Where } @@ -230,7 +230,7 @@ type BaseVersionArgs = { pagination?: boolean req: PayloadRequest skip?: number - sort?: string + sort?: Sort versions?: boolean where?: Where } diff --git a/packages/payload/src/globals/operations/findVersions.ts b/packages/payload/src/globals/operations/findVersions.ts index 19d5bd6f46..33383901a9 100644 --- a/packages/payload/src/globals/operations/findVersions.ts +++ b/packages/payload/src/globals/operations/findVersions.ts @@ -1,5 +1,5 @@ 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 { SanitizedGlobalConfig } from '../config/types.js' @@ -20,7 +20,7 @@ export type Arguments = { pagination?: boolean req?: PayloadRequest showHiddenFields?: boolean - sort?: string + sort?: Sort where?: Where } diff --git a/packages/payload/src/globals/operations/local/findVersions.ts b/packages/payload/src/globals/operations/local/findVersions.ts index 80ab12dc4d..67a4e1a1e4 100644 --- a/packages/payload/src/globals/operations/local/findVersions.ts +++ b/packages/payload/src/globals/operations/local/findVersions.ts @@ -1,6 +1,6 @@ import type { PaginatedDocs } from '../../../database/types.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 { DataFromGlobalSlug } from '../../config/types.js' @@ -19,7 +19,7 @@ export type Options = { req?: PayloadRequest showHiddenFields?: boolean slug: TSlug - sort?: string + sort?: Sort user?: Document where?: Where } diff --git a/packages/payload/src/types/index.ts b/packages/payload/src/types/index.ts index 9c673be6a4..7890e9e944 100644 --- a/packages/payload/src/types/index.ts +++ b/packages/payload/src/types/index.ts @@ -110,6 +110,8 @@ export type Where = { or?: Where[] } +export type Sort = Array | string + /** * Applies pagination for join fields for including collection relationships */ diff --git a/packages/payload/src/versions/drafts/getQueryDraftsSort.ts b/packages/payload/src/versions/drafts/getQueryDraftsSort.ts index e4ccd3b6f4..8ef9d09e22 100644 --- a/packages/payload/src/versions/drafts/getQueryDraftsSort.ts +++ b/packages/payload/src/versions/drafts/getQueryDraftsSort.ts @@ -1,4 +1,5 @@ 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 @@ -9,8 +10,8 @@ export const getQueryDraftsSort = ({ sort, }: { collectionConfig: SanitizedCollectionConfig - sort: string -}): string => { + sort?: Sort +}): Sort => { if (!sort) { if (collectionConfig.defaultSort) { sort = collectionConfig.defaultSort @@ -19,17 +20,24 @@ export const getQueryDraftsSort = ({ } } - let direction = '' - let orderBy = sort - - if (sort[0] === '-') { - direction = '-' - orderBy = sort.substring(1) + if (typeof sort === 'string') { + sort = [sort] } - if (orderBy === 'id') { - return `${direction}parent` - } + return sort.map((field: string) => { + let orderBy: string + let direction = '' + if (field[0] === '-') { + orderBy = field.substring(1) + direction = '-' + } else { + orderBy = field + } - return `${direction}version.${orderBy}` + if (orderBy === 'id') { + return `${direction}parent` + } + + return `${direction}version.${orderBy}` + }) } diff --git a/test/sort/collections/DefaultSort/index.ts b/test/sort/collections/DefaultSort/index.ts new file mode 100644 index 0000000000..5254f4b8b6 --- /dev/null +++ b/test/sort/collections/DefaultSort/index.ts @@ -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', + }, + ], +} diff --git a/test/sort/collections/Drafts/index.ts b/test/sort/collections/Drafts/index.ts new file mode 100644 index 0000000000..a6d73ae2d9 --- /dev/null +++ b/test/sort/collections/Drafts/index.ts @@ -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', + }, + ], +} diff --git a/test/sort/collections/Localized/index.ts b/test/sort/collections/Localized/index.ts new file mode 100644 index 0000000000..0f87a5653b --- /dev/null +++ b/test/sort/collections/Localized/index.ts @@ -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, + }, + ], + }, + ], +} diff --git a/test/sort/collections/Posts/index.ts b/test/sort/collections/Posts/index.ts new file mode 100644 index 0000000000..b37832f757 --- /dev/null +++ b/test/sort/collections/Posts/index.ts @@ -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', + }, + ], + }, + ], +} diff --git a/test/sort/config.ts b/test/sort/config.ts new file mode 100644 index 0000000000..78868504bc --- /dev/null +++ b/test/sort/config.ts @@ -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'), + }, +}) diff --git a/test/sort/eslint.config.js b/test/sort/eslint.config.js new file mode 100644 index 0000000000..f295df083f --- /dev/null +++ b/test/sort/eslint.config.js @@ -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 diff --git a/test/sort/int.spec.ts b/test/sort/int.spec.ts new file mode 100644 index 0000000000..730f7b4895 --- /dev/null +++ b/test/sort/int.spec.ts @@ -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[]) { + for (const item of data) { + await payload.create({ collection, data: item }) + } +} diff --git a/test/sort/payload-types.ts b/test/sort/payload-types.ts new file mode 100644 index 0000000000..715c5e6c72 --- /dev/null +++ b/test/sort/payload-types.ts @@ -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 {} +} \ No newline at end of file diff --git a/test/sort/tsconfig.eslint.json b/test/sort/tsconfig.eslint.json new file mode 100644 index 0000000000..b34cc7afbb --- /dev/null +++ b/test/sort/tsconfig.eslint.json @@ -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" + ] +} diff --git a/test/sort/tsconfig.json b/test/sort/tsconfig.json new file mode 100644 index 0000000000..3c43903cfd --- /dev/null +++ b/test/sort/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +}