Skip to content

Commit 4b4d61c

Browse files
fix: restore falling back to current document values for undefined fields (#16272)
### What? **Behavior prior to this change:** When restoring a version, `beforeValidate` field processing backfilled `undefined` fields from the current document data into the to-be-restored document data. This behavior is correct for partial updates (preserving unmodified fields), but incorrect for restore operations, where the version snapshot is the complete desired state. This is the current behavior on `main`: Note how the `Radio` field is not set in the first version, but set to `Test en` after the restore of said first version. https://github.com/user-attachments/assets/56467956-7caa-4363-989e-bcd1a663ce7a **Behavior with this change:** The desired fallback behavior during partial updates is retained, but the to-be-restored document no longer runs this fallback behavior. The restored document equals the document that the user triggered the restore for. Note how now the `Radio` field is not set again after the restore, just like it was when the first version was created: https://github.com/user-attachments/assets/0b5833b5-3694-4b10-9a11-f25f5c1a3b24 ### Why? When a user restores an old document version, the user's expectation is that the old document is what they are going to get after successful restore. It is unexpected if the document is a "frankenstein" version of the old document and the current document, especially since the user has no way to see the resulting document before the restore and can't know which attributes might be set from the current document as fallback value. ### How? `getFallbackValue` is skipped during restore operations denoted with the new request context `isRestoringVersion = true`. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1214068985579207 --------- Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
1 parent cb97614 commit 4b4d61c

4 files changed

Lines changed: 74 additions & 2 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ export const restoreVersionOperation = async <
180180
// beforeValidate - Fields
181181
// /////////////////////////////////////
182182

183+
req.context.isRestoringVersion = true
184+
183185
const reqWithValidationLocale = Object.assign(Object.create(req), req, {
184186
fallbackLocale: null,
185187
locale: validationLocale,

packages/payload/src/fields/hooks/beforeValidate/promise.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ export const promise = async <T>({
278278
executed: false,
279279
value: undefined,
280280
}
281-
if (typeof siblingData[field.name!] === 'undefined') {
281+
if (typeof siblingData[field.name!] === 'undefined' && !req.context?.isRestoringVersion) {
282282
fallbackResult.value = await getFallbackValue({ field, req, siblingDoc })
283283
fallbackResult.executed = true
284284
}
@@ -334,7 +334,7 @@ export const promise = async <T>({
334334
}
335335
}
336336

337-
if (typeof siblingData[field.name!] === 'undefined') {
337+
if (typeof siblingData[field.name!] === 'undefined' && !req.context?.isRestoringVersion) {
338338
siblingData[field.name!] = !fallbackResult.executed
339339
? await getFallbackValue({ field, req, siblingDoc })
340340
: fallbackResult.value

packages/payload/src/globals/operations/restoreVersion.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
9292
req,
9393
})
9494

95+
req.context.isRestoringVersion = true
96+
9597
// /////////////////////////////////////
9698
// Update global
9799
// /////////////////////////////////////

test/versions/int.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,74 @@ describe('Versions', () => {
784784
})
785785
expect(latestDraft.blocksField).toHaveLength(0)
786786
})
787+
788+
it('should not copy current document fields into restored version', async () => {
789+
// Create doc with a block (only text set), leaving radio/select/localized unset
790+
const doc = await payload.create({
791+
collection: draftCollectionSlug,
792+
data: {
793+
blocksField: [
794+
{
795+
blockType: 'block',
796+
text: 'original-text',
797+
},
798+
],
799+
description: 'initial description',
800+
title: 'leak test',
801+
},
802+
draft: true,
803+
})
804+
805+
const blockId = doc.blocksField?.[0]!.id
806+
807+
// Update doc to set radio, select, and block localized field
808+
await payload.update({
809+
id: doc.id,
810+
collection: draftCollectionSlug,
811+
data: {
812+
blocksField: [
813+
{
814+
id: blockId,
815+
blockType: 'block',
816+
localized: 'leaked-value',
817+
text: 'original-text',
818+
},
819+
],
820+
description: 'updated description',
821+
radio: 'test',
822+
select: ['test1'],
823+
title: 'leak test',
824+
},
825+
draft: true,
826+
})
827+
828+
// Find versions and restore the original (oldest) version
829+
const versions = await payload.findVersions({
830+
collection: draftCollectionSlug,
831+
where: { parent: { equals: doc.id } },
832+
})
833+
834+
const originalVersion = versions.docs[versions.docs.length - 1]
835+
836+
await payload.restoreVersion({
837+
id: originalVersion!.id,
838+
collection: draftCollectionSlug,
839+
})
840+
841+
const restored = await payload.findByID({
842+
id: doc.id,
843+
collection: draftCollectionSlug,
844+
draft: true,
845+
})
846+
847+
// Top-level fields should NOT have leaked from the updated version
848+
expect(restored.radio).toBeFalsy()
849+
expect(restored.select).toEqual([])
850+
851+
// Block sub-fields should NOT have leaked either
852+
expect(restored.blocksField?.[0]!.localized).toBeFalsy()
853+
expect(restored.blocksField?.[0]!.text).toBe('original-text')
854+
})
787855
})
788856

789857
it('should restore published version with correct data', async () => {

0 commit comments

Comments
 (0)