From 7c0694c00cf3701913d63412e2933469a5ec4dce Mon Sep 17 00:00:00 2001 From: Marek Broz <48223014+brozmarek@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:08:27 +0200 Subject: [PATCH] fix(db-mongodb): serialize paginated count and docs inside transactions --- packages/db-mongodb/package.json | 2 +- .../src/utilities/aggregatePaginate.ts | 16 ++- pnpm-lock.yaml | 10 +- test/database/int.spec.ts | 114 ++++++++++++++++++ 4 files changed, 132 insertions(+), 10 deletions(-) diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json index e085775ad74..3d2eba50dc2 100644 --- a/packages/db-mongodb/package.json +++ b/packages/db-mongodb/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "mongoose": "8.22.1", - "mongoose-paginate-v2": "1.9.4", + "mongoose-paginate-v2": "1.9.5", "prompts": "2.4.2", "uuid": "14.0.0" }, diff --git a/packages/db-mongodb/src/utilities/aggregatePaginate.ts b/packages/db-mongodb/src/utilities/aggregatePaginate.ts index 5e0b6d1de3e..7ba72b7f9d8 100644 --- a/packages/db-mongodb/src/utilities/aggregatePaginate.ts +++ b/packages/db-mongodb/src/utilities/aggregatePaginate.ts @@ -84,10 +84,18 @@ export const aggregatePaginate = async ({ } } - const [docs, countResult] = await Promise.all([ - Model.aggregate(aggregation, { collation, session }), - countPromise, - ]) + let docs: unknown[] + let countResult: null | number + + if (session) { + docs = await Model.aggregate(aggregation, { collation, session }) + countResult = await countPromise + } else { + ;[docs, countResult] = await Promise.all([ + Model.aggregate(aggregation, { collation, session }), + countPromise, + ]) + } const count = countResult === null ? docs.length : countResult diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67f9464100b..4d8f6641532 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,8 +373,8 @@ importers: specifier: 8.22.1 version: 8.22.1(@aws-sdk/credential-providers@3.1063.0) mongoose-paginate-v2: - specifier: 1.9.4 - version: 1.9.4(mongoose@8.22.1(@aws-sdk/credential-providers@3.1063.0)) + specifier: 1.9.5 + version: 1.9.5(mongoose@8.22.1(@aws-sdk/credential-providers@3.1063.0)) prompts: specifier: 2.4.2 version: 2.4.2 @@ -12053,8 +12053,8 @@ packages: peerDependencies: mongoose: '>=5.11.10' - mongoose-paginate-v2@1.9.4: - resolution: {integrity: sha512-0LOsVEQmjrbJKVDi/IvFEhIezmuRjUE4loGgslv57j9nK/NMC+mbKT0QnaPSPpib4lByKVBcy3VbDa1TvlHZjA==} + mongoose-paginate-v2@1.9.5: + resolution: {integrity: sha512-sgPbV0G7SfdW1XdEuNMokzJOT85GNx786itp6UiNmDvKGyxMSHbeyn4tMZBseOmGu2uOo5ByVgfjhaxkOBY71A==} engines: {node: '>=4.0.0'} mongoose@8.22.1: @@ -25581,7 +25581,7 @@ snapshots: mongoose: 8.22.1(@aws-sdk/credential-providers@3.1063.0) mpath: 0.8.4 - mongoose-paginate-v2@1.9.4(mongoose@8.22.1(@aws-sdk/credential-providers@3.1063.0)): + mongoose-paginate-v2@1.9.5(mongoose@8.22.1(@aws-sdk/credential-providers@3.1063.0)): dependencies: mongoose-lean-virtuals: 1.1.1(mongoose@8.22.1(@aws-sdk/credential-providers@3.1063.0)) transitivePeerDependencies: diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index b5819beb20f..cc80f1371bb 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -3,7 +3,9 @@ import type { MongooseAdapter } from '@payloadcms/db-mongodb' import type { PostgresAdapter } from '@payloadcms/db-postgres' import type { Table } from 'drizzle-orm' import type { + CollectionSlug, DataFromCollectionSlug, + PaginatedDocs, Payload, PayloadRequest, TypeWithID, @@ -2170,6 +2172,118 @@ describe('database', () => { }) describe('transactions', () => { + describe('pagination', { db: 'mongo' }, () => { + const createdDocs: { collection: CollectionSlug; id: number | string }[] = [] + + const trackCreatedDoc = (collection: CollectionSlug, id: number | string) => { + createdDocs.push({ collection, id }) + } + + afterEach(async () => { + for (const { collection, id } of [...createdDocs].reverse()) { + await payload.delete({ collection, id }) + } + + createdDocs.length = 0 + }) + + it('should run paginated find operations inside a transaction session', async () => { + const uniqueTitle = `transaction pagination ${randomUUID()}` + + const simpleOne = await payload.create({ + collection: 'simple', + data: { text: `${uniqueTitle} simple one` }, + }) + trackCreatedDoc('simple', simpleOne.id) + + const simpleTwo = await payload.create({ + collection: 'simple', + data: { text: `${uniqueTitle} simple two` }, + }) + trackCreatedDoc('simple', simpleTwo.id) + + const categoryOne = await payload.create({ + collection: 'categories', + data: { title: `${uniqueTitle} category one` }, + }) + trackCreatedDoc('categories', categoryOne.id) + + const categoryTwo = await payload.create({ + collection: 'categories', + data: { title: `${uniqueTitle} category two` }, + }) + trackCreatedDoc('categories', categoryTwo.id) + + const postOne = await payload.create({ + collection: postsSlug, + data: { + category: categoryOne.id, + title: `${uniqueTitle} post one`, + }, + }) + trackCreatedDoc(postsSlug, postOne.id) + + const postTwo = await payload.create({ + collection: postsSlug, + data: { + category: categoryTwo.id, + title: `${uniqueTitle} post two`, + }, + }) + trackCreatedDoc(postsSlug, postTwo.id) + + const req = { + payload, + user, + } as unknown as PayloadRequest + + await initTransaction(req) + + let aggregatePaginatedResult!: PaginatedDocs + let modelPaginatedResult!: PaginatedDocs + + try { + modelPaginatedResult = await payload.find({ + collection: 'simple', + limit: 1, + page: 1, + req, + where: { + text: { + contains: uniqueTitle, + }, + }, + }) + + aggregatePaginatedResult = await payload.find({ + collection: postsSlug, + limit: 1, + page: 1, + req, + sort: 'category.title', + where: { + title: { + contains: uniqueTitle, + }, + }, + }) + + await commitTransaction(req) + } catch (error) { + await killTransaction(req) + throw error + } + + expect(modelPaginatedResult.totalDocs).toBe(2) + expect(modelPaginatedResult.docs).toHaveLength(1) + expect(modelPaginatedResult.hasNextPage).toBe(true) + + expect(aggregatePaginatedResult.totalDocs).toBe(2) + expect(aggregatePaginatedResult.docs).toHaveLength(1) + expect(aggregatePaginatedResult.hasNextPage).toBe(true) + }) + }) + describe('local api', () => { // sqlite cannot handle concurrent write transactions if (