Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/configuration/collections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ The following options are available:
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `bulkOperations` | Configure bulk operation behavior. Set `singleTransaction: true` to process documents one at a time in separate transactions. Useful for avoiding database transaction limits with large datasets. Defaults to global config. |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
Expand Down
75 changes: 38 additions & 37 deletions docs/configuration/overview.mdx

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion packages/payload/src/collections/config/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IncomingAuthType, LoginWithUsernameOptions } from '../../auth/types.js'
import type { Config } from '../../config/types.js'
import type { CollectionConfig } from './types.js'

import { defaultAccess } from '../../auth/defaultAccess.js'
Expand Down Expand Up @@ -54,7 +55,10 @@ export const defaults: Partial<CollectionConfig> = {
versions: false,
}

export const addDefaultsToCollectionConfig = (collection: CollectionConfig): CollectionConfig => {
export const addDefaultsToCollectionConfig = (
collection: CollectionConfig,
config?: Config,
): CollectionConfig => {
collection.access = {
create: defaultAccess,
delete: defaultAccess,
Expand Down Expand Up @@ -105,6 +109,11 @@ export const addDefaultsToCollectionConfig = (collection: CollectionConfig): Col
...(collection.hooks || {}),
}

collection.bulkOperations = {
singleTransaction: config?.bulkOperations?.singleTransaction ?? false,
...(collection.bulkOperations || {}),
}

collection.timestamps = collection.timestamps ?? true
collection.upload = collection.upload ?? false
collection.versions = collection.versions ?? false
Expand Down
2 changes: 1 addition & 1 deletion packages/payload/src/collections/config/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const sanitizeCollection = async (
// Make copy of collection config
// /////////////////////////////////

const sanitized: CollectionConfig = addDefaultsToCollectionConfig(collection)
const sanitized: CollectionConfig = addDefaultsToCollectionConfig(collection, config)

// /////////////////////////////////
// Sanitize fields
Expand Down
13 changes: 13 additions & 0 deletions packages/payload/src/collections/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,19 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
* Use `true` to enable with default options
*/
auth?: boolean | IncomingAuthType
/**
* Configuration for bulk operations
*/
bulkOperations?: {
/**
* When true, bulk operations will process documents one at a time
* in separate transactions instead of all at once in a single transaction.
* Useful for avoiding database-side transaction limitations.
*
* @default false
*/
singleTransaction?: boolean
}
/** Extension point to add your custom data. Server only. */
custom?: Record<string, any>
/**
Expand Down
24 changes: 23 additions & 1 deletion packages/payload/src/collections/operations/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ export const deleteOperation = async <
const { id } = doc

try {
// Each document gets its own transaction when singleTransaction is enabled
let docShouldCommit = false
if (collectionConfig.bulkOperations?.singleTransaction) {
docShouldCommit = await initTransaction(req)
}

// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
Expand Down Expand Up @@ -278,9 +284,15 @@ export const deleteOperation = async <
// /////////////////////////////////////
// 8. Return results
// /////////////////////////////////////
if (docShouldCommit) {
await commitTransaction(req)
}

return result
} catch (error) {
if (collectionConfig.bulkOperations?.singleTransaction) {
await killTransaction(req)
}
errors.push({
id: doc.id,
message: error instanceof Error ? error.message : 'Unknown error',
Expand All @@ -289,7 +301,17 @@ export const deleteOperation = async <
return null
})

const awaitedDocs = await Promise.all(promises)
// Process sequentially when using single transaction mode to avoid shared state issues
// Process in parallel when using one transaction for better performance
let awaitedDocs
if (collectionConfig.bulkOperations?.singleTransaction) {
awaitedDocs = []
for (const promise of promises) {
awaitedDocs.push(await promise)
}
} else {
awaitedDocs = await Promise.all(promises)
}

// /////////////////////////////////////
// Delete Preferences
Expand Down
25 changes: 24 additions & 1 deletion packages/payload/src/collections/operations/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ export const updateOperation = async <
const { id } = docWithLocales

try {
// Each document gets its own transaction when singleTransaction is enabled
let docShouldCommit = false
if (collectionConfig.bulkOperations?.singleTransaction) {
docShouldCommit = await initTransaction(req)
}

const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
Expand Down Expand Up @@ -263,8 +269,15 @@ export const updateOperation = async <
showHiddenFields: showHiddenFields!,
})

if (docShouldCommit) {
await commitTransaction(req)
}

return updatedDoc
} catch (error) {
if (collectionConfig.bulkOperations?.singleTransaction) {
await killTransaction(req)
}
errors.push({
id,
message: error instanceof Error ? error.message : 'Unknown error',
Expand All @@ -279,7 +292,17 @@ export const updateOperation = async <
req,
})

const awaitedDocs = await Promise.all(promises)
// Process sequentially when using single transaction mode to avoid shared state issues
// Process in parallel when using one transaction for better performance
let awaitedDocs: (DataFromCollectionSlug<TSlug> | null)[]
if (collectionConfig.bulkOperations?.singleTransaction) {
awaitedDocs = []
for (const promise of promises) {
awaitedDocs.push(await promise)
}
} else {
awaitedDocs = await Promise.all(promises)
}

let result = {
docs: awaitedDocs.filter(Boolean),
Expand Down
14 changes: 14 additions & 0 deletions packages/payload/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,20 @@ export type Config = {
* @experimental This property is experimental and may change in future releases. Use at your own risk.
*/
bodyParser?: Partial<BusboyConfig>
/**
* Global default configuration for bulk operations across all collections.
* Can be overridden per collection.
*/
bulkOperations?: {
/**
* When true, bulk operations will process documents one at a time
* in separate transactions instead of all at once in a single transaction.
* Useful for avoiding transaction limitations with large datasets.
*
* @default false
*/
singleTransaction?: boolean
}
/**
* Manage the datamodel of your application
*
Expand Down
54 changes: 54 additions & 0 deletions test/database/int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2011,6 +2011,60 @@ describe('database', () => {
expect(worldDocs).toHaveLength(5)
})

it('should bulk update with singleTransaction: true', async () => {
const collectionConfig = payload.config.collections.find((c) => c.slug === collection)

assert(collectionConfig)

const originalConfig = collectionConfig.bulkOperations
collectionConfig.bulkOperations = { singleTransaction: true }

try {
const posts = await Promise.all([
payload.create({ collection, data: { title: 'test1' } }),
payload.create({ collection, data: { title: 'test2' } }),
payload.create({ collection, data: { title: 'test3' } }),
])

const result = await payload.update({
collection,
data: { title: 'updated' },
where: { id: { in: posts.map((p) => p.id) } },
})

expect(result.docs).toHaveLength(3)
expect(result.errors).toHaveLength(0)
} finally {
collectionConfig.bulkOperations = originalConfig
}
})

it('should bulk delete with singleTransaction: true', async () => {
const collectionConfig = payload.config.collections.find((c) => c.slug === collection)

assert(collectionConfig)

const originalConfig = collectionConfig.bulkOperations
collectionConfig.bulkOperations = { singleTransaction: true }

try {
const posts = await Promise.all([
payload.create({ collection, data: { title: 'toDelete1' } }),
payload.create({ collection, data: { title: 'toDelete2' } }),
])

const result = await payload.delete({
collection,
where: { id: { in: posts.map((p) => p.id) } },
})

expect(result.docs).toHaveLength(2)
expect(result.errors).toHaveLength(0)
} finally {
collectionConfig.bulkOperations = originalConfig
}
})

it('should CRUD point field', async () => {
const result = await payload.create({
collection: 'default-values',
Expand Down
Loading