diff --git a/docs/database/transactions.mdx b/docs/database/transactions.mdx index 373e67a0d8..70bb026838 100644 --- a/docs/database/transactions.mdx +++ b/docs/database/transactions.mdx @@ -8,7 +8,7 @@ desc: Database transactions are fully supported within Payload. Database transactions allow your application to make a series of database changes in an all-or-nothing commit. Consider an HTTP request that creates a new **Order** and has an `afterChange` hook to update the stock count of related **Items**. If an error occurs when updating an **Item** and an HTTP error is returned to the user, you would not want the new **Order** to be persisted or any other items to be changed either. This kind of interaction with the database is handled seamlessly with transactions. -By default, Payload will use transactions for all operations, as long as it is supported by the configured database. Database changes are contained within all Payload operations and any errors thrown will result in all changes being rolled back without being committed. When transactions are not supported by the database, Payload will continue to operate as expected without them. +By default, Payload will use transactions for all data changing operations, as long as it is supported by the configured database. Database changes are contained within all Payload operations and any errors thrown will result in all changes being rolled back without being committed. When transactions are not supported by the database, Payload will continue to operate as expected without them. Note: @@ -114,3 +114,18 @@ standalonePayloadScript() ## Disabling Transactions If you wish to disable transactions entirely, you can do so by passing `false` as the `transactionOptions` in your database adapter configuration. All the official Payload database adapters support this option. + +In addition to allowing database transactions to be disabled at the adapter level. You can prevent Payload from using a transaction in direct calls to the local API by adding `disableTransaction: true` to the args. For example: + +```ts +await payload.update({ + collection: 'posts', + data: { + some: 'data', + }, + where: { + slug: { equals: 'my-slug' } + }, + req: { disableTransaction: true }, +}) +``` diff --git a/docs/local-api/overview.mdx b/docs/local-api/overview.mdx index 537e031a42..c6ddb5783b 100644 --- a/docs/local-api/overview.mdx +++ b/docs/local-api/overview.mdx @@ -77,20 +77,21 @@ Both options function in exactly the same way outside of one having HMR support You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in. -| Local Option | Description | -| ------------------ | ------------ | -| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | -| `data` | The data to use within the operation. Required for `create`, `update`. | -| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. | -| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. | -| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. | -| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. | -| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents).| -| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | -| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | -| `pagination` | Set to false to return all documents and avoid querying for document counts. | -| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | -| `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. | +| Local Option | Description | +|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | +| `data` | The data to use within the operation. Required for `create`, `update`. | +| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. | +| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. | +| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. | +| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. | +| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). | +| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | +| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | +| `pagination` | Set to false to return all documents and avoid querying for document counts. | +| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | +| `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. | +| `disableTransaction` | When set to `true`, a [database transactions](../database/transactions) will not be initialized. | _There are more options available on an operation by operation basis outlined below._ diff --git a/packages/payload/src/collections/operations/create.ts b/packages/payload/src/collections/operations/create.ts index fd239fd7a8..bb223a100d 100644 --- a/packages/payload/src/collections/operations/create.ts +++ b/packages/payload/src/collections/operations/create.ts @@ -34,6 +34,7 @@ export type Arguments = { collection: Collection data: RequiredDataFromCollectionSlug depth?: number + disableTransaction?: boolean disableVerificationEmail?: boolean draft?: boolean overrideAccess?: boolean @@ -48,7 +49,7 @@ export const createOperation = async ( let args = incomingArgs try { - const shouldCommit = await initTransaction(args.req) + const shouldCommit = !args.disableTransaction && (await initTransaction(args.req)) ensureUsernameOrEmail({ authOptions: args.collection.config.auth, diff --git a/packages/payload/src/collections/operations/delete.ts b/packages/payload/src/collections/operations/delete.ts index b6b2b24e3c..9eabf12f7a 100644 --- a/packages/payload/src/collections/operations/delete.ts +++ b/packages/payload/src/collections/operations/delete.ts @@ -22,6 +22,7 @@ import { buildAfterOperation } from './utils.js' export type Arguments = { collection: Collection depth?: number + disableTransaction?: boolean overrideAccess?: boolean overrideLock?: boolean req: PayloadRequest @@ -41,7 +42,7 @@ export const deleteOperation = async ( let args = incomingArgs try { - const shouldCommit = await initTransaction(args.req) + const shouldCommit = !args.disableTransaction && (await initTransaction(args.req)) // ///////////////////////////////////// // beforeOperation - Collection // ///////////////////////////////////// diff --git a/packages/payload/src/collections/operations/deleteByID.ts b/packages/payload/src/collections/operations/deleteByID.ts index c9eb68ae0c..eced5be71b 100644 --- a/packages/payload/src/collections/operations/deleteByID.ts +++ b/packages/payload/src/collections/operations/deleteByID.ts @@ -19,6 +19,7 @@ import { buildAfterOperation } from './utils.js' export type Arguments = { collection: Collection depth?: number + disableTransaction?: boolean id: number | string overrideAccess?: boolean overrideLock?: boolean @@ -32,7 +33,7 @@ export const deleteByIDOperation = async ( let args = incomingArgs try { - const shouldCommit = await initTransaction(args.req) + const shouldCommit = !args.disableTransaction && (await initTransaction(args.req)) // ///////////////////////////////////// // beforeOperation - Collection diff --git a/packages/payload/src/collections/operations/duplicate.ts b/packages/payload/src/collections/operations/duplicate.ts index 6539b802e9..4fe5e4c588 100644 --- a/packages/payload/src/collections/operations/duplicate.ts +++ b/packages/payload/src/collections/operations/duplicate.ts @@ -28,6 +28,7 @@ import { buildAfterOperation } from './utils.js' export type Arguments = { collection: Collection depth?: number + disableTransaction?: boolean draft?: boolean id: number | string overrideAccess?: boolean @@ -42,7 +43,7 @@ export const duplicateOperation = async ( const operation = 'create' try { - const shouldCommit = await initTransaction(args.req) + const shouldCommit = !args.disableTransaction && (await initTransaction(args.req)) // ///////////////////////////////////// // beforeOperation - Collection diff --git a/packages/payload/src/collections/operations/local/create.ts b/packages/payload/src/collections/operations/local/create.ts index c4d26ef18c..7a63c327c8 100644 --- a/packages/payload/src/collections/operations/local/create.ts +++ b/packages/payload/src/collections/operations/local/create.ts @@ -16,6 +16,7 @@ export type Options = { context?: RequestContext data: RequiredDataFromCollectionSlug depth?: number + disableTransaction?: boolean disableVerificationEmail?: boolean draft?: boolean fallbackLocale?: TypedLocale @@ -38,6 +39,7 @@ export default async function createLocal( collection: collectionSlug, data, depth, + disableTransaction, disableVerificationEmail, draft, file, @@ -61,6 +63,7 @@ export default async function createLocal( collection, data, depth, + disableTransaction, disableVerificationEmail, draft, overrideAccess, diff --git a/packages/payload/src/collections/operations/local/delete.ts b/packages/payload/src/collections/operations/local/delete.ts index b418455e83..8d9191faad 100644 --- a/packages/payload/src/collections/operations/local/delete.ts +++ b/packages/payload/src/collections/operations/local/delete.ts @@ -14,6 +14,7 @@ export type BaseOptions = { */ context?: RequestContext depth?: number + disableTransaction?: boolean fallbackLocale?: TypedLocale locale?: TypedLocale overrideAccess?: boolean @@ -55,6 +56,7 @@ async function deleteLocal( id, collection: collectionSlug, depth, + disableTransaction, overrideAccess = true, overrideLock, showHiddenFields, @@ -73,6 +75,7 @@ async function deleteLocal( id, collection, depth, + disableTransaction, overrideAccess, overrideLock, req: await createLocalReq(options, payload), diff --git a/packages/payload/src/collections/operations/local/duplicate.ts b/packages/payload/src/collections/operations/local/duplicate.ts index fb2463400f..5e7097f6ab 100644 --- a/packages/payload/src/collections/operations/local/duplicate.ts +++ b/packages/payload/src/collections/operations/local/duplicate.ts @@ -14,6 +14,7 @@ export type Options = { */ context?: RequestContext depth?: number + disableTransaction?: boolean draft?: boolean fallbackLocale?: TypedLocale id: number | string @@ -32,6 +33,7 @@ export async function duplicate( id, collection: collectionSlug, depth, + disableTransaction, draft, overrideAccess = true, showHiddenFields, @@ -57,6 +59,7 @@ export async function duplicate( id, collection, depth, + disableTransaction, draft, overrideAccess, req, diff --git a/packages/payload/src/collections/operations/local/update.ts b/packages/payload/src/collections/operations/local/update.ts index 6177e3b003..609661c3e6 100644 --- a/packages/payload/src/collections/operations/local/update.ts +++ b/packages/payload/src/collections/operations/local/update.ts @@ -24,6 +24,7 @@ export type BaseOptions = { context?: RequestContext data: DeepPartial> depth?: number + disableTransaction?: boolean draft?: boolean fallbackLocale?: TypedLocale file?: File @@ -73,6 +74,7 @@ async function updateLocal( collection: collectionSlug, data, depth, + disableTransaction, draft, file, filePath, @@ -101,6 +103,7 @@ async function updateLocal( collection, data, depth, + disableTransaction, draft, overrideAccess, overrideLock, diff --git a/packages/payload/src/collections/operations/update.ts b/packages/payload/src/collections/operations/update.ts index baab4f3fb8..ffcba363c2 100644 --- a/packages/payload/src/collections/operations/update.ts +++ b/packages/payload/src/collections/operations/update.ts @@ -38,6 +38,7 @@ export type Arguments = { collection: Collection data: DeepPartial> depth?: number + disableTransaction?: boolean disableVerificationEmail?: boolean draft?: boolean limit?: number @@ -55,7 +56,7 @@ export const updateOperation = async ( let args = incomingArgs try { - const shouldCommit = await initTransaction(args.req) + const shouldCommit = !args.disableTransaction && (await initTransaction(args.req)) // ///////////////////////////////////// // beforeOperation - Collection diff --git a/packages/payload/src/collections/operations/updateByID.ts b/packages/payload/src/collections/operations/updateByID.ts index f9146b6211..6fe0942484 100644 --- a/packages/payload/src/collections/operations/updateByID.ts +++ b/packages/payload/src/collections/operations/updateByID.ts @@ -39,6 +39,7 @@ export type Arguments = { collection: Collection data: DeepPartial> depth?: number + disableTransaction?: boolean disableVerificationEmail?: boolean draft?: boolean id: number | string @@ -56,7 +57,7 @@ export const updateByIDOperation = async ( let args = incomingArgs try { - const shouldCommit = await initTransaction(args.req) + const shouldCommit = !args.disableTransaction && (await initTransaction(args.req)) // ///////////////////////////////////// // beforeOperation - Collection diff --git a/packages/payload/src/globals/operations/update.ts b/packages/payload/src/globals/operations/update.ts index 9d981b8e37..d6d01c8880 100644 --- a/packages/payload/src/globals/operations/update.ts +++ b/packages/payload/src/globals/operations/update.ts @@ -21,6 +21,7 @@ type Args = { autosave?: boolean data: DeepPartial, 'id'>> depth?: number + disableTransaction?: boolean draft?: boolean globalConfig: SanitizedGlobalConfig overrideAccess?: boolean @@ -42,6 +43,7 @@ export const updateOperation = async ( slug, autosave, depth, + disableTransaction, draft: draftArg, globalConfig, overrideAccess, @@ -53,7 +55,7 @@ export const updateOperation = async ( } = args try { - const shouldCommit = await initTransaction(req) + const shouldCommit = !disableTransaction && (await initTransaction(req)) let { data } = args diff --git a/test/database/config.ts b/test/database/config.ts index e267435c21..0d243cea37 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -28,6 +28,16 @@ export default buildConfigWithDefaults({ type: 'text', required: true, }, + { + name: 'hasTransaction', + type: 'checkbox', + hooks: { + beforeChange: [({ req }) => !!req.transactionID], + }, + admin: { + readOnly: true, + }, + }, { name: 'throwAfterChange', type: 'checkbox', diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index 5c27156156..5bd207bbf2 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -458,6 +458,46 @@ describe('database', () => { ).rejects.toThrow('Not Found') }) } + + describe('disableTransaction', () => { + let disabledTransactionPost + beforeAll(async () => { + disabledTransactionPost = await payload.create({ + collection, + data: { + title, + }, + disableTransaction: true, + }) + }) + it('should not use transaction calling create() with disableTransaction', () => { + expect(disabledTransactionPost.hasTransaction).toBeFalsy() + }) + it('should not use transaction calling update() with disableTransaction', async () => { + const result = await payload.update({ + collection, + id: disabledTransactionPost.id, + data: { + title, + }, + disableTransaction: true, + }) + + expect(result.hasTransaction).toBeFalsy() + }) + it('should not use transaction calling delete() with disableTransaction', async () => { + const result = await payload.delete({ + collection, + id: disabledTransactionPost.id, + data: { + title, + }, + disableTransaction: true, + }) + + expect(result.hasTransaction).toBeFalsy() + }) + }) }) })