Skip to content

Commit 9c3f863

Browse files
authored
feat: adds overrideLock flag to update & delete operations (#8294)
- Adds `overrideLock` flag to `update` & `delete` operations - Instead of throwing an `APIError` (500) when trying to update / delete a locked document - now throw a `Locked` (423) error status
1 parent 879f690 commit 9c3f863

File tree

15 files changed

+310
-60
lines changed

15 files changed

+310
-60
lines changed

docs/admin/locked-documents.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,22 @@ export const Posts: CollectionConfig = {
5858
Document locking affects both the Local API and the REST API, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
5959

6060
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
61+
62+
#### Overriding Locks
63+
64+
For operations like update and delete, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
65+
66+
By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`.
67+
68+
```ts
69+
const result = await payload.update({
70+
collection: 'posts',
71+
id: '123',
72+
data: {
73+
title: 'New title',
74+
},
75+
overrideLock: false, // Enforces the document lock, preventing updates if the document is locked
76+
})
77+
```
78+
79+
This option is particularly useful in scenarios where administrative privileges or specific workflows require you to override the lock and ensure the operation is completed.

docs/local-api/overview.mdx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,18 @@ Both options function in exactly the same way outside of one having HMR support
7777

7878
You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in.
7979

80-
| Local Option | Description |
81-
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82-
| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. |
83-
| `data` | The data to use within the operation. Required for `create`, `update`. |
84-
| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. |
85-
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
86-
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
87-
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
88-
| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. |
89-
| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. |
90-
| `pagination` | Set to false to return all documents and avoid querying for document counts. |
80+
| Local Option | Description |
81+
| ------------------ | ------------ |
82+
| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. |
83+
| `data` | The data to use within the operation. Required for `create`, `update`. |
84+
| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. |
85+
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
86+
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
87+
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
88+
| `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).|
89+
| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. |
90+
| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. |
91+
| `pagination` | Set to false to return all documents and avoid querying for document counts. |
9192
| `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. |
9293
| `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. |
9394

@@ -206,6 +207,7 @@ const result = await payload.update({
206207
fallbackLocale: false,
207208
user: dummyUser,
208209
overrideAccess: false,
210+
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
209211
showHiddenFields: true,
210212

211213
// If your collection supports uploads, you can upload
@@ -244,6 +246,7 @@ const result = await payload.update({
244246
fallbackLocale: false,
245247
user: dummyUser,
246248
overrideAccess: false,
249+
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
247250
showHiddenFields: true,
248251

249252
// If your collection supports uploads, you can upload
@@ -270,6 +273,7 @@ const result = await payload.delete({
270273
fallbackLocale: false,
271274
user: dummyUser,
272275
overrideAccess: false,
276+
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
273277
showHiddenFields: true,
274278
})
275279
```
@@ -293,6 +297,7 @@ const result = await payload.delete({
293297
fallbackLocale: false,
294298
user: dummyUser,
295299
overrideAccess: false,
300+
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
296301
showHiddenFields: true,
297302
})
298303
```
@@ -429,6 +434,7 @@ const result = await payload.updateGlobal({
429434
fallbackLocale: false,
430435
user: dummyUser,
431436
overrideAccess: false,
437+
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
432438
showHiddenFields: true,
433439
})
434440
```

packages/payload/src/collections/operations/delete.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type Arguments = {
2323
collection: Collection
2424
depth?: number
2525
overrideAccess?: boolean
26+
overrideLock?: boolean
2627
req: PayloadRequest
2728
showHiddenFields?: boolean
2829
where: Where
@@ -65,6 +66,7 @@ export const deleteOperation = async <TSlug extends CollectionSlug>(
6566
collection: { config: collectionConfig },
6667
depth,
6768
overrideAccess,
69+
overrideLock,
6870
req: {
6971
fallbackLocale,
7072
locale,
@@ -126,6 +128,7 @@ export const deleteOperation = async <TSlug extends CollectionSlug>(
126128
id,
127129
collectionSlug: collectionConfig.slug,
128130
lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`,
131+
overrideLock,
129132
req,
130133
})
131134

packages/payload/src/collections/operations/deleteByID.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type Arguments = {
2121
depth?: number
2222
id: number | string
2323
overrideAccess?: boolean
24+
overrideLock?: boolean
2425
req: PayloadRequest
2526
showHiddenFields?: boolean
2627
}
@@ -58,6 +59,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug>(
5859
collection: { config: collectionConfig },
5960
depth,
6061
overrideAccess,
62+
overrideLock,
6163
req: {
6264
fallbackLocale,
6365
locale,
@@ -118,6 +120,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug>(
118120
id,
119121
collectionSlug: collectionConfig.slug,
120122
lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`,
123+
overrideLock,
121124
req,
122125
})
123126

packages/payload/src/collections/operations/local/delete.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type BaseOptions<TSlug extends CollectionSlug> = {
1717
fallbackLocale?: TypedLocale
1818
locale?: TypedLocale
1919
overrideAccess?: boolean
20+
overrideLock?: boolean
2021
req?: PayloadRequest
2122
showHiddenFields?: boolean
2223
user?: Document
@@ -55,6 +56,7 @@ async function deleteLocal<TSlug extends CollectionSlug>(
5556
collection: collectionSlug,
5657
depth,
5758
overrideAccess = true,
59+
overrideLock,
5860
showHiddenFields,
5961
where,
6062
} = options
@@ -72,6 +74,7 @@ async function deleteLocal<TSlug extends CollectionSlug>(
7274
collection,
7375
depth,
7476
overrideAccess,
77+
overrideLock,
7578
req: await createLocalReq(options, payload),
7679
showHiddenFields,
7780
where,

packages/payload/src/collections/operations/local/update.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type BaseOptions<TSlug extends CollectionSlug> = {
3030
filePath?: string
3131
locale?: TypedLocale
3232
overrideAccess?: boolean
33+
overrideLock?: boolean
3334
overwriteExistingFiles?: boolean
3435
publishSpecificLocale?: string
3536
req?: PayloadRequest
@@ -75,6 +76,7 @@ async function updateLocal<TSlug extends CollectionSlug>(
7576
file,
7677
filePath,
7778
overrideAccess = true,
79+
overrideLock,
7880
overwriteExistingFiles = false,
7981
publishSpecificLocale,
8082
showHiddenFields,
@@ -100,6 +102,7 @@ async function updateLocal<TSlug extends CollectionSlug>(
100102
depth,
101103
draft,
102104
overrideAccess,
105+
overrideLock,
103106
overwriteExistingFiles,
104107
payload,
105108
publishSpecificLocale,

packages/payload/src/collections/operations/update.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
4141
disableVerificationEmail?: boolean
4242
draft?: boolean
4343
overrideAccess?: boolean
44+
overrideLock?: boolean
4445
overwriteExistingFiles?: boolean
4546
req: PayloadRequest
4647
showHiddenFields?: boolean
@@ -78,6 +79,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
7879
depth,
7980
draft: draftArg = false,
8081
overrideAccess,
82+
overrideLock,
8183
overwriteExistingFiles = false,
8284
req: {
8385
fallbackLocale,
@@ -186,6 +188,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
186188
id,
187189
collectionSlug: collectionConfig.slug,
188190
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
191+
overrideLock,
189192
req,
190193
})
191194

packages/payload/src/collections/operations/updateByID.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
4343
draft?: boolean
4444
id: number | string
4545
overrideAccess?: boolean
46+
overrideLock?: boolean
4647
overwriteExistingFiles?: boolean
4748
publishSpecificLocale?: string
4849
req: PayloadRequest
@@ -86,6 +87,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
8687
depth,
8788
draft: draftArg = false,
8889
overrideAccess,
90+
overrideLock,
8991
overwriteExistingFiles = false,
9092
publishSpecificLocale,
9193
req: {
@@ -150,6 +152,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
150152
id,
151153
collectionSlug: collectionConfig.slug,
152154
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
155+
overrideLock,
153156
req,
154157
})
155158

packages/payload/src/errors/Locked.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import httpStatus from 'http-status'
2+
3+
import { APIError } from './APIError.js'
4+
5+
export class Locked extends APIError {
6+
constructor(message: string) {
7+
super(message, httpStatus.LOCKED)
8+
}
9+
}

packages/payload/src/errors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { Forbidden } from './Forbidden.js'
1010
export { InvalidConfiguration } from './InvalidConfiguration.js'
1111
export { InvalidFieldName } from './InvalidFieldName.js'
1212
export { InvalidFieldRelationship } from './InvalidFieldRelationship.js'
13+
export { Locked } from './Locked.js'
1314
export { LockedAuth } from './LockedAuth.js'
1415
export { MissingCollectionLabel } from './MissingCollectionLabel.js'
1516
export { MissingEditorProp } from './MissingEditorProp.js'

0 commit comments

Comments
 (0)