feat: add hooks to restoreVersion collection operation (#13333)

Adds missing hooks to the restoreVersion operation.
- beforeOperation
- beforeValidate - Fields
- beforeValidate - Collection
- beforeChange - Collection
- beforeChange - Fields
- afterOperation
This commit is contained in:
Jarrod Flesch
2025-07-30 21:25:53 -04:00
committed by GitHub
parent df91321f4a
commit 9031f3bf23
7 changed files with 172 additions and 17 deletions

View File

@@ -85,6 +85,7 @@ export type HookOperationType =
| 'readDistinct'
| 'refresh'
| 'resetPassword'
| 'restoreVersion'
| 'update'
type CreateOrUpdateOperation = Extract<HookOperationType, 'create' | 'update'>

View File

@@ -291,6 +291,7 @@ export const createOperation = async <
autosave,
collection: collectionConfig,
docWithLocales: result,
operation: 'create',
payload,
publishSpecificLocale,
req,

View File

@@ -10,15 +10,23 @@ import { combineQueries } from '../../database/combineQueries.js'
import { APIError, Forbidden, NotFound } from '../../errors/index.js'
import { afterChange } from '../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion.js'
import { saveVersion } from '../../versions/saveVersion.js'
import { buildAfterOperation } from './utils.js'
export type Arguments = {
collection: Collection
currentDepth?: number
depth?: number
disableErrors?: boolean
disableTransaction?: boolean
draft?: boolean
id: number | string
overrideAccess?: boolean
@@ -35,7 +43,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
id,
collection: { config: collectionConfig },
depth,
draft,
draft: draftArg = false,
overrideAccess = false,
populate,
req,
@@ -45,6 +53,25 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
} = args
try {
const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation) {
args =
(await hook({
args,
collection: args.collection.config,
context: args.req.context,
operation: 'restoreVersion',
req: args.req,
})) || args
}
}
if (!id) {
throw new APIError('Missing ID of version to restore.', httpStatus.BAD_REQUEST)
}
@@ -68,7 +95,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
throw new NotFound(req.t)
}
const parentDocID = rawVersion.parent
const { parent: parentDocID, version: versionToRestoreWithLocales } = rawVersion
// /////////////////////////////////////
// Access
@@ -90,6 +117,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
where: combineQueries({ id: { equals: parentDocID } }, accessResults),
}
// Get the document from the non versioned collection
const doc = await req.payload.db.findOne(findOneArgs)
if (!doc && !hasWherePolicy) {
@@ -109,7 +137,6 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// /////////////////////////////////////
// fetch previousDoc
// /////////////////////////////////////
const prevDocWithLocales = await getLatestCollectionVersion({
id: parentDocID,
config: collectionConfig,
@@ -118,6 +145,109 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
req,
})
// originalDoc with hoisted localized data
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: deepCopyObjectSimple(prevDocWithLocales),
draft: draftArg,
fallbackLocale: null,
global: null,
locale: locale!,
overrideAccess: true,
req,
showHiddenFields: true,
})
// version data with hoisted localized data
const prevVersionDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: deepCopyObjectSimple(versionToRestoreWithLocales),
draft: draftArg,
fallbackLocale: null,
global: null,
locale: locale!,
overrideAccess: true,
req,
showHiddenFields: true,
})
let data = deepCopyObjectSimple(prevVersionDoc)
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
data = await beforeValidate({
id: parentDocID,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
global: null,
operation: 'update',
overrideAccess,
req,
})
// /////////////////////////////////////
// beforeValidate - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeValidate?.length) {
for (const hook of collectionConfig.hooks.beforeValidate) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}
}
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeChange?.length) {
for (const hook of collectionConfig.hooks.beforeChange) {
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}
}
// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
let result = await beforeChange({
id: parentDocID,
collection: collectionConfig,
context: req.context,
data: { ...data, id: parentDocID },
doc: originalDoc,
docWithLocales: versionToRestoreWithLocales,
global: null,
operation: 'update',
overrideAccess,
req,
skipValidation:
draftArg && collectionConfig.versions.drafts && !collectionConfig.versions.drafts.validate,
})
// /////////////////////////////////////
// Update
// /////////////////////////////////////
@@ -128,10 +258,10 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
select: incomingSelect,
})
let result = await req.payload.db.updateOne({
result = await req.payload.db.updateOne({
id: parentDocID,
collection: collectionConfig.slug,
data: rawVersion.version,
data: result,
req,
select,
})
@@ -140,18 +270,16 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// Save `previousDoc` as a version after restoring
// /////////////////////////////////////
const prevVersion = { ...prevDocWithLocales }
delete prevVersion.id
await payload.db.createVersion({
result = await saveVersion({
id: parentDocID,
autosave: false,
collectionSlug: collectionConfig.slug,
createdAt: prevVersion.createdAt,
parent: parentDocID,
collection: collectionConfig,
docWithLocales: result,
draft: draftArg,
operation: 'restoreVersion',
payload,
req,
updatedAt: new Date().toISOString(),
versionData: draft ? { ...rawVersion.version, _status: 'draft' } : rawVersion.version,
select,
})
// /////////////////////////////////////
@@ -225,6 +353,21 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
}
}
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation({
args,
collection: collectionConfig,
operation: 'restoreVersion',
result,
})
if (shouldCommit) {
await commitTransaction(req)
}
return result
} catch (error: unknown) {
await killTransaction(req)

View File

@@ -314,6 +314,7 @@ export const updateDocument = async <
collection: collectionConfig,
docWithLocales: result,
draft: shouldSaveDraft,
operation: 'update',
payload,
publishSpecificLocale,
req,

View File

@@ -2,7 +2,7 @@ import type { forgotPasswordOperation } from '../../auth/operations/forgotPasswo
import type { loginOperation } from '../../auth/operations/login.js'
import type { refreshOperation } from '../../auth/operations/refresh.js'
import type { resetPasswordOperation } from '../../auth/operations/resetPassword.js'
import type { CollectionSlug } from '../../index.js'
import type { CollectionSlug, restoreVersionOperation } from '../../index.js'
import type { PayloadRequest } from '../../types/index.js'
import type { SanitizedCollectionConfig, SelectFromCollectionSlug } from '../config/types.js'
import type { countOperation } from './count.js'
@@ -36,6 +36,7 @@ export type AfterOperationMap<TOperationGeneric extends CollectionSlug> = {
login: typeof loginOperation<TOperationGeneric>
refresh: typeof refreshOperation
resetPassword: typeof resetPasswordOperation<TOperationGeneric>
restoreVersion: typeof restoreVersionOperation
update: typeof updateOperation<TOperationGeneric, SelectFromCollectionSlug<TOperationGeneric>>
updateByID: typeof updateByIDOperation<
TOperationGeneric,
@@ -108,6 +109,11 @@ export type AfterOperationArg<TOperationGeneric extends CollectionSlug> = {
operation: 'resetPassword'
result: Awaited<ReturnType<AfterOperationMap<TOperationGeneric>['resetPassword']>>
}
| {
args: Parameters<AfterOperationMap<TOperationGeneric>['restoreVersion']>[0]
operation: 'restoreVersion'
result: Awaited<ReturnType<AfterOperationMap<TOperationGeneric>['restoreVersion']>>
}
| {
args: Parameters<AfterOperationMap<TOperationGeneric>['update']>[0]
operation: 'update'

View File

@@ -282,6 +282,7 @@ export const updateOperation = async <
docWithLocales: result,
draft: shouldSaveDraft,
global: globalConfig,
operation: 'update',
payload,
publishSpecificLocale,
req,

View File

@@ -16,6 +16,7 @@ type Args = {
draft?: boolean
global?: SanitizedGlobalConfig
id?: number | string
operation?: 'create' | 'restoreVersion' | 'update'
payload: Payload
publishSpecificLocale?: string
req?: PayloadRequest
@@ -30,6 +31,7 @@ export const saveVersion = async ({
docWithLocales: doc,
draft,
global,
operation,
payload,
publishSpecificLocale,
req,
@@ -126,7 +128,7 @@ export const saveVersion = async ({
const createVersionArgs = {
autosave: Boolean(autosave),
collectionSlug: undefined as string | undefined,
createdAt: now,
createdAt: operation === 'restoreVersion' ? versionData.createdAt : now,
globalSlug: undefined as string | undefined,
parent: collection ? id : undefined,
publishedLocale: publishSpecificLocale || undefined,