Skip to content

fix(cloudinary): persist clientUploadContext fields on Payload 3.82+#148

Draft
jhb-dev wants to merge 1 commit into
mainfrom
fix/cloudinary-client-upload-persistence
Draft

fix(cloudinary): persist clientUploadContext fields on Payload 3.82+#148
jhb-dev wants to merge 1 commit into
mainfrom
fix/cloudinary-client-upload-persistence

Conversation

@jhb-dev
Copy link
Copy Markdown
Contributor

@jhb-dev jhb-dev commented May 18, 2026

Summary

Restores persistence of cloudinaryPublicId and url for client-side uploads after the upstream regression in @payloadcms/plugin-cloud-storage v3.82+.

Root cause

In Payload 3.82, @payloadcms/plugin-cloud-storage changed its afterChange hook to skip adapter.handleUpload(...) whenever the incoming file carries a clientUploadContext (upstream PR #16115). For stateless backends like S3 this is fine — the file is already at a deterministic key. But this plugin used handleUpload to copy publicId / secureUrl from clientUploadContext into the doc's cloudinaryPublicId and url fields. With that call skipped:

  • cloudinaryPublicId is never persisted →
  • adapter.generateURL returns undefined
  • data.url ends up unset →
  • Admin falls back to /api/{collection}/file/{filename}
  • Payload's local-file fallback errors with "missing on disk".

Fix

Adds a collection-level beforeChange hook on each Cloudinary-enabled collection. The hook reads req.file.clientUploadContext and writes both cloudinaryPublicId and url directly into data. Collection-level hooks run after field-level hooks, so this also overrides the URL field whose beforeChange saw no cloudinaryPublicId.

Also drops the now-dead if (clientUploadContext) branch from src/handleUpload.ts — upstream filters those files out before calling the adapter, so the branch is unreachable.

Trade-off — this is a workaround

This restores correctness without depending on upstream. The deeper problem is an upstream API contract violation: the HandleUpload type still declares clientUploadContext as a parameter, but the runtime filters those calls away (afterChange.js:35). An alternate PR (refactor the plugin to be stateless like @payloadcms/storage-s3) is opened separately.

Test plan

  • New src/index.test.ts reproduces the bug: runs the plugin and exercises the resulting collection's beforeChange hook with a simulated req.file.clientUploadContext; asserts data.cloudinaryPublicId and data.url are set. Failed before the fix, passes after.
  • pnpm typecheck clean.
  • pnpm lint clean.
  • pnpm build produces a clean dist/ with no *.test.* files.
  • End-to-end check in cloudinary/dev — start the dev app, upload an image to the images collection (which uses disablePayloadAccessControl: true + clientUploads: true), confirm the doc lands with cloudinaryPublicId and url populated and the admin preview renders.

Adds vitest scaffolding

This plugin had no test setup; the PR mirrors what vercel-deployments already does: vitest devDep, test / test:watch scripts, vitest.config.ts, and a tsconfig.build.json that excludes test files from the published build (matching swc's --ignore flag).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant