Skip to content

fix: reuploading file on Save draft resets main doc _status to draft#10

Open
deepshekhardas wants to merge 7 commits into
mainfrom
fix/16633-draft-save-resets-status
Open

fix: reuploading file on Save draft resets main doc _status to draft#10
deepshekhardas wants to merge 7 commits into
mainfrom
fix/16633-draft-save-resets-status

Conversation

@deepshekhardas

@deepshekhardas deepshekhardas commented May 19, 2026

Copy link
Copy Markdown
Owner

Fixes payloadcms#16633

For an upload-enabled collection with drafts enabled, replacing a published file and saving as a draft incorrectly overwrites the main collection document's _status from 'published' to 'draft'.

The bug was in collections/operations/utilities/update.ts where data._status = 'draft' was set unconditionally when isSavingDraft was true, even when updating an existing published document.

Fix by only setting _status = 'draft' for new documents (when id is undefined). For existing documents, the main doc should remain published while a draft version is created.


Summary by cubic

Prevents published docs from reverting to draft when replacing a file and saving as draft. Also includes stability fixes across drizzle, plugin-cloud-storage, plugin-mcp, and env loading.

  • Bug Fixes

    • Draft save: only set _status = 'draft' for new docs; existing published docs stay published.
    • Blocks: purge old relationship rows on reorder via prefix path deletes; also fixes stale rels on version restore.
    • Localized status: ensure updates when only localized _status changes.
    • plugin-cloud-storage: fall back to req.file for filename/mimeType and use payloadUploadSizes when data.sizes is missing to prevent skipped uploads after field projection.
    • plugin-mcp: accept both "payload-mcp-api-keys API-Key " and "Bearer " headers.
    • Env loading: support Next.js 15.5+ by importing @next/env via namespaced fallback.
  • Dependencies

    • Lazy-load drizzle-kit in drizzle adapters to avoid bundling in production/edge.

Written for commit c719c36. Summary will update on new commits. Review in cubic

Developer added 7 commits May 19, 2026 10:24
The MCP endpoint was accepting only Bearer token but Payload convention
uses 'payload-mcp-api-keys API-Key <key>' format. Now accepts both:
- 'payload-mcp-api-keys API-Key <key>' (Payload convention)
- 'Bearer <key>' (backwards compatible)

This makes the plugin consistent with other Payload API-key surfaces.

Fixes: payloadcms#16572
When using localizeStatus, the last version wasn't retrieved properly
when publishing directly. This is because the version_updatedAt and
version_createdAt weren't being set properly when publishing.

The fix adds a check for localized status (_status) in locales to
ensure main row data update happens even when only localized fields
have changed.

Fixes: payloadcms#16395
drizzle-kit/api was being required at module load time, causing it
to be included in production bundle for OpenNext Cloudflare and
other edge runtimes.

This moves the require() inside the function so it's only loaded
when actually needed (during migrations/schema push).

Also fixes the same issue in postgres adapter.

Fixes: payloadcms#16470
When a POST request uses ?select[…] to project mimeType/filename out of the
response doc, the cloud-storage plugin silently skips uploading to S3.

Fix by falling back to req.file properties when data.filename/data.mimeType
are undefined due to select projection. Also handle sizes fallback using
payloadUploadSizes when data.sizes is missing.

Closes payloadcms#16670
When blocks containing relationship fields are reordered, old _rels rows at
previous path positions were never deleted, causing stale FK references.

Fix by adding prefix-based deletion for blocks fields:
1. Add 'prefix' property to RelationshipToDelete type
2. In transformBlocks, signal that all old rels under the blocks field
   should be purged by adding a prefix entry to relationshipsToDelete
3. Update deleteExistingRowsByPath to support prefix deletions using
   LIKE queries (e.g., path LIKE 'layout.%')

This also addresses the related issue payloadcms#15976 (rels not cleaned on version
restore) since the same root cause applies.

Closes payloadcms#16647
On Next.js 15.5+, @next/env no longer exposes a default export - only
named exports remain. The existing default import causes undefined to
be destructured, throwing 'Cannot destructure property loadEnvConfig'.

Fix by using namespace import with fallback:
- import * as nextEnvImport from '@next/env'
- const { loadEnvConfig } = nextEnvImport.default ?? nextEnvImport

The ?? fallback maintains backwards-compat with Next.js < 15.5 while
supporting 15.5+.

Closes payloadcms#16674
For an upload-enabled collection with drafts enabled, replacing a published
file and saving as a draft incorrectly overwrites the main collection
document's _status from 'published' to 'draft'.

The bug was in collections/operations/utilities/update.ts where
data._status = 'draft' was set unconditionally when isSavingDraft was true,
even when updating an existing published document.

Fix by only setting _status = 'draft' for new documents (when id is undefined).
For existing documents, the main doc should remain published while a draft
version is created.

Closes payloadcms#16633

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/drizzle/src/postgres/requireDrizzleKit.ts">

<violation number="1" location="packages/drizzle/src/postgres/requireDrizzleKit.ts:4">
P1: `require('module')` is used before any `require` exists, which will throw in ESM runtime.</violation>
</file>

<file name="packages/drizzle/src/sqlite/requireDrizzleKit.ts">

<violation number="1" location="packages/drizzle/src/sqlite/requireDrizzleKit.ts:4">
P1: `require('module')` is called before `require` is defined. In this ESM module, `require` is not globally available, so this will throw `ReferenceError: require is not defined` at runtime.</violation>
</file>

<file name="packages/plugin-cloud-storage/src/utilities/getIncomingFiles.ts">

<violation number="1" location="packages/plugin-cloud-storage/src/utilities/getIncomingFiles.ts:39">
P2: The new `sizes` fallback uses `payloadUploadSizes` (buffers) where image size metadata is expected, so the fallback path never adds resized files.</violation>
</file>

<file name="packages/drizzle/src/upsertRow/deleteExistingRowsByPath.ts">

<violation number="1" location="packages/drizzle/src/upsertRow/deleteExistingRowsByPath.ts:83">
P1: Escape LIKE wildcard characters in dynamic prefixes before building the delete pattern; otherwise `%`/`_` in a path can over-match and delete unintended rows.</violation>
</file>

<file name="packages/drizzle/src/transform/write/blocks.ts">

<violation number="1" location="packages/drizzle/src/transform/write/blocks.ts:150">
P2: Prefix deletion here relies on an unescaped SQL LIKE pattern, so `_`/`%` in block field names can over-delete unrelated relationship paths.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const require = createRequire(import.meta.url)

export const requireDrizzleKit: RequireDrizzleKit = () => {
const { createRequire } = require('module')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: require('module') is used before any require exists, which will throw in ESM runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/drizzle/src/postgres/requireDrizzleKit.ts, line 4:

<comment>`require('module')` is used before any `require` exists, which will throw in ESM runtime.</comment>

<file context>
@@ -1,10 +1,8 @@
-const require = createRequire(import.meta.url)
-
 export const requireDrizzleKit: RequireDrizzleKit = () => {
+  const { createRequire } = require('module')
+  const require = createRequire(import.meta.url)
   const {
</file context>

const require = createRequire(import.meta.url)

export const requireDrizzleKit: RequireDrizzleKit = () => {
const { createRequire } = require('module')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: require('module') is called before require is defined. In this ESM module, require is not globally available, so this will throw ReferenceError: require is not defined at runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/drizzle/src/sqlite/requireDrizzleKit.ts, line 4:

<comment>`require('module')` is called before `require` is defined. In this ESM module, `require` is not globally available, so this will throw `ReferenceError: require is not defined` at runtime.</comment>

<file context>
@@ -1,10 +1,8 @@
-const require = createRequire(import.meta.url)
-
 export const requireDrizzleKit: RequireDrizzleKit = () => {
+  const { createRequire } = require('module')
+  const require = createRequire(import.meta.url)
   const {
</file context>

for (const prefix of localizedPrefixPathsToDelete) {
const whereConstraints = [
eq(table[parentColumnName], parentID),
like(table[pathColumnName], `${prefix}%`),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Escape LIKE wildcard characters in dynamic prefixes before building the delete pattern; otherwise %/_ in a path can over-match and delete unintended rows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/drizzle/src/upsertRow/deleteExistingRowsByPath.ts, line 83:

<comment>Escape LIKE wildcard characters in dynamic prefixes before building the delete pattern; otherwise `%`/`_` in a path can over-match and delete unintended rows.</comment>

<file context>
@@ -66,4 +75,34 @@ export const deleteExistingRowsByPath = async ({
+    for (const prefix of localizedPrefixPathsToDelete) {
+      const whereConstraints = [
+        eq(table[parentColumnName], parentID),
+        like(table[pathColumnName], `${prefix}%`),
+      ]
+
</file context>

if (data?.sizes) {
Object.entries(data.sizes).forEach(([key, resizedFileData]) => {
if (payloadUploadSizes?.[key] && resizedFileData.mimeType) {
const sizes = data?.sizes ?? payloadUploadSizes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new sizes fallback uses payloadUploadSizes (buffers) where image size metadata is expected, so the fallback path never adds resized files.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin-cloud-storage/src/utilities/getIncomingFiles.ts, line 39:

<comment>The new `sizes` fallback uses `payloadUploadSizes` (buffers) where image size metadata is expected, so the fallback path never adds resized files.</comment>

<file context>
@@ -21,21 +21,25 @@ export function getIncomingFiles({
-    if (data?.sizes) {
-      Object.entries(data.sizes).forEach(([key, resizedFileData]) => {
-        if (payloadUploadSizes?.[key] && resizedFileData.mimeType) {
+    const sizes = data?.sizes ?? payloadUploadSizes
+    if (sizes) {
+      Object.entries(sizes).forEach(([key, resizedFileData]) => {
</file context>


relationshipsToDelete.push({
path: `${path || ''}${field.name}.`,
prefix: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Prefix deletion here relies on an unescaped SQL LIKE pattern, so _/% in block field names can over-delete unrelated relationship paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/drizzle/src/transform/write/blocks.ts, line 150:

<comment>Prefix deletion here relies on an unescaped SQL LIKE pattern, so `_`/`%` in block field names can over-delete unrelated relationship paths.</comment>

<file context>
@@ -144,4 +144,9 @@ export const transformBlocks = ({
+
+  relationshipsToDelete.push({
+    path: `${path || ''}${field.name}.`,
+    prefix: true,
+  })
 }
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reuploading a file on "Save draft" resets the main collection document's _status to "draft"

1 participant