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 { MongooseUpdateQueryOptions } from 'mongoose'
import type { UpdateMany } from 'payload'
import { flattenWhereToOperators, type UpdateMany } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js' import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js' import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
@@ -21,11 +23,30 @@ export const updateMany: UpdateMany = async function updateMany(
req, req,
returning, returning,
select, select,
sort: sortArg,
where, 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 }) 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 = { const options: MongooseUpdateQueryOptions = {
...optionsArgs, ...optionsArgs,
lean: true, lean: true,
@@ -53,7 +74,7 @@ export const updateMany: UpdateMany = async function updateMany(
const documentsToUpdate = await Model.find( const documentsToUpdate = await Model.find(
query, query,
{}, {},
{ ...options, limit, projection: { _id: 1 } }, { ...options, limit, projection: { _id: 1 }, sort },
) )
if (documentsToUpdate.length === 0) { if (documentsToUpdate.length === 0) {
return null return null
@@ -71,7 +92,14 @@ export const updateMany: UpdateMany = async function updateMany(
return null return null
} }
const result = await Model.find(query, {}, options) const result = await Model.find(
query,
{},
{
...options,
sort,
},
)
transform({ transform({
adapter: this, adapter: this,

View File

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

View File

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

View File

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

View File

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

View File

@@ -317,7 +317,7 @@ describe('database', () => {
payload.config.db.allowIDOnCreate = false 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 let id: any = null
if (payload.db.name === 'mongoose') { if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString() id = new mongoose.Types.ObjectId().toHexString()
@@ -332,7 +332,7 @@ describe('database', () => {
expect(post.id).toBe(id) 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 let id: any = null
if (payload.db.name === 'mongoose') { if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString() id = new mongoose.Types.ObjectId().toHexString()
@@ -354,7 +354,7 @@ describe('database', () => {
expect(post.doc.id).toBe(id) expect(post.doc.id).toBe(id)
}) })
it('GraphQL - accepts ID on create', async () => { it('graphQL - accepts ID on create', async () => {
let id: any = null let id: any = null
if (payload.db.name === 'mongoose') { if (payload.db.name === 'mongoose') {
id = new mongoose.Types.ObjectId().toHexString() id = new mongoose.Types.ObjectId().toHexString()
@@ -1168,6 +1168,140 @@ describe('database', () => {
expect(notUpdatedDocs?.[5]?.title).toBe('not updated') 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 () => { it('ensure updateMany correctly handles 0 limit', async () => {
await payload.db.deleteMany({ await payload.db.deleteMany({
collection: postsSlug, collection: postsSlug,

View File

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