payloadcms / payload

Payload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.
https://payloadcms.com
MIT License
27.71k stars 1.72k forks source link

Removing `localized: true` breaks admin and client #9094

Open hdodov opened 1 week ago

hdodov commented 1 week ago

Describe the Bug

When you remove a field that has previously had localized: true and a value in the database, the front-end breaks.

Link to the code that reproduces this issue

https://github.com/hdodov/test-payload/tree/localize-issues

Reproduction Steps

  1. git clone git@github.com:hdodov/test-payload.git
  2. pnpm docker to start the project
  3. Open http://localhost:3000/admin/collections/pages
  4. Create a page with the fields:
    • slug: foo
    • text: hello
  5. You'll see this in the database:
    {
     "_id" : ObjectId("672efe5985ce4bd7437d6d7c"),
     "slug" : "foo",
     "text" : "hello",
     "createdAt" : ISODate("2024-11-09T06:16:57.505+0000"),
     "updatedAt" : ISODate("2024-11-09T06:16:57.505+0000"),
     "__v" : NumberInt(0)
    }
  6. Add LOCALIZED=true in .env
  7. Restart the server
  8. Change the text field to hello2
  9. You'll get this in the database:
    {
     "_id" : ObjectId("672efe5985ce4bd7437d6d7c"),
     "slug" : "foo",
    -  "text" : "hello",
    +  "text" : {
    +    "en" : "hello2"
    +  },
     "createdAt" : ISODate("2024-11-09T06:16:57.505+0000"),
     "updatedAt" : ISODate("2024-11-09T06:36:22.517+0000"),
     "__v" : NumberInt(0)
    }
  10. The admin and the page still work as expected
  11. Remove LOCALIZED=true from .env
  12. Restart the server
  13. Data remains the same in the database (expected)
  14. Field value in the admin changes to [object Object] (not expected) Image
  15. There's the following error on the front-end (not expected):
    Error: Objects are not valid as a React child (found: object with keys {en}). If you meant to render a collection of children, use an array instead.
     at throwOnInvalidObjectType (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:4419:13)
     at reconcileChildFibersImpl (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:5356:11)
     at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:5398:33)
     at reconcileChildren (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7721:13)
     at beginWork (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:9934:13)
     at runWithFiberInDEV (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:544:16)
     at performUnitOfWork (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:14997:22)
     at workLoopSync (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:14827:41)
     at renderRootSync (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:14807:11)
     at performWorkOnRoot (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:14342:44)
     at performWorkOnRootViaSchedulerTask (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:15853:7)
     at MessagePort.performWorkUntilDeadline (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.0.0_react-dom@19.0.0-rc-65a56d0e-20241020_react@19.0.0-rc-65a56d0e-20241020__react@19_7kfqem4pd6aim6nxuy4v6dxrry/node_modules/next/dist/compiled/scheduler/cjs/scheduler.development.js:44:48)

    Image

Proposed Solution

Payload should check if fields are coming in the form of an object from the database. It already normalizes { en: "content" } to "content" when the field is marked as localized. It just has to do this when the field is not marked as localized too. In other words, it should function like this:

Field is localied Database value Requested locale Returned value
Yes "foo" en "foo"
Yes "foo" bg "foo"
Yes { "en": "foo" } en "foo"
Yes { "en": "foo" } bg "foo"
No "foo" en "foo"
No "foo" bg "foo"
No { "en": "foo" } en "foo"
No { "en": "foo" } bg "foo"
No { "bg": "фуу" } bg ""

…but the current behavior is this:

Field is localied Database value Requested locale Returned value
Yes "foo" en "foo"
Yes "foo" bg "foo"
Yes { "en": "foo" } en "foo"
Yes { "en": "foo" } bg "foo"
No "foo" en "foo"
No "foo" bg "foo"
No { "en": "foo" } en { "en": "foo" }
No { "en": "foo" } bg { "en": "foo" }
No { "bg": "фуу" } bg { "bg": "фуу" }

Which area(s) are affected? (Select all that apply)

area: core

Environment Info

Binaries:
  Node: 20.17.0
  npm: 10.8.2
  Yarn: N/A
  pnpm: 9.9.0
Relevant Packages:
  payload: 3.0.0-beta.126
  next: 15.0.0
  @payloadcms/db-mongodb: 3.0.0-beta.126
  @payloadcms/email-nodemailer: 3.0.0-beta.126
  @payloadcms/graphql: 3.0.0-beta.126
  @payloadcms/next/utilities: 3.0.0-beta.126
  @payloadcms/payload-cloud: 3.0.0-beta.126
  @payloadcms/richtext-lexical: 3.0.0-beta.126
  @payloadcms/translations: 3.0.0-beta.126
  @payloadcms/ui/shared: 3.0.0-beta.126
  react: 19.0.0-rc-65a56d0e-20241020
  react-dom: 19.0.0-rc-65a56d0e-20241020
Operating System:
  Platform: darwin
  Arch: x64
  Version: Darwin Kernel Version 23.6.0: Mon Jul 29 21:13:00 PDT 2024; root:xnu-10063.141.2~1/RELEASE_X86_64
  Available memory (MB): 32768
  Available CPU cores: 16
hdodov commented 1 week ago

This leads to another error when trying to do a payload.update():

CastError: Cast to embedded failed for value "{...}" (type Object) at path "blocks" because of "CastError"
    at model.Query.exec (webpack-internal:///(rsc)/./node_modules/.pnpm/mongoose@6.12.3_@aws-sdk+client-sso-oidc@3.679.0_@aws-sdk+client-sts@3.679.0_/node_modules/mongoose/lib/query.js:4921:21)
    at Query.then (webpack-internal:///(rsc)/./node_modules/.pnpm/mongoose@6.12.3_@aws-sdk+client-sso-oidc@3.679.0_@aws-sdk+client-sts@3.679.0_/node_modules/mongoose/lib/query.js:5020:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

In the error message:

I think this cast error refers to the fact that Payload attempts to slide in the object { "en": "..." } into a field that expects a string:

  1. When the field has localize: true, it's an object with each locale as the key
  2. When localize: true is removed, it is now expected to be a string
  3. The object still remains in the database
  4. When performing an update, even on a completely unrelated field, Payload uses the current value (the object), when a string is expected