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 { 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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,6 +532,7 @@ export type UpdateManyArgs = {
|
|||||||
*/
|
*/
|
||||||
returning?: boolean
|
returning?: boolean
|
||||||
select?: SelectType
|
select?: SelectType
|
||||||
|
sort?: Sort
|
||||||
where: Where
|
where: Where
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
| {
|
| {
|
||||||
|
|||||||
Reference in New Issue
Block a user