payloadcms / payloadcms/payload

Restoring a version stamps the new version row's createdAt with the restored document's createdAt instead of the restore time

Open
#17,760 0 comments 0 reactions 0 assignees View on GitHub

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.ts

    createdAt: operation === 'restoreVersion' ? versionData.createdAt : now,
    

    (versionData is the restored document copy, so its createdAt is the document's original creation time.)

  • Globals: packages/payload/src/globals/operations/restoreVersion.ts

    createdAt: result.createdAt ? new Date(result.createdAt).toISOString() : now,
    

    (result is the just-updated global document.)

Reproduction Steps
  1. Enable versions on a collection (drafts optional).
  2. create a document, then update it, so there are at least two versions.
  3. await payload.restoreVersion({ collection, id }) using the id of the older version.
  4. Read the newest version row: await payload.findVersions({ collection, where: { and: [{ latest: { equals: true } }, { parent: { equals: docId } }] } }).
  5. Observe on the restore-created row: createdAt equals the document's original createdAt (much earlier than the restore), while updatedAt is ~now. Expected: createdAt ≈ now, equal to updatedAt.

The global path (restoreGlobalVersionfindGlobalVersions) 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 on 3.88.0) and confirmed present on main (v4) — both restore code paths are identical on main.
  • Database: adapter-agnostic (the stored value is wrong on all adapters). The list-ordering symptom is visible on SQL adapters (@payloadcms/drizzle); MongoDB's -updatedAt default 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.