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
This commit is contained in:
Patrik
2024-09-19 17:07:02 -04:00
committed by GitHub
parent 879f690161
commit 9c3f863ad2
15 changed files with 307 additions and 57 deletions

View File

@@ -58,3 +58,22 @@ export const Posts: CollectionConfig = {
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. 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.
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal. Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
#### Overriding Locks
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.
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`.
```ts
const result = await payload.update({
collection: 'posts',
id: '123',
data: {
title: 'New title',
},
overrideLock: false, // Enforces the document lock, preventing updates if the document is locked
})
```
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.

View File

@@ -77,17 +77,18 @@ 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. |
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. | | `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use 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. | | `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | | `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).|
| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | | `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. |
| `pagination` | Set to false to return all documents and avoid querying for document counts. | | `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. | | `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. |
@@ -206,6 +207,7 @@ const result = await payload.update({
fallbackLocale: false, fallbackLocale: false,
user: dummyUser, user: dummyUser,
overrideAccess: false, overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true, showHiddenFields: true,
// If your collection supports uploads, you can upload // If your collection supports uploads, you can upload
@@ -244,6 +246,7 @@ const result = await payload.update({
fallbackLocale: false, fallbackLocale: false,
user: dummyUser, user: dummyUser,
overrideAccess: false, overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true, showHiddenFields: true,
// If your collection supports uploads, you can upload // If your collection supports uploads, you can upload
@@ -270,6 +273,7 @@ const result = await payload.delete({
fallbackLocale: false, fallbackLocale: false,
user: dummyUser, user: dummyUser,
overrideAccess: false, overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true, showHiddenFields: true,
}) })
``` ```
@@ -293,6 +297,7 @@ const result = await payload.delete({
fallbackLocale: false, fallbackLocale: false,
user: dummyUser, user: dummyUser,
overrideAccess: false, overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true, showHiddenFields: true,
}) })
``` ```
@@ -429,6 +434,7 @@ const result = await payload.updateGlobal({
fallbackLocale: false, fallbackLocale: false,
user: dummyUser, user: dummyUser,
overrideAccess: false, overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true, showHiddenFields: true,
}) })
``` ```

View File

@@ -23,6 +23,7 @@ export type Arguments = {
collection: Collection collection: Collection
depth?: number depth?: number
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
req: PayloadRequest req: PayloadRequest
showHiddenFields?: boolean showHiddenFields?: boolean
where: Where where: Where
@@ -65,6 +66,7 @@ export const deleteOperation = async <TSlug extends CollectionSlug>(
collection: { config: collectionConfig }, collection: { config: collectionConfig },
depth, depth,
overrideAccess, overrideAccess,
overrideLock,
req: { req: {
fallbackLocale, fallbackLocale,
locale, locale,
@@ -126,6 +128,7 @@ export const deleteOperation = async <TSlug extends CollectionSlug>(
id, id,
collectionSlug: collectionConfig.slug, collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`, lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`,
overrideLock,
req, req,
}) })

View File

@@ -21,6 +21,7 @@ export type Arguments = {
depth?: number depth?: number
id: number | string id: number | string
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
req: PayloadRequest req: PayloadRequest
showHiddenFields?: boolean showHiddenFields?: boolean
} }
@@ -58,6 +59,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug>(
collection: { config: collectionConfig }, collection: { config: collectionConfig },
depth, depth,
overrideAccess, overrideAccess,
overrideLock,
req: { req: {
fallbackLocale, fallbackLocale,
locale, locale,
@@ -118,6 +120,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug>(
id, id,
collectionSlug: collectionConfig.slug, collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`, lockErrorMessage: `Document with ID ${id} is currently locked and cannot be deleted.`,
overrideLock,
req, req,
}) })

View File

