feat(db-*): support sort in db.updateMany (#11768)

This adds support for `sort` in `payload.db.updateMany`.

## Example

```ts
const updatedDocs = await payload.db.updateMany({
  collection: 'posts',
  data: {
    title: 'updated',
  },
  limit: 5,
  sort: '-numberField', // <= new
  where: {
    id: {
      exists: true,
    },
  },
})
```
This commit is contained in:
Alessio Gravili
2025-03-19 10:47:58 -06:00
committed by GitHub
parent 68f2582b9a
commit e96d3c87e2
7 changed files with 225 additions and 35 deletions

View File

@@ -1,9 +1,11 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { UpdateMany } from 'payload'
import { flattenWhereToOperators, type UpdateMany } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
@@ -21,11 +23,30 @@ export const updateMany: UpdateMany = async function updateMany(
req,
returning,
select,
sort: sortArg,
where,
},
) {
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const { collectionConfig, Model } = getCollection({ adapter: this, collectionSlug })
let sort: Record<string, unknown> | undefined
if (!hasNearConstraint) {
sort = buildSortParam({
config: this.payload.config,
fields: collectionConfig.flattenedFields,
locale,
sort: sortArg || collectionConfig.defaultSort,
timestamps: true,
})
}
const options: MongooseUpdateQueryOptions = {
...optionsArgs,
lean: true,
@@ -53,7 +74,7 @@ export const updateMany: UpdateMany = async function updateMany(
const documentsToUpdate = await Model.find(
query,
{},
{ ...options, limit, projection: { _id: 1 } },
{ ...options, limit, projection: { _id: 1 }, sort },
)
if (documentsToUpdate.length === 0) {
return null
@@ -71,7 +92,14 @@ export const updateMany: UpdateMany = async function updateMany(
return null
}
const result = await Model.find(query, {}, options)
const result = await Model.find(
query,
{},
{
...options,
sort,
},
)
transform({
adapter: this,

View File

@@ -3,8 +3,9 @@ import type { UpdateMany } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import type { ChainedMethods, DrizzleAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
@@ -21,6 +22,7 @@ export const updateMany: UpdateMany = async function updateMany(
req,
returning,
select,
sort: sortArg,
where: whereToUse,
},
) {
@@ -28,10 +30,13 @@ export const updateMany: UpdateMany = async function updateMany(
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const { joins, selectFields, where } = buildQuery({
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collection.defaultSort
const { joins, orderBy, selectFields, where } = buildQuery({
adapter: this,
fields: collection.flattenedFields,
locale,
sort,
tableName,
where: whereToUse,
})
@@ -40,6 +45,14 @@ export const updateMany: UpdateMany = async function updateMany(
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: orderBy
? [
{
args: [() => orderBy.map(({ column, order }) => order(column))],
method: 'orderBy',
},
]
: [],
db,
joins,
selectFields,
@@ -49,28 +62,35 @@ export const updateMany: UpdateMany = async function updateMany(
if (selectDistinctResult?.[0]?.id) {
idsToUpdate = selectDistinctResult?.map((doc) => doc.id)
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
} else if (whereToUse && !joins.length) {
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
const _db = db as LibSQLDatabase
const table = this.tables[tableName]
const docsToUpdate =
typeof limit === 'number' && limit > 0
? await _db
.select({
id: table.id,
const query = _db.select({ id: table.id }).from(table).where(where)
const chainedMethods: ChainedMethods = []
if (typeof limit === 'number' && limit > 0) {
chainedMethods.push({
args: [limit],
method: 'limit',
})
.from(table)
.where(where)
.limit(limit)
: await _db
.select({
id: table.id,
}
if (orderBy) {
chainedMethods.push({
args: [() => orderBy.map(({ column, order }) => order(column))],
method: 'orderBy',
})
}
const docsToUpdate = await chainMethods({
methods: chainedMethods,
query,
})
.from(table)
.where(where)
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
}

View File

@@ -532,6 +532,7 @@ export type UpdateManyArgs = {
*/
returning?: boolean
select?: SelectType
sort?: Sort
where: Where
}

View File

@@ -82,7 +82,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
defaultIDType: number;
};
globals: {};
globalsSelect: {};
@@ -118,7 +118,7 @@ export interface UserAuthOperations {
* via the `definition` "posts".
*/
export interface Post {
id: string;
id: number;
title?: string | null;
description?: string | null;
defaultValueField?: string | null;
@@ -155,7 +155,7 @@ export interface Post {
* via the `definition` "users".
*/
export interface User {
id: string;
id: number;
updatedAt: string;
createdAt: string;
email: string;
@@ -172,20 +172,20 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
id: number;
document?:
| ({
relationTo: 'posts';
value: string | Post;
value: number | Post;
} | null)
| ({
relationTo: 'users';
value: string | User;
value: number | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
updatedAt: string;
createdAt: string;
@@ -195,10 +195,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
id: number;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
key?: string | null;
value?:
@@ -218,7 +218,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;

View File

@@ -44,6 +44,10 @@ export default buildConfigWithDefaults({
type: 'text',
required: true,
},
{
name: 'number',
type: 'number',
},
{
type: 'tabs',
tabs: [

View File

@@ -317,7 +317,7 @@ describe('database', () => {
payload.config.db.allowIDOnCreate = false
})
it('Local API - accepts ID on create', async () => {
it('local API - accepts ID on create', async () => {
let id: any = null
if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString()
@@ -332,7 +332,7 @@ describe('database', () => {
expect(post.id).toBe(id)
})
it('REST API - accepts ID on create', async () => {
it('rEST API - accepts ID on create', async () => {
let id: any = null
if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString()
@@ -354,7 +354,7 @@ describe('database', () => {
expect(post.doc.id).toBe(id)
})
it('GraphQL - accepts ID on create', async () => {
it('graphQL - accepts ID on create', async () => {
let id: any = null
if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString()
@@ -1168,6 +1168,140 @@ describe('database', () => {
expect(notUpdatedDocs?.[5]?.title).toBe('not updated')
})
it('ensure updateMany respects limit and sort', async () => {
await payload.db.deleteMany({
collection: postsSlug,
where: {
id: {
exists: true,
},
},
})
const numbers = Array.from({ length: 11 }, (_, i) => i)
// shuffle the numbers
numbers.sort(() => Math.random() - 0.5)
// create 11 documents numbered 0-10, but in random order
for (const i of numbers) {
await payload.create({
collection: postsSlug,
data: {
title: 'not updated',
number: i,
},
})
}
const result = await payload.db.updateMany({
collection: postsSlug,
data: {
title: 'updated',
},
limit: 5,
sort: 'number',
where: {
id: {
exists: true,
},
},
})
expect(result?.length).toBe(5)
for (let i = 0; i < 5; i++) {
expect(result?.[i]?.number).toBe(i)
expect(result?.[i]?.title).toBe('updated')
}
// Ensure all posts minus the one we don't want updated are updated
const { docs } = await payload.find({
collection: postsSlug,
depth: 0,
pagination: false,
sort: 'number',
where: {
title: {
equals: 'updated',
},
},
})
expect(docs).toHaveLength(5)
for (let i = 0; i < 5; i++) {
expect(docs?.[i]?.number).toBe(i)
expect(docs?.[i]?.title).toBe('updated')
}
})
it('ensure updateMany respects limit and negative sort', async () => {
await payload.db.deleteMany({
collection: postsSlug,
where: {
id: {
exists: true,
},
},
})
const numbers = Array.from({ length: 11 }, (_, i) => i)
// shuffle the numbers
numbers.sort(() => Math.random() - 0.5)
// create 11 documents numbered 0-10, but in random order
for (const i of numbers) {
await payload.create({
collection: postsSlug,
data: {
title: 'not updated',
number: i,
},
})
}
const result = await payload.db.updateMany({
collection: postsSlug,
data: {
title: 'updated',
},
limit: 5,
sort: '-number',
where: {
id: {
exists: true,
},
},
})
expect(result?.length).toBe(5)
for (let i = 10; i > 5; i--) {
expect(result?.[-i + 10]?.number).toBe(i)
expect(result?.[-i + 10]?.title).toBe('updated')
}
// Ensure all posts minus the one we don't want updated are updated
const { docs } = await payload.find({
collection: postsSlug,
depth: 0,
pagination: false,
sort: '-number',
where: {
title: {
equals: 'updated',
},
},
})
expect(docs).toHaveLength(5)
for (let i = 10; i > 5; i--) {
expect(docs?.[-i + 10]?.number).toBe(i)
expect(docs?.[-i + 10]?.title).toBe('updated')
}
})
it('ensure updateMany correctly handles 0 limit', async () => {
await payload.db.deleteMany({
collection: postsSlug,

View File

@@ -54,6 +54,7 @@ export type SupportedTimezones =
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
@@ -151,6 +152,7 @@ export interface UserAuthOperations {
export interface Post {
id: string;
title: string;
number?: number | null;
D1?: {
D2?: {
D3?: {
@@ -545,6 +547,7 @@ export interface PayloadMigration {
*/
export interface PostsSelect<T extends boolean = true> {
title?: T;
number?: T;
D1?:
| T
| {