payloadcms / payloadcms/payload
Restoring a version stamps the new version row's createdAt with the restored document's createdAt instead of the restore time
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Restoring a version stamps the new version row's createdAt with the restored document's createdAt instead of the restore time
Describe the Bug
When you restore a version (collection or global), Payload writes a brand-new version row to record the restore. That new row's updatedAt is correctly set to "now", but its createdAt is set to the restored document's original createdAt rather than the time the row was actually created.
This breaks the version-row timestamp contract that Payload enforces everywhere else: a version row's createdAt/updatedAt describe when that row was written, while the document's own birth time is preserved inside the row's snapshot (version.createdAt). The restore paths conflate the two and copy the document's createdAt onto the row.
User-visible impact: on SQL adapters (@payloadcms/drizzle), the admin Versions list default-sorts by the row's createdAt (buildOrderBy falls back to -createdAt) while displaying updatedAt. A version created by a restore therefore sinks to the bottom of the list even though it is the newest entry, and relative-time labels in the compare view read the wrong field. The stored value is equally wrong on the MongoDB adapter; only the default list ordering there (-updatedAt) hides the symptom.
Link to the code that reproduces this issue
Reproducible against the standard test/versions suite (see steps below). The two lines:
-
Collections:
packages/payload/src/versions/saveVersion.tscreatedAt: operation === 'restoreVersion' ? versionData.createdAt : now,(
versionDatais the restored document copy, so itscreatedAtis the document's original creation time.) -
Globals:
packages/payload/src/globals/operations/restoreVersion.tscreatedAt: result.createdAt ? new Date(result.createdAt).toISOString() : now,(
resultis the just-updated global document.)
Reproduction Steps
- Enable versions on a collection (drafts optional).
createa document, thenupdateit, so there are at least two versions.await payload.restoreVersion({ collection, id })using the id of the older version.- Read the newest version row:
await payload.findVersions({ collection, where: { and: [{ latest: { equals: true } }, { parent: { equals: docId } }] } }). - Observe on the restore-created row:
createdAtequals the document's originalcreatedAt(much earlier than the restore), whileupdatedAtis ~now. Expected:createdAt≈ now, equal toupdatedAt.
The global path (restoreGlobalVersion → findGlobalVersions) shows the same defect via a separate code path.
Expected Behaviour
The restore-created version row's createdAt should be the time the row was created (≈ now, matching its updatedAt), exactly like every non-restore version write. The restored document's own createdAt must remain unchanged — which it already is, since it is preserved in the row's snapshot and on the document record itself.
Environment
- Reproduced on
3.x(checked on3.88.0) and confirmed present onmain(v4) — both restore code paths are identical onmain. - Database: adapter-agnostic (the stored value is wrong on all adapters). The list-ordering symptom is visible on SQL adapters (
@payloadcms/drizzle); MongoDB's-updatedAtdefault ordering masks it.
Additional Context
The defect predates PR #13333 (pre-#13333 code already did createdAt: prevVersion.createdAt; #13333 relocated the logic into saveVersion.ts). The sibling PR #13335 ("ensure updates to createdAt and updatedAt are respected", merged the same day) refreshed updatedAt on both restore paths but did not touch the row createdAt — this looks like an overlooked gap during that refactor rather than intended behaviour.
I have a fix + integration tests ready and will open a PR referencing this issue.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/payload/src/versions/saveVersion.ts and packages/payload/src/globals/operations/restoreVersion.ts, then run the standard test/versions suite using the collection and global restore paths. Done means restore-created version rows have createdAt matching the write time while the restored document's createdAt remains preserved, with integration coverage for both paths.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 42/100