diff --git a/packages/db-mongodb/src/updateMany.ts b/packages/db-mongodb/src/updateMany.ts index ceac20fa54..a43ed685ef 100644 --- a/packages/db-mongodb/src/updateMany.ts +++ b/packages/db-mongodb/src/updateMany.ts @@ -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 | 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, diff --git a/packages/drizzle/src/updateMany.ts b/packages/drizzle/src/updateMany.ts index 40cea50911..8cde607a59 100644 --- a/packages/drizzle/src/updateMany.ts +++ b/packages/drizzle/src/updateMany.ts @@ -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, - }) - .from(table) - .where(where) - .limit(limit) - : await _db - .select({ - id: table.id, - }) - .from(table) - .where(where) + 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', + }) + } + + if (orderBy) { + chainedMethods.push({ + args: [() => orderBy.map(({ column, order }) => order(column))], + method: 'orderBy', + }) + } + + const docsToUpdate = await chainMethods({ + methods: chainedMethods, + query, + }) idsToUpdate = docsToUpdate?.map((doc) => doc.id) } diff --git a/packages/payload/src/database/types.ts b/packages/payload/src/database/types.ts index 5159f74c9b..b208b5ed01 100644 --- a/packages/payload/src/database/types.ts +++ b/packages/payload/src/database/types.ts @@ -532,6 +532,7 @@ export type UpdateManyArgs = { */ returning?: boolean select?: SelectType + sort?: Sort where: Where } diff --git a/test/bulk-edit/payload-types.ts b/test/bulk-edit/payload-types.ts index 40df2be010..f1b36a851e 100644 --- a/test/bulk-edit/payload-types.ts +++ b/test/bulk-edit/payload-types.ts @@ -82,7 +82,7 @@ export interface Config { 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; 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; diff --git a/test/database/config.ts b/test/database/config.ts index 7e372c6f6f..b7e73aed69 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -44,6 +44,10 @@ export default buildConfigWithDefaults({ type: 'text', required: true, }, + { + name: 'number', + type: 'number', + }, { type: 'tabs', tabs: [ diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index 9a6626bd90..eb44172d92 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -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, diff --git a/test/database/payload-types.ts b/test/database/payload-types.ts index a80a82aa00..470a03a4a6 100644 --- a/test/database/payload-types.ts +++ b/test/database/payload-types.ts @@ -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 { title?: T; + number?: T; D1?: | T | {