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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -532,6 +532,7 @@ export type UpdateManyArgs = {
|
||||
*/
|
||||
returning?: boolean
|
||||
select?: SelectType
|
||||
sort?: Sort
|
||||
where: Where
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -44,6 +44,10 @@ export default buildConfigWithDefaults({
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user