feat: add disableTransaction to local api (#8697)

This commit is contained in:
Dan Ribbens
2024-10-14 14:39:20 -04:00
committed by GitHub
parent 08f883166e
commit d781624a86
15 changed files with 108 additions and 22 deletions

View File

@@ -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. 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.
<Banner type="info"> <Banner type="info">
<strong>Note:</strong> <strong>Note:</strong>
@@ -114,3 +114,18 @@ standalonePayloadScript()
## Disabling Transactions ## 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. 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 },
})
```

View File

@@ -78,7 +78,7 @@ 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. 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 | | Local Option | Description |
| ------------------ | ------------ | |-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | | `collection` | Required for Collection operations. Specifies the Collection slug to operate against. |
| `data` | The data to use within the operation. Required for `create`, `update`. | | `data` | The data to use within the operation. Required for `create`, `update`. |
| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. | | `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. |
@@ -91,6 +91,7 @@ You can specify more options within the Local API vs. REST or GraphQL due to the
| `pagination` | Set to false to return all documents and avoid querying for document counts. | | `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. | | `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. | | `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._ _There are more options available on an operation by operation basis outlined below._

View File

@@ -34,6 +34,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
collection: Collection collection: Collection
data: RequiredDataFromCollectionSlug<TSlug> data: RequiredDataFromCollectionSlug<TSlug>
depth?: number depth?: number
disableTransaction?: boolean
disableVerificationEmail?: boolean disableVerificationEmail?: boolean
draft?: boolean draft?: boolean
overrideAccess?: boolean overrideAccess?: boolean
@@ -48,7 +49,7 @@ export const createOperation = async <TSlug extends CollectionSlug>(
let args = incomingArgs let args = incomingArgs
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
ensureUsernameOrEmail<TSlug>({ ensureUsernameOrEmail<TSlug>({
authOptions: args.collection.config.auth, authOptions: args.collection.config.auth,

View File

@@ -22,6 +22,7 @@ import { buildAfterOperation } from './utils.js'
export type Arguments = { export type Arguments = {
collection: Collection collection: Collection
depth?: number depth?: number
disableTransaction?: boolean
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean overrideLock?: boolean
req: PayloadRequest req: PayloadRequest
@@ -41,7 +42,7 @@ export const deleteOperation = async <TSlug extends CollectionSlug>(
let args = incomingArgs let args = incomingArgs
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
// ///////////////////////////////////// // /////////////////////////////////////
// beforeOperation - Collection // beforeOperation - Collection
// ///////////////////////////////////// // /////////////////////////////////////

View File

@@ -19,6 +19,7 @@ import { buildAfterOperation } from './utils.js'
export type Arguments = { export type Arguments = {
collection: Collection collection: Collection
depth?: number depth?: number
disableTransaction?: boolean
id: number | string id: number | string
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean overrideLock?: boolean
@@ -32,7 +33,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug>(
let args = incomingArgs let args = incomingArgs
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
// ///////////////////////////////////// // /////////////////////////////////////
// beforeOperation - Collection // beforeOperation - Collection

View File

@@ -28,6 +28,7 @@ import { buildAfterOperation } from './utils.js'
export type Arguments = { export type Arguments = {
collection: Collection collection: Collection
depth?: number depth?: number
disableTransaction?: boolean
draft?: boolean draft?: boolean
id: number | string id: number | string
overrideAccess?: boolean overrideAccess?: boolean
@@ -42,7 +43,7 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
const operation = 'create' const operation = 'create'
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
// ///////////////////////////////////// // /////////////////////////////////////
// beforeOperation - Collection // beforeOperation - Collection

View File

@@ -16,6 +16,7 @@ export type Options<TSlug extends CollectionSlug> = {
context?: RequestContext context?: RequestContext
data: RequiredDataFromCollectionSlug<TSlug> data: RequiredDataFromCollectionSlug<TSlug>
depth?: number depth?: number
disableTransaction?: boolean
disableVerificationEmail?: boolean disableVerificationEmail?: boolean
draft?: boolean draft?: boolean
fallbackLocale?: TypedLocale fallbackLocale?: TypedLocale
@@ -38,6 +39,7 @@ export default async function createLocal<TSlug extends CollectionSlug>(
collection: collectionSlug, collection: collectionSlug,
data, data,
depth, depth,
disableTransaction,
disableVerificationEmail, disableVerificationEmail,
draft, draft,
file, file,
@@ -61,6 +63,7 @@ export default async function createLocal<TSlug extends CollectionSlug>(
collection, collection,
data, data,
depth, depth,
disableTransaction,
disableVerificationEmail, disableVerificationEmail,
draft, draft,
overrideAccess, overrideAccess,

View File

@@ -14,6 +14,7 @@ export type BaseOptions<TSlug extends CollectionSlug> = {
*/ */
context?: RequestContext context?: RequestContext
depth?: number depth?: number
disableTransaction?: boolean
fallbackLocale?: TypedLocale fallbackLocale?: TypedLocale
locale?: TypedLocale locale?: TypedLocale
overrideAccess?: boolean overrideAccess?: boolean
@@ -55,6 +56,7 @@ async function deleteLocal<TSlug extends CollectionSlug>(
id, id,
collection: collectionSlug, collection: collectionSlug,
depth, depth,
disableTransaction,
overrideAccess = true, overrideAccess = true,
overrideLock, overrideLock,
showHiddenFields, showHiddenFields,
@@ -73,6 +75,7 @@ async function deleteLocal<TSlug extends CollectionSlug>(
id, id,
collection, collection,
depth, depth,
disableTransaction,
overrideAccess, overrideAccess,
overrideLock, overrideLock,
req: await createLocalReq(options, payload), req: await createLocalReq(options, payload),

View File

@@ -14,6 +14,7 @@ export type Options<TSlug extends CollectionSlug> = {
*/ */
context?: RequestContext context?: RequestContext
depth?: number depth?: number
disableTransaction?: boolean
draft?: boolean draft?: boolean
fallbackLocale?: TypedLocale fallbackLocale?: TypedLocale
id: number | string id: number | string
@@ -32,6 +33,7 @@ export async function duplicate<TSlug extends CollectionSlug>(
id, id,
collection: collectionSlug, collection: collectionSlug,
depth, depth,
disableTransaction,
draft, draft,
overrideAccess = true, overrideAccess = true,
showHiddenFields, showHiddenFields,
@@ -57,6 +59,7 @@ export async function duplicate<TSlug extends CollectionSlug>(
id, id,
collection, collection,
depth, depth,
disableTransaction,
draft, draft,
overrideAccess, overrideAccess,
req, req,

View File

@@ -24,6 +24,7 @@ export type BaseOptions<TSlug extends CollectionSlug> = {
context?: RequestContext context?: RequestContext
data: DeepPartial<RequiredDataFromCollectionSlug<TSlug>> data: DeepPartial<RequiredDataFromCollectionSlug<TSlug>>
depth?: number depth?: number
disableTransaction?: boolean
draft?: boolean draft?: boolean
fallbackLocale?: TypedLocale fallbackLocale?: TypedLocale
file?: File file?: File
@@ -73,6 +74,7 @@ async function updateLocal<TSlug extends CollectionSlug>(
collection: collectionSlug, collection: collectionSlug,
data, data,
depth, depth,
disableTransaction,
draft, draft,
file, file,
filePath, filePath,
@@ -101,6 +103,7 @@ async function updateLocal<TSlug extends CollectionSlug>(
collection, collection,
data, data,
depth, depth,
disableTransaction,
draft, draft,
overrideAccess, overrideAccess,
overrideLock, overrideLock,

View File

@@ -38,6 +38,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
collection: Collection collection: Collection
data: DeepPartial<RequiredDataFromCollectionSlug<TSlug>> data: DeepPartial<RequiredDataFromCollectionSlug<TSlug>>
depth?: number depth?: number
disableTransaction?: boolean
disableVerificationEmail?: boolean disableVerificationEmail?: boolean
draft?: boolean draft?: boolean
limit?: number limit?: number
@@ -55,7 +56,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
let args = incomingArgs let args = incomingArgs
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
// ///////////////////////////////////// // /////////////////////////////////////
// beforeOperation - Collection // beforeOperation - Collection

View File

@@ -39,6 +39,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
collection: Collection collection: Collection
data: DeepPartial<RequiredDataFromCollectionSlug<TSlug>> data: DeepPartial<RequiredDataFromCollectionSlug<TSlug>>
depth?: number depth?: number
disableTransaction?: boolean
disableVerificationEmail?: boolean disableVerificationEmail?: boolean
draft?: boolean draft?: boolean
id: number | string id: number | string
@@ -56,7 +57,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
let args = incomingArgs let args = incomingArgs
try { try {
const shouldCommit = await initTransaction(args.req) const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
// ///////////////////////////////////// // /////////////////////////////////////
// beforeOperation - Collection // beforeOperation - Collection

View File

@@ -21,6 +21,7 @@ type Args<TSlug extends GlobalSlug> = {
autosave?: boolean autosave?: boolean
data: DeepPartial<Omit<DataFromGlobalSlug<TSlug>, 'id'>> data: DeepPartial<Omit<DataFromGlobalSlug<TSlug>, 'id'>>
depth?: number depth?: number
disableTransaction?: boolean
draft?: boolean draft?: boolean
globalConfig: SanitizedGlobalConfig globalConfig: SanitizedGlobalConfig
overrideAccess?: boolean overrideAccess?: boolean
@@ -42,6 +43,7 @@ export const updateOperation = async <TSlug extends GlobalSlug>(
slug, slug,
autosave, autosave,
depth, depth,
disableTransaction,
draft: draftArg, draft: draftArg,
globalConfig, globalConfig,
overrideAccess, overrideAccess,
@@ -53,7 +55,7 @@ export const updateOperation = async <TSlug extends GlobalSlug>(
} = args } = args
try { try {
const shouldCommit = await initTransaction(req) const shouldCommit = !disableTransaction && (await initTransaction(req))
let { data } = args let { data } = args

View File

@@ -28,6 +28,16 @@ export default buildConfigWithDefaults({
type: 'text', type: 'text',
required: true, required: true,
}, },
{
name: 'hasTransaction',
type: 'checkbox',
hooks: {
beforeChange: [({ req }) => !!req.transactionID],
},
admin: {
readOnly: true,
},
},
{ {
name: 'throwAfterChange', name: 'throwAfterChange',
type: 'checkbox', type: 'checkbox',

View File

@@ -458,6 +458,46 @@ describe('database', () => {
).rejects.toThrow('Not Found') ).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()
})
})
}) })
}) })