@@ -17,6 +17,7 @@ export type BaseOptions<TSlug extends CollectionSlug> = {
fallbackLocale?: TypedLocale fallbackLocale?: TypedLocale
locale?: TypedLocale locale?: TypedLocale
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
req?: PayloadRequest req?: PayloadRequest
showHiddenFields?: boolean showHiddenFields?: boolean
user?: Document user?: Document
@@ -55,6 +56,7 @@ async function deleteLocal<TSlug extends CollectionSlug>(
collection: collectionSlug, collection: collectionSlug,
depth, depth,
overrideAccess = true, overrideAccess = true,
overrideLock,
showHiddenFields, showHiddenFields,
where, where,
} = options } = options
@@ -72,6 +74,7 @@ async function deleteLocal<TSlug extends CollectionSlug>(
collection, collection,
depth, depth,
overrideAccess, overrideAccess,
overrideLock,
req: await createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,
where, where,

View File

@@ -30,6 +30,7 @@ export type BaseOptions<TSlug extends CollectionSlug> = {
filePath?: string filePath?: string
locale?: TypedLocale locale?: TypedLocale
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
overwriteExistingFiles?: boolean overwriteExistingFiles?: boolean
publishSpecificLocale?: string publishSpecificLocale?: string
req?: PayloadRequest req?: PayloadRequest
@@ -75,6 +76,7 @@ async function updateLocal<TSlug extends CollectionSlug>(
file, file,
filePath, filePath,
overrideAccess = true, overrideAccess = true,
overrideLock,
overwriteExistingFiles = false, overwriteExistingFiles = false,
publishSpecificLocale, publishSpecificLocale,
showHiddenFields, showHiddenFields,
@@ -100,6 +102,7 @@ async function updateLocal<TSlug extends CollectionSlug>(
depth, depth,
draft, draft,
overrideAccess, overrideAccess,
overrideLock,
overwriteExistingFiles, overwriteExistingFiles,
payload, payload,
publishSpecificLocale, publishSpecificLocale,

View File

@@ -41,6 +41,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
disableVerificationEmail?: boolean disableVerificationEmail?: boolean
draft?: boolean draft?: boolean
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
overwriteExistingFiles?: boolean overwriteExistingFiles?: boolean
req: PayloadRequest req: PayloadRequest
showHiddenFields?: boolean showHiddenFields?: boolean
@@ -78,6 +79,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
depth, depth,
draft: draftArg = false, draft: draftArg = false,
overrideAccess, overrideAccess,
overrideLock,
overwriteExistingFiles = false, overwriteExistingFiles = false,
req: { req: {
fallbackLocale, fallbackLocale,
@@ -186,6 +188,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
id, id,
collectionSlug: collectionConfig.slug, collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`, lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
overrideLock,
req, req,
}) })

View File

@@ -43,6 +43,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
draft?: boolean draft?: boolean
id: number | string id: number | string
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
overwriteExistingFiles?: boolean overwriteExistingFiles?: boolean
publishSpecificLocale?: string publishSpecificLocale?: string
req: PayloadRequest req: PayloadRequest
@@ -86,6 +87,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
depth, depth,
draft: draftArg = false, draft: draftArg = false,
overrideAccess, overrideAccess,
overrideLock,
overwriteExistingFiles = false, overwriteExistingFiles = false,
publishSpecificLocale, publishSpecificLocale,
req: { req: {
@@ -150,6 +152,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
id, id,
collectionSlug: collectionConfig.slug, collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`, lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
overrideLock,
req, req,
}) })

View File

@@ -0,0 +1,9 @@
import httpStatus from 'http-status'
import { APIError } from './APIError.js'
export class Locked extends APIError {
constructor(message: string) {
super(message, httpStatus.LOCKED)
}
}

View File

@@ -10,6 +10,7 @@ export { Forbidden } from './Forbidden.js'
export { InvalidConfiguration } from './InvalidConfiguration.js' export { InvalidConfiguration } from './InvalidConfiguration.js'
export { InvalidFieldName } from './InvalidFieldName.js' export { InvalidFieldName } from './InvalidFieldName.js'
export { InvalidFieldRelationship } from './InvalidFieldRelationship.js' export { InvalidFieldRelationship } from './InvalidFieldRelationship.js'
export { Locked } from './Locked.js'
export { LockedAuth } from './LockedAuth.js' export { LockedAuth } from './LockedAuth.js'
export { MissingCollectionLabel } from './MissingCollectionLabel.js' export { MissingCollectionLabel } from './MissingCollectionLabel.js'
export { MissingEditorProp } from './MissingEditorProp.js' export { MissingEditorProp } from './MissingEditorProp.js'

View File

@@ -16,6 +16,7 @@ export type Options<TSlug extends GlobalSlug> = {
fallbackLocale?: TypedLocale fallbackLocale?: TypedLocale
locale?: TypedLocale locale?: TypedLocale
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
publishSpecificLocale?: TypedLocale publishSpecificLocale?: TypedLocale
req?: PayloadRequest req?: PayloadRequest
showHiddenFields?: boolean showHiddenFields?: boolean
@@ -33,6 +34,7 @@ export default async function updateLocal<TSlug extends GlobalSlug>(
depth, depth,
draft, draft,
overrideAccess = true, overrideAccess = true,
overrideLock,
publishSpecificLocale, publishSpecificLocale,
showHiddenFields, showHiddenFields,
} = options } = options
@@ -50,6 +52,7 @@ export default async function updateLocal<TSlug extends GlobalSlug>(
draft, draft,
globalConfig, globalConfig,
overrideAccess, overrideAccess,
overrideLock,
publishSpecificLocale, publishSpecificLocale,
req: await createLocalReq(options, payload), req: await createLocalReq(options, payload),
showHiddenFields, showHiddenFields,

View File

@@ -24,6 +24,7 @@ type Args<TSlug extends GlobalSlug> = {
draft?: boolean draft?: boolean
globalConfig: SanitizedGlobalConfig globalConfig: SanitizedGlobalConfig
overrideAccess?: boolean overrideAccess?: boolean
overrideLock?: boolean
publishSpecificLocale?: string publishSpecificLocale?: string
req: PayloadRequest req: PayloadRequest
showHiddenFields?: boolean showHiddenFields?: boolean
@@ -44,6 +45,7 @@ export const updateOperation = async <TSlug extends GlobalSlug>(
draft: draftArg, draft: draftArg,
globalConfig, globalConfig,
overrideAccess, overrideAccess,
overrideLock,
publishSpecificLocale, publishSpecificLocale,
req: { fallbackLocale, locale, payload }, req: { fallbackLocale, locale, payload },
req, req,
@@ -121,6 +123,7 @@ export const updateOperation = async <TSlug extends GlobalSlug>(
await checkDocumentLockStatus({ await checkDocumentLockStatus({
globalSlug: slug, globalSlug: slug,
lockErrorMessage: `Global with slug "${slug}" is currently locked by another user and cannot be updated.`, lockErrorMessage: `Global with slug "${slug}" is currently locked by another user and cannot be updated.`,
overrideLock,
req, req,
}) })

View File

@@ -817,6 +817,7 @@ export {
InvalidConfiguration, InvalidConfiguration,
InvalidFieldName, InvalidFieldName,
InvalidFieldRelationship, InvalidFieldRelationship,
Locked,
LockedAuth, LockedAuth,
MissingCollectionLabel, MissingCollectionLabel,
MissingEditorProp, MissingEditorProp,

View File

@@ -2,7 +2,7 @@ import type { TypeWithID } from '../collections/config/types.js'
import type { PaginatedDocs } from '../database/types.js' import type { PaginatedDocs } from '../database/types.js'
import type { JsonObject, PayloadRequest } from '../types/index.js' import type { JsonObject, PayloadRequest } from '../types/index.js'
import { APIError } from '../errors/index.js' import { Locked } from '../errors/index.js'
type CheckDocumentLockStatusArgs = { type CheckDocumentLockStatusArgs = {
collectionSlug?: string collectionSlug?: string
@@ -10,6 +10,7 @@ type CheckDocumentLockStatusArgs = {
id?: number | string id?: number | string
lockDurationDefault?: number lockDurationDefault?: number
lockErrorMessage?: string lockErrorMessage?: string
overrideLock?: boolean
req: PayloadRequest req: PayloadRequest
} }
@@ -19,6 +20,7 @@ export const checkDocumentLockStatus = async ({
globalSlug, globalSlug,
lockDurationDefault = 300, // Default 5 minutes in seconds lockDurationDefault = 300, // Default 5 minutes in seconds
lockErrorMessage, lockErrorMessage,
overrideLock = true,
req, req,
}: CheckDocumentLockStatusArgs): Promise<void> => { }: CheckDocumentLockStatusArgs): Promise<void> => {
const { payload } = req const { payload } = req
@@ -30,11 +32,6 @@ export const checkDocumentLockStatus = async ({
const isLockingEnabled = lockDocumentsProp !== false const isLockingEnabled = lockDocumentsProp !== false
// If lockDocuments is explicitly set to false, skip the lock logic and return early
if (isLockingEnabled === false) {
return
}
let lockedDocumentQuery = {} let lockedDocumentQuery = {}
if (collectionSlug) { if (collectionSlug) {
@@ -50,45 +47,47 @@ export const checkDocumentLockStatus = async ({
throw new Error('Either collectionSlug or globalSlug must be provided.') throw new Error('Either collectionSlug or globalSlug must be provided.')
} }
const defaultLockErrorMessage = collectionSlug // Only perform lock checks if overrideLock is false and locking is enabled
? `Document with ID ${id} is currently locked by another user and cannot be modified.` if (!overrideLock && isLockingEnabled !== false) {
: `Global document with slug "${globalSlug}" is currently locked by another user and cannot be modified.` const defaultLockErrorMessage = collectionSlug
? `Document with ID ${id} is currently locked by another user and cannot be modified.`
: `Global document with slug "${globalSlug}" is currently locked by another user and cannot be modified.`
const finalLockErrorMessage = lockErrorMessage || defaultLockErrorMessage const finalLockErrorMessage = lockErrorMessage || defaultLockErrorMessage
const lockedDocumentResult: PaginatedDocs<JsonObject & TypeWithID> = await payload.find({ const lockedDocumentResult: PaginatedDocs<JsonObject & TypeWithID> = await payload.find({
collection: 'payload-locked-documents', collection: 'payload-locked-documents',
depth: 1, depth: 1,
limit: 1, limit: 1,
pagination: false, pagination: false,
req, req,
sort: '-updatedAt', sort: '-updatedAt',
where: lockedDocumentQuery, where: lockedDocumentQuery,
}) })
// If there's a locked document, check lock conditions // If there's a locked document, check lock conditions
const lockedDoc = lockedDocumentResult?.docs[0] const lockedDoc = lockedDocumentResult?.docs[0]
if (!lockedDoc) { if (lockedDoc) {
return const lastEditedAt = new Date(lockedDoc?.editedAt)
} const now = new Date()
const lastEditedAt = new Date(lockedDoc?.editedAt) const lockDuration =
const now = new Date() typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
const lockDuration = const lockDurationInMilliseconds = lockDuration * 1000
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault const currentUserId = req.user?.id
const lockDurationInMilliseconds = lockDuration * 1000 // document is locked by another user and the lock hasn't expired
const currentUserId = req.user?.id if (
lockedDoc.user?.value?.id !== currentUserId &&
// document is locked by another user and the lock hasn't expired now.getTime() - lastEditedAt.getTime() <= lockDurationInMilliseconds
if ( ) {
lockedDoc.user?.value?.id !== currentUserId && throw new Locked(finalLockErrorMessage)
now.getTime() - lastEditedAt.getTime() <= lockDurationInMilliseconds }
) { }
throw new APIError(finalLockErrorMessage)
} }
// Perform the delete operation regardless of overrideLock status
await payload.db.deleteMany({ await payload.db.deleteMany({
collection: 'payload-locked-documents', collection: 'payload-locked-documents',
req, req,

View File

@@ -1,5 +1,5 @@
import path from 'path' import path from 'path'
import { APIError, NotFound, type Payload } from 'payload' import { Locked, NotFound, type Payload } from 'payload'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js' import type { NextRESTClient } from '../helpers/NextRESTClient.js'
@@ -156,6 +156,7 @@ describe('Locked documents', () => {
data: { data: {
text: 'updated post 2', text: 'updated post 2',
}, },
overrideLock: false,
id: newPost2.id, id: newPost2.id,
}) })
@@ -209,6 +210,7 @@ describe('Locked documents', () => {
data: { data: {
globalText: 'global text 2', globalText: 'global text 2',
}, },
overrideLock: false,
slug: menuSlug, slug: menuSlug,
}) })
@@ -269,15 +271,21 @@ describe('Locked documents', () => {
data: { data: {
text: 'updated post', text: 'updated post',
}, },
overrideLock: false, // necessary to trigger the lock check
id: newPost.id, id: newPost.id,
}) })
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(APIError) expect(error).toBeInstanceOf(Locked)
expect(error.message).toMatch(/currently locked by another user and cannot be updated/) expect(error.message).toMatch(/currently locked by another user and cannot be updated/)
} }
const updatedPost = await payload.findByID({
collection: postsSlug,
id: newPost.id,
})
// Should not allow update - expect data not to change // Should not allow update - expect data not to change
expect(newPost.text).toEqual('some post') expect(updatedPost.text).toEqual('some post')
}) })
it('should not allow update of locked document - global', async () => { it('should not allow update of locked document - global', async () => {
@@ -300,15 +308,20 @@ describe('Locked documents', () => {
data: { data: {
globalText: 'updated global text', globalText: 'updated global text',
}, },
overrideLock: false, // necessary to trigger the lock check
slug: menuSlug, slug: menuSlug,
}) })
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(APIError) expect(error).toBeInstanceOf(Locked)
expect(error.message).toMatch(/currently locked by another user and cannot be updated/) expect(error.message).toMatch(/currently locked by another user and cannot be updated/)
} }
const updatedGlobalMenu = await payload.findGlobal({
slug: menuSlug,
})
// Should not allow update - expect data not to change // Should not allow update - expect data not to change
expect(menu.globalText).toEqual('global text') expect(updatedGlobalMenu.globalText).toEqual('global text 2')
}) })
// Try to delete locked document (collection) // Try to delete locked document (collection)
@@ -341,11 +354,21 @@ describe('Locked documents', () => {
await payload.delete({ await payload.delete({
collection: postsSlug, collection: postsSlug,
id: newPost3.id, id: newPost3.id,
overrideLock: false, // necessary to trigger the lock check
}) })
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(APIError) expect(error).toBeInstanceOf(Locked)
expect(error.message).toMatch(/currently locked and cannot be deleted/) expect(error.message).toMatch(/currently locked and cannot be deleted/)
} }
const findPostDocs = await payload.find({
collection: postsSlug,
where: {
id: { equals: newPost3.id },
},
})
expect(findPostDocs.docs).toHaveLength(1)
}) })
it('should allow delete of stale locked document - collection', async () => { it('should allow delete of stale locked document - collection', async () => {
@@ -381,6 +404,7 @@ describe('Locked documents', () => {
await payload.delete({ await payload.delete({
collection: postsSlug, collection: postsSlug,
id: newPost4.id, id: newPost4.id,
overrideLock: false,
}) })
const findPostDocs = await payload.find({ const findPostDocs = await payload.find({
@@ -411,4 +435,171 @@ describe('Locked documents', () => {
expect(docsFromLocksCollection.docs).toHaveLength(0) expect(docsFromLocksCollection.docs).toHaveLength(0)
}) })
it('should allow update of locked document w/ overrideLock flag - collection', async () => {
const newPost5 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 5',
},
})
// Give locking ownership to another user
const lockedDocInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
editedAt: new Date().toISOString(),
user: {
relationTo: 'users',
value: user2.id,
},
document: {
relationTo: 'posts',
value: newPost5.id,
},
globalSlug: undefined,
},
})
const updateLockedDoc = await payload.update({
collection: postsSlug,
data: {
text: 'updated post 5',
},
id: newPost5.id,
overrideLock: true,
})
// Should allow update since using overrideLock flag
expect(updateLockedDoc.text).toEqual('updated post 5')
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedDocInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedDocInstance.id },
},
})
// Updating a document with the local API should not keep a stored doc
// in the payload-locked-documents collection
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should allow update of locked document w/ overrideLock flag - global', async () => {
// Give locking ownership to another user
const lockedGlobalInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
editedAt: new Date().toISOString(),
globalSlug: menuSlug,
user: {
relationTo: 'users',
value: user2.id,
},
document: undefined,
},
})
const updateGlobalLockedDoc = await payload.updateGlobal({
data: {
globalText: 'updated global text 2',
},
slug: menuSlug,
overrideLock: true,
})
// Should allow update since using overrideLock flag
expect(updateGlobalLockedDoc.globalText).toEqual('updated global text 2')
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedGlobalInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedGlobalInstance.id },
},
})
// Updating a document with the local API should not keep a stored doc
// in the payload-locked-documents collection
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
it('should allow delete of locked document w/ overrideLock flag - collection', async () => {
const newPost6 = await payload.create({
collection: postsSlug,
data: {
text: 'new post 6',
},
})
// Give locking ownership to another user
const lockedDocInstance = await payload.create({
collection: lockedDocumentCollection,
data: {
editedAt: new Date().toISOString(),
user: {
relationTo: 'users',
value: user2.id,
},
document: {
relationTo: 'posts',
value: newPost6.id,
},
globalSlug: undefined,
},
})
await payload.delete({
collection: postsSlug,
id: newPost6.id,
overrideLock: true,
})
const findPostDocs = await payload.find({
collection: postsSlug,
where: {
id: { equals: newPost6.id },
},
})
expect(findPostDocs.docs).toHaveLength(0)
// Check to make sure the document does not exist in payload-locked-documents anymore
try {
await payload.findByID({
collection: lockedDocumentCollection,
id: lockedDocInstance.id,
})
} catch (error) {
expect(error).toBeInstanceOf(NotFound)
}
const docsFromLocksCollection = await payload.find({
collection: lockedDocumentCollection,
where: {
id: { equals: lockedDocInstance.id },
},
})
expect(docsFromLocksCollection.docs).toHaveLength(0)
})
}) })