Compare commits

..

167 Commits

Author SHA1 Message Date
Jarrod Flesch
c9d0d1ffcc clean up provider a bit 2025-07-30 21:52:48 -04:00
Jarrod Flesch
9031f3bf23 feat: add hooks to restoreVersion collection operation (#13333)
Adds missing hooks to the restoreVersion operation.
- beforeOperation
- beforeValidate - Fields
- beforeValidate - Collection
- beforeChange - Collection
- beforeChange - Fields
- afterOperation
2025-07-30 21:25:53 -04:00
Muhammad Nizar
df91321f4a feat(translations): add Indonesian translations (#13323)
### What?
Translated payload to Indonesian

### Why?
I needed to have payload in Indonesian

### How?
By following the
[readme](https://github.com/payloadcms/payload/blob/main/packages/translations/README.md)
2025-07-30 21:35:22 +00:00
Patrik
11755089f8 feat: adds trash support to the count operation (#13304)
### What?

- Updated the `countOperation` to respect the `trash` argument.

### Why?

- Previously, `count` would incorrectly include trashed documents even
when `trash` was not specified.
- This change aligns `count` behavior with `find` and other operations,
providing accurate counts for normal and trashed documents.

### How?

- Applied `appendNonTrashedFilter` in `countOperation` to automatically
exclude soft-deleted docs when `trash: false` (default).
- Added `trash` argument support in Local API, REST API (`/count`
endpoints), and GraphQL (`count<Collection>` queries).
2025-07-30 14:11:11 -07:00
Patrik
a8b6983ab5 test: adds e2e tests for auth enabled collections with trash enabled (#13317)
### What?
- Added new end-to-end tests covering trash functionality for
auth-enabled collections (e.g., `users`).
- Implemented test cases for:
  - Display of the trash tab in the list view.
  - Trashing a user and verifying its appearance in the trash view.
  - Accessing the trashed user edit view.
  - Ensuring all auth fields are properly disabled in trashed state.
  - Restoring a trashed user and verifying its status.

### Why?
- To ensure that the trash (soft-delete) feature works consistently for
collections with `auth: true`.
- To prevent regressions in user management flows, especially around
disabling and restoring trashed users.

### How?
- Added a new `Auth enabled collection` test suite in the E2E `Trash`
tests.
2025-07-30 14:11:02 -07:00
Jarrod Flesch
f2d4004237 fix(ui): incorrect padding when using rtl (#13338)
Fixes inconsistent padding in the doc view for RTL viewing.

###  Before

#### Desktop
<img width="1331" height="310" alt="CleanShot 2025-07-30 at 16 37 30"
src="https://github.com/user-attachments/assets/48d3e127-09dd-4356-99ae-16fe47817937"
/>

#### Mobile
<img width="619" height="328" alt="CleanShot 2025-07-30 at 16 37 52"
src="https://github.com/user-attachments/assets/36169ca5-c1a2-4175-a6e1-f0a4784d5e9e"
/>


###  After

#### Desktop
<img width="1675" height="291" alt="CleanShot 2025-07-30 at 16 39 18"
src="https://github.com/user-attachments/assets/1da78a8a-b236-4f95-9eb2-8b5055b676ae"
/>

#### Mobile
<img width="531" height="309" alt="CleanShot 2025-07-30 at 16 39 30"
src="https://github.com/user-attachments/assets/af858bfc-6d75-4139-ada1-4f8100744bfb"
/>
2025-07-30 21:03:03 +00:00
Alessio Gravili
8a489410ad fix(next): incorrect version view stepnav value (#13305)
Previously, the version view stepnav incorrectly displayed the version
ID instead of the parent document ID in the navigation. It also
incorrectly pulled the field value if `useAsTitle` was set, displayed
the `[undefined]` instead.

**Edit View:**
<img width="2218" height="244" alt="Screenshot 2025-07-28 at 16 55
26@2x"
src="https://github.com/user-attachments/assets/7b144480-ec5a-4592-b603-09e1e35bd558"
/>

## Version View - Before:

<img width="2280" height="378" alt="Screenshot 2025-07-28 at 16 56
02@2x"
src="https://github.com/user-attachments/assets/8b79bab3-a79b-4930-ade1-2da450f00fc7"
/>


## Version View - After:

**Version View:**
<img width="2222" height="358" alt="Screenshot 2025-07-28 at 16 55
46@2x"
src="https://github.com/user-attachments/assets/bb0fffbb-c3e6-419f-ad72-5731e85059cc"
/>


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210907527783756
2025-07-30 20:59:48 +00:00
Alessio Gravili
095e7d904f fix(next): safely access relationTo value from relationship field (#13337)
Fixes https://github.com/payloadcms/payload/issues/13330
2025-07-30 20:54:04 +00:00
Patrik
c48b57fdbf fix(next): incorrect doc link in trash view with groupBy enabled (#13332)
### What?

Fixes an issue where document links in the trash view were incorrectly
generated when group-by was enabled for a collection. Previously, links
in grouped tables would omit the `/trash` segment, causing navigation to
crash while trying to route to the default edit view instead of the
trashed document view.

### Why?

When viewing a collection in group-by mode, document rows are rendered
in grouped tables via the `handleGroupBy` logic. However, these tables
were unaware of whether the view was operating in trash mode, so the
generated row links did not include the necessary `/trash` segment. This
broke navigation when trying to view or edit trashed documents.

### How?

- Threaded the `viewType` prop through `renderListView` into the
`handleGroupBy` utility.
- Passed `viewType` into each `renderTable` call within `handleGroupBy`,
ensuring proper link generation.
- `renderTable` already supports `viewType` and appends `/trash` to edit
links when it's set to 'trash'.
2025-07-30 13:17:03 -07:00
Sasha
b26a73be4a fix: querying hasMany: true select fields inside polymorphic joins (#13334)
This PR fixes queries like this:

```ts
const findFolder = await payload.find({
  collection: 'payload-folders',
  where: {
    id: {
      equals: folderDoc.id,
    },
  },
  joins: {
    documentsAndFolders: {
      limit: 100_000,
      sort: 'name',
      where: {
        and: [
          {
            relationTo: {
              equals: 'payload-folders',
            },
          },
          {
            folderType: {
              in: ['folderPoly1'], // previously this didn't work
            },
          },
        ],
      },
    },
  },
})
```

Additionally, this PR potentially fixes querying JSON fields by the top
level path, for example if your JSON field has a value like: `[1, 2]`,
previously `where: { json: { equals: 1 } }` didn't work, however with a
value like `{ nested: [1, 2] }` and a query `where: { 'json.nested': {
equals: 1 } }`it did.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-07-30 15:30:20 -04:00
Alessio Gravili
3114b89d4c perf: 23% faster job queue system on postgres/sqlite (#13187)
Previously, a single run of the simplest job queue workflow (1 single
task, no db calls by user code in the task - we're just testing db
system overhead) would result in **22 db roundtrips** on drizzle. This
PR reduces it to **17 db roundtrips** by doing the following:

- Modifies db.updateJobs to use the new optimized upsertRow function if
the update is simple
- Do not unnecessarily pass the job log to the final job update when the
workflow completes => allows using the optimized upsertRow function, as
only the main table is involved

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210888186878606
2025-07-30 16:23:43 +03:00
Evelyn Hathaway
227a20e94b fix(richtext-lexical): recursively unwrap generic Slate nodes in Lexical migration converter (#13202)
## What?

The Slate to Lexical migration script assumes that the depth of Slate
nodes matches the depth of the Lexical schema, which isn't necessarily
true. This pull request fixes this assumption by first checking for
children and unwrapping the text nodes.

## Why?

During my migration, I ran into a lot of copy + pasted rich text with
list items with untyped nodes with `children`. The existing migration
script assumed that since list items can't have paragraphs, all untyped
nodes inside must be text nodes.

The result of the migration script was a lot of invalid text nodes with
`text: undefined` and all of the content in the `children` being
silently lost. Beyond the silent loss, the invalid text nodes caused the
Lexical editor to unmount with an error about accessing `0 of
undefined`, so those documents couldn't be edited.

This additionally makes the migration script more closely align with the
[recursive serialization logic recommendation from the Payload Slate
Rich Text
documentation](https://payloadcms.com/docs/rich-text/slate#generating-html).

## Visualization

### Slate

```txt
Slate rich text content
┣━┳━ Unordered list
┋ ┣━┳━ List item
┋ ┋ ┗━┳━ Generic (paragraph-like, untyped with children)
┋ ┋   ┣━━━ Text (untyped) `Hello `
┋ ┋   ┗━━━ Text (untyped) `World!
[...]
```

### Lexical Before PR

```txt
Lexical rich text content (invalid)
┣━┳━ Unordered list
┋ ┣━┳━ List item
┋ ┋ ┗━━━ Invalid text (assumed the generic node was text, stopped processing children, cannot restore lost text without a restoring backup with Slate and rerunning the script after this MR)
[...]
```

### Lexical After PR

```txt
Lexical rich text content
┣━┳━ Unordered list
┋ ┣━┳━ List item
┋ ┋ ┣━━━ Text `Hello `
┋ ┋ ┗━━━ Text `World!
[...]
```

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
2025-07-30 13:16:18 +00:00
Jarrod Flesch
a22f27de1c test: stabilize frequent fails (#13318)
Adjusts tests that "flake" frequently.
2025-07-30 05:52:01 -07:00
Patrik
e7124f6176 fix(next): cannot filter trash (#13320)
### What?

- Updated `TrashView` to pass `trash: true` as a dedicated prop instead
of embedding it in the `query` object.
- Modified `renderListView` to correctly merge `trash` and `where`
queries by using both `queryFromArgs` and `queryFromReq`.
- Ensured filtering (via `where`) works correctly in the trash view.

### Why?

Previously, the `trash: true` flag was injected into the `query` object,
and `renderListView` only used `queryFromArgs`.
This caused the `where` clause from filters (added by the
`WhereBuilder`) to be overridden, breaking filtering in the trash view.

### How?

- Introduced an explicit `trash` prop in `renderListView` arguments.
- Updated `TrashView` to pass `trash: true` separately.
- Updated `renderListView` to apply the `trash` filter in addition to
any `where` conditions.
2025-07-29 14:29:04 -07:00
Elliot DeNolf
183f313387 chore(release): v3.49.1 [skip ci] 2025-07-29 16:38:50 -04:00
contip
b1fa76e397 fix: keep apiKey encrypted in refresh operation (#13063) (#13177)
### What?
Prevents decrypted apiKey from being saved back to database on the auth
refresh operation.

### Why?
References issue #13063: refreshing a token for a logged-in user
decrypted `apiKey` and wrote it back in plaintext, corrupting the user
record.

### How?
The user is now fetched with `db.findOne` instead of `findByID`,
preserving the encryption of the key when saved back to the database
using `db.updateOne`. The user record is then re-fetched using
`findByID`, allowing for the decrypted key to be provided in the
response.

### Tests
*  keeps apiKey encrypted in DB after refresh
*  returns user with decrypted apiKey after refresh

Fixes #13063
2025-07-29 16:27:45 -04:00
German Jablonski
08942494e3 fix: filters cookies with the payload- prefix in getExternalFile by default (#13215)
### What

- filters cookies with the `payload-` prefix in `getExternalFile` by
default (if `externalFileHeaderFilter` is not used).
- Document in `externalFileHeaderFilter`, that the user should handle
the removing of the payload cookie.

### Why

In the Payload application, the `getExternalFile` function sends the
user's cookies to an external server when fetching media, inadvertently
exposing the user's session to that third-party service.




```ts
const headers = uploadConfig.externalFileHeaderFilter
  ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers)))
  : { cookie: req.headers?.get('cookie') };

const res = await fetch(fileURL, {
  credentials: 'include',
  headers,
  method: 'GET',
});
```
Although the
[externalFileHeaderFilter](https://payloadcms.com/docs/upload/overview#collection-upload-options)
function can strip sensitive cookies from the request, the default
config includes the session cookie, violating the secure-by-default
principle.

### How

- If `externalFileHeaderFilter` is not defined, any cookie beginning
with `payload-` is filtered.
- Added 2 tests: both for the case where `externalFileHeaderFilter` is
defined and for the case where it is not.





---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210561338171125
2025-07-29 16:21:50 -04:00
Adebayo Ajayi
da8bf69054 fix(translations): improve swedish translations (#13272)
### What?
Update Swedish translation, removing minor inconsistencies and opting
for more natural sounding translations
### Why?
The current Swedish translation contained some minor grammatical issues
and inconsistencies that make the UI feel less natural to Swedish users.
### How?
- Fixed "e-post" hyphenation consistency
- Changed "Alla platser" → "Alla språk" (locales should be "languages")
- Improved action verbs: "Tydlig" → "Rensa", "Stänga" → "Stäng"
- Made "Kollapsa" → "Fäll ihop" more natural
- Standardized preview terminology: "Live förhandsgranskning" →
"förhandsgranskning"
- Fixed terminology: "fältdatabas" → "fältdata" (fältdatabas mean field
database while fältdata means field data)
- Changed "Programinställningar" → "Systeminställningar" (more
appropriate for software)
- Fixed punctuation: em dash → comma in "sorryNotFound"
- Improved "Visa endast läsning" → "Visa som skrivskyddad"
(grammatically correct)

Fixes #
2025-07-29 18:37:12 +00:00
brutumfulmen97
26d9daeccf fix(translations): missing closing brace in rs latin translation for lastSavedAgo (#13172)
Fixed missing closing brace in the translations package for the language
rs latin.
<img width="885" height="200" alt="image"
src="https://github.com/user-attachments/assets/12d00305-6cc9-46ce-87e8-2c66f9d9e63c"
/>
Here is the code diff.

Co-authored-by: Vlatko Percic <vlatko@studiopresent.com>
2025-07-29 18:20:29 +00:00
Alejandro Martinez
fc5944840e docs: remove asterisk from optional url property in live preview docs (#13108)
### What?

url option is not required for Link Preview, so I removed the asterisk.

### Why?

Here is the type definition

[Type Definition
Link](c6105f1e0d/packages/payload/src/config/types.ts (L159))
2025-07-29 14:16:19 -04:00
Aayush Rajagopalan
9e04dbb1ca docs: typo in vercel-content-link.mdx (#13170)
### Fixes:
'title,' -> 'title',
2025-07-29 18:07:15 +00:00
Sean Zubrickas
72954ce9f2 docs: fixes typo in ternary operator for live preview docs (#13163)
Fixes ternary operator in live preview docs.
2025-07-29 13:56:31 -04:00
Jacob Fletcher
e50220374e fix(next): group by null relationship crashes list view (#13315)
When grouping by a relationship field and it's value is `null`, the list
view crashes.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210916642997992
2025-07-29 11:55:02 -04:00
Jacob Fletcher
61ee8fadca fix(ui): autosave-enabled document drawers close unexpectedly within the join field (#13298)
Fixes #12975.

When editing autosave-enabled documents through the join field, the
document drawer closes unexpectedly on every autosave interval, making
it nearly impossible to use.

This is because as of #12842, the underlying relationship table
re-renders on every autosave event, remounting the drawer each time. The
fix is to lift the drawer out of table's rendering tree and into the
join field itself. This way all rows share the same drawer, whose
rendering lifecycle has been completely decoupled from the table's
state.

Note: this is very similar to how relationship fields achieve similar
functionality.

This PR also adds jsdocs to the `useDocumentDrawer` hook and strengthens
its types.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210906078627353
2025-07-29 11:49:15 -04:00
Jarrod Flesch
8d84352ee9 fix(next): catch list filter errors, prevent list view crash (#13297)
Catches list filter errors and prevents the list view from crashing when
attempting to search on fields the user does not have access to. Instead
just shows the default "no results found" message.
2025-07-29 11:30:07 -04:00
Elliot DeNolf
4beb27b9ad ci: show path value in audit-dependencies script [skip ci] (#13314)
Improve audit-dependencies script to show the vulnerable package path:

```diff
   {
     "package": "form-data",
     "vulnerable": "<2.5.4",
-    "fixed_in": ">=2.5.4"
+    "fixed_in": ">=2.5.4",
+    "findings": [
+      {
+        "version": "2.5.2",
+        "paths": [
+          "packages/storage-gcs > @google-cloud/storage@7.14.0 > retry-request@7.0.2 > @types/request@2.48.12 > form-data@2.5.2"
+        ]
+      }
+    ]
   }
 ]
```
2025-07-29 11:08:39 -04:00
Jacob Fletcher
c5c8c13057 fix(next): pass req through document tab conditions and custom server components (#13302)
Custom document tab components (server components) do not receive the
`user` prop, as the types suggest. This makes it difficult to wire up
conditional rendering based on the user. This is because tab conditions
don't receive a user argument either, forcing you to render the default
tab component yourself—but a custom component should not be needed for
this in the first place.

Now they both receive `req` alongside `user`, which is more closely
aligned with custom field components.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210906078627357
2025-07-28 23:35:38 -04:00
Jacob Fletcher
a888d5cc53 chore(ui): var name typo in relationship field (#13295)
Fixes typo in variable name within the relationship field component.

`disableFormModication` → `disableFormModification`
2025-07-28 23:34:32 -04:00
Jarrod Flesch
72349245ca test: fix flaky sorting test (#13303)
Ensures the browser uses fresh data after seeding by refreshing the
route and navigating when done.
2025-07-29 03:25:09 +00:00
Alessio Gravili
4fde0f23ce fix: use atomic operation for incrementing login attempts (#13204)
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210561338171141
2025-07-28 16:08:10 -07:00
Patrik
aff2ce1b9b fix(next): unable to view trashed documents when group-by is enabled (#13300)
### What?

- Fixed an issue where group-by enabled collections with `trash: true`
were not showing trashed documents in the collection’s trash view.
- Ensured that the `trash` query argument is properly passed to the
`findDistinct` call within `handleGroupBy`, allowing trashed documents
to be included in grouped list views.

### Why?

Previously, when viewing the trash view of a collection with both
**group-by** and **trash** enabled, trashed documents would not appear.
This was caused by the `trash` argument not being forwarded to
`findDistinct` in `handleGroupBy`, which resulted in empty or incorrect
group-by results.

### How?

- Passed the `trash` flag through all relevant `findDistinct` and `find`
calls in `handleGroupBy`.
2025-07-28 11:29:04 -07:00
Alessio Gravili
5c94d2dc71 feat: support next.js 15.4.4 (#13280)
- bumps next.js from 15.3.2 to 15.4.4 in monorepo and templates. It's
important to run our tests against the latest Next.js version to
guarantee full compatibility.
- bumps playwright because of peer dependency conflict with next 15.4.4
- bumps react types because why not

https://nextjs.org/blog/next-15-4

As part of this upgrade, the functionality added by
https://github.com/payloadcms/payload/pull/11658 broke. This PR fixes it
by creating a wrapper around `React.isValidElemen`t that works for
Next.js 15.4.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210803039809808
2025-07-28 16:23:43 +00:00
Jarrod Flesch
b1aac19668 chore(next): cleanup unused code (#13292)
Looks like a merge resolution kept unused code. The same condition is
added a couple lines below this removal.
2025-07-28 13:43:51 +00:00
Sean Zubrickas
d093bb1f00 fix: refactors toast error rendering (#13252)
Fixes #13191

- Render a single html element for single error messages
- Preserve ul structure for multiple errors
- Updates tests to check for both cases
2025-07-28 05:59:25 -07:00
Alessio Gravili
2e9ba10fb5 docs: remove obsolete scheduler property (#13278)
That property does not exist and was used in a previous, outdated
implementation of auto scheduling
2025-07-25 16:25:47 -07:00
Alessio Gravili
8518141a5e fix(drizzle): respect join.type config (#13258)
Respects join.type instead of hardcoding leftJoin
2025-07-25 15:46:20 -07:00
Alessio Gravili
6d6c9ebc56 perf(drizzle): 2x faster db.deleteMany (#13255)
Previously, `db.deleteMany` on postgres resulted in 2 roundtrips to the
database (find + delete with ids). This PR passes the where query
directly to the `deleteWhere` function, resulting in only one roundtrip
to the database (delete with where).

If the where query queries other tables (=> joins required), this falls
back to find + delete with ids. However, this is also more optimized
than before, as we now pass `select: { id: true }` to the findMany
query.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210871676349299
2025-07-25 15:46:09 -07:00
German Jablonski
7cd4a8a602 fix(richtext-lexical): unify indent between different converters and make paragraphs and lists match without CSS (#13274)
Previously, the Lexical editor was using px, and the JSX converter was
using rem. #12848 fixed the inconsistency by changing the editor to rem,
but it should have been the other way around, changing the JSX converter
to px.

You can see the latest explanation about why it should be 40px
[here](https://github.com/payloadcms/payload/issues/13130#issuecomment-3058348085).
In short, that's the default indentation all browsers use for lists.

This time I'm making sure to leave clear comments everywhere and a test
to avoid another regression.

Here is an image of what the e2e test looks like:

<img width="321" height="678" alt="image"
src="https://github.com/user-attachments/assets/8880c7cb-a954-4487-8377-aee17c06754c"
/>

The first part is the Lexical editor, the second is the JSX converter.

As you can see, the checkbox in JSX looks a little odd because it uses
an input checkbox (as opposed to a pseudo-element in the Lexical
editor). I thought about adding an inline style to move it slightly to
the left, but I found that browsers don't have a standard size for the
checkbox; it varies by browser and device.
That requires a little more thought; I'll address that in a future PR.

Fixes #13130
2025-07-25 22:58:49 +01:00
Jarrod Flesch
bc802846c5 fix: serve svg+xml as svg (#13277)
Based from https://github.com/payloadcms/payload/pull/13276

Fixes https://github.com/payloadcms/payload/issues/7624

If an uploaded image has `.svg` ext, and the mimeType is read as
`application/xml` adjust the mimeType to `image/svg+xml`.

---------

Co-authored-by: Philipp Schneider <47689073+philipp-tailor@users.noreply.github.com>
2025-07-25 21:00:51 +00:00
Jarrod Flesch
e8f6cb5ed1 fix: svg+xml file detection (#13276)
Adds logic for svg+xml file type detection.

---------

Co-authored-by: Philipp Schneider <47689073+philipp-tailor@users.noreply.github.com>
2025-07-25 18:33:53 +00:00
Elliot DeNolf
23bd67515c templates: bump for v3.49.0 (#13273)
🤖 Automated bump of templates for v3.49.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-25 13:39:09 -04:00
Jarrod Flesch
e29d1d98d4 fix(plugin-multi-tenant): prefer assigned tenants for selector population (#13213)
When populating the selector it should populate it with assigned tenants
before fetching all tenants that a user has access to.

You may have "public" tenants and while a user may have _access_ to the
tenant, the selector should show the ones they are assigned to. Users
with full access are the ones that should be able to see the public ones
for editing.
2025-07-25 10:10:26 -04:00
Elliot DeNolf
4ac428d250 chore(release): v3.49.0 [skip ci] 2025-07-25 09:27:41 -04:00
Sasha
75385de01f fix: filtering by polymorphic relationships inside other fields (#13265)
Previously, filtering by a polymorphic relationship inside an array /
group (unless the `name` is `version`) / tab caused `QueryError: The
following path cannot be queried:`.
2025-07-25 09:10:21 -04:00
Patrik
f63dc2a10c feat: adds trash support (soft deletes) (#12656)
### What?

This PR introduces complete trash (soft-delete) support. When a
collection is configured with `trash: true`, documents can now be
soft-deleted and restored via both the API and the admin panel.

```
import type { CollectionConfig } from 'payload'

const Posts: CollectionConfig = {
  slug: 'posts',
  trash: true, // <-- New collection config prop @default false
  fields: [
    {
      name: 'title',
      type: 'text',
    },
    // other fields...
  ],
}
```

### Why

Soft deletes allow developers and admins to safely remove documents
without losing data immediately. This enables workflows like reversible
deletions, trash views, and auditing—while preserving compatibility with
drafts, autosave, and version history.

### How?

#### Backend

- Adds new `trash: true` config option to collections.
- When enabled:
  - A `deletedAt` timestamp is conditionally injected into the schema.
- Soft deletion is performed by setting `deletedAt` instead of removing
the document from the database.
- Extends all relevant API operations (`find`, `findByID`, `update`,
`delete`, `versions`, etc.) to support a new `trash` param:
  - `trash: false` → excludes trashed documents (default)
  - `trash: true` → includes both trashed and non-trashed documents
- To query **only trashed** documents: use `trash: true` with a `where`
clause like `{ deletedAt: { exists: true } }`
- Enforces delete access control before allowing a soft delete via
update or updateByID.
- Disables version restoring on trashed documents (must be restored
first).

#### Admin Panel

- Adds a dedicated **Trash view**: `/collections/:collectionSlug/trash`
- Default delete action now soft-deletes documents when `trash: true` is
set.
- **Delete confirmation modal** includes a checkbox to permanently
delete instead.
- Trashed documents:
- Displays UI banner for better clarity of trashed document edit view vs
non-trashed document edit view
  - Render in a read-only edit view
  - Still allow access to **Preview**, **API**, and **Versions** tabs
- Updated Status component:
- Displays “Previously published” or “Previously a draft” for trashed
documents.
  - Disables status-changing actions when documents are in trash.
- Adds new **Restore** bulk action to clear the `deletedAt` timestamp.
- New `Restore` and `Permanently Delete` buttons for
single-trashed-document restore and permanent deletion.
- **Restore confirmation modal** includes a checkbox to restore as
`published`, defaults to `draft`.
- Adds **Empty Trash** and **Delete permanently** bulk actions.
  
#### Notes

- This feature is completely opt-in. Collections without trash: true
behave exactly as before.



https://github.com/user-attachments/assets/00b83f8a-0442-441e-a89e-d5dc1f49dd37
2025-07-25 09:08:22 -04:00
German Jablonski
4a712b3483 fix(ui): preserve localized blocks and arrays when using CopyToLocale (#13216)
## Problem:
In PR #11887, a bug fix for `copyToLocale` was introduced to address
issues with copying content between locales in Postgres. However, an
incorrect algorithm was used, which removed all "id" properties from
documents being copied. This led to bug #12536, where `copyToLocale`
would mistakenly delete the document in the source language, affecting
not only Postgres but any database.

## Cause and Solution:

When copying documents with localized arrays or blocks, Postgres throws
errors if there are two blocks with the same ID. This is why PR #11887
removed all IDs from the document to avoid conflicts. However, this
removal was too broad and caused issues in cases where it was
unnecessary.


The correct solution should remove the IDs only in nested fields whose
ancestors are localized. The reasoning is as follows:
- When an array/block is **not localized** (`localized: false`), if it
contains localized fields, these fields share the same ID across
different locales.
- When an array/block **is localized** (`localized: true`), its
descendant fields cannot share the same ID across different locales if
Postgres is being used. This wouldn't be an issue if the table
containing localized blocks had a composite primary key of `locale +
id`. However, since the primary key is just `id`, we need to assign a
new ID for these fields.

This PR properly removes IDs **only for nested fields** whose ancestors
are localized.

Fixes #12536

## Example:
### Before Fix:
```js
// Original document (en)
array: [{
  id: "123",
  text: { en: "English text" }
}]

// After copying to 'es' locale, a new ID was created instead of updating the existing item
array: [{
  id: "456",  // 🐛 New ID created!
  text: { es: "Spanish text" } // 🐛 'en' locale is missing
}]
```
### After fix:
```js
// After fix
array: [{
  id: "123",  //  Same ID maintained
  text: {
    en: "English text",
    es: "Spanish text"  //  Properly merged with existing item
  }
}]
```


## Additional fixes:

### TraverseFields

In the process of designing an appropriate solution, I detected a couple
of bugs in traverseFields that are also addressed in this PR.

### Fixed MongoDB Empty Array Handling

During testing, I discovered that MongoDB and PostgreSQL behave
differently when querying documents that don't exist in a specific
locale:
- PostgreSQL: Returns the document with data from the fallback locale
- MongoDB: Returns the document with empty arrays for localized fields

This difference caused `copyToLocale` to fail in MongoDB because the
merge algorithm only checked for `null` or `undefined` values, but not
empty arrays. When MongoDB returned `content: []` for a non-existent
locale, the algorithm would attempt to iterate over the empty array
instead of using the source locale's data.

### Move test e2e to int

The test introduced in #11887 didn't catch the bug because our e2e suite
doesn't run on Postgres. I migrated the test to an integration test that
does run on Postgres and MongoDB.
2025-07-24 20:37:13 +01:00
Jarrod Flesch
fa7d209cc9 fix(ui): incorrect blocks label sizing (#13264)
Blocks container labels should match the Array and Tab labels. Uses same
styling approach as Array labels.

### Before
<img width="229" height="260" alt="CleanShot 2025-07-24 at 12 26 38"
src="https://github.com/user-attachments/assets/9c4eb7c5-3638-4b47-805b-1206f195f5eb"
/>

### After
<img width="245" height="259" alt="CleanShot 2025-07-24 at 12 27 00"
src="https://github.com/user-attachments/assets/c04933b4-226f-403b-9913-24ba00857aab"
/>
2025-07-24 19:34:29 +00:00
Jacob Fletcher
bccf6ab16f feat: group by (#13138)
Supports grouping documents by specific fields within the list view.

For example, imagine having a "posts" collection with a "categories"
field. To report on each specific category, you'd traditionally filter
for each category, one at a time. This can be quite inefficient,
especially with large datasets.

Now, you can interact with all categories simultaneously, grouped by
distinct values.

Here is a simple demonstration:


https://github.com/user-attachments/assets/0dcd19d2-e983-47e6-9ea2-cfdd2424d8b5

Enable on any collection by setting the `admin.groupBy` property:

```ts
import type { CollectionConfig } from 'payload'

const MyCollection: CollectionConfig = {
  // ...
  admin: {
    groupBy: true
  }
}
```

This is currently marked as beta to gather feedback while we reach full
stability, and to leave room for API changes and other modifications.
Use at your own risk.

Note: when using `groupBy`, bulk editing is done group-by-group. In the
future we may support cross-group bulk editing.

Dependent on #13102 (merged).

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210774523852467

---------

Co-authored-by: Paul Popus <paul@payloadcms.com>
2025-07-24 14:00:52 -04:00
Dan Ribbens
14322a71bb docs(plugin-import-export): document plugin-import-export (#13243)
Add documentation for @payloadcms/plugin-import-export.
2025-07-24 17:03:21 +00:00
Patrik
7e81d30808 fix(ui): ensure document unlocks when logging out from edit view of a locked document (#13142)
### What?

Refactors the `LeaveWithoutSaving` modal to be generic and delegates
document unlock logic back to the `DefaultEditView` component via a
callback.

### Why?

Previously, `unlockDocument` was triggered in a cleanup `useEffect` in
the edit view. When logging out from the edit view, the unlock request
would often fail due to the session ending — leaving the document in a
locked state.

### How?

- Introduced `onConfirm` and `onPrevent` props for `LeaveWithoutSaving`.
- Moved all document lock/unlock logic into `DefaultEditView`’s
`handleLeaveConfirm`.
- Captures the next navigation target via `onPrevent` and evaluates
whether to unlock based on:
  - Locking being enabled.
  - Current user owning the lock.
- Navigation not targeting internal admin views (`/preview`, `/api`,
`/versions`).

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-07-24 09:18:49 -07:00
Sasha
a83ed5ebb5 fix(db-postgres): search is broken when useAsTitle is not specified (#13232)
Fixes https://github.com/payloadcms/payload/issues/13171
2025-07-24 18:42:17 +03:00
Patrik
8f85da8931 fix(plugin-import-export): json preview and downloads preserve nesting and exclude disabled fields (#13210)
### What?

Improves both the JSON preview and export functionality in the
import-export plugin:
- Preserves proper nesting of object and array fields (e.g., groups,
tabs, arrays)
- Excludes any fields explicitly marked as `disabled` via
`custom.plugin-import-export`
- Ensures downloaded files use proper JSON formatting when `format` is
`json` (no CSV-style flattening)

### Why?

Previously:
- The JSON preview flattened all fields to a single level and included
disabled fields.
- Exported files with `format: json` were still CSV-style data encoded
as `.json`, rather than real JSON.

### How?

- Refactored `/preview-data` JSON handling to preserve original document
shape.
- Applied `removeDisabledFields` to clean nested fields using
dot-notation paths.
- Updated `createExport` to skip `flattenObject` for JSON formats, using
a nested JSON filter instead.
- Fixed streaming and buffered export paths to output valid JSON arrays
when `format` is `json`.
2025-07-24 11:36:46 -04:00
Jacob Fletcher
e48427e59a feat(ui): expose refresh method to list drawer context (#13173) 2025-07-24 10:12:45 -04:00
Sasha
7ae4f8c709 docs: add status to forbidden field names when using Postgres and drafts are enabled (#13233)
Fixes https://github.com/payloadcms/payload/issues/13144
2025-07-24 16:29:53 +03:00
Alessio Gravili
1ad7b55e05 refactor(drizzle): use getTableName utility (#13257)
~~Sometimes, drizzle is adding the same join to the joins array twice
(`addJoinTable`), despite the table being the same. This is due to a bug
in `getNameFromDrizzleTable` where it would sometimes return a UUID
instead of the table name.~~

~~This PR changes it to read from the drizzle:BaseName symbol instead,
which is correctly returning the table name in my testing. It falls back
to `getTableName`, which uses drizzle:Name.~~

This for some reason fails the tests. Instead, this PR just uses the
getTableName utility now instead of searching for the symbol manually.
2025-07-24 12:04:16 +00:00
Alessio Gravili
aeee0704dd chore: add new int test verifying that select *improves* performance of new optimization (#13254)
https://github.com/payloadcms/payload/pull/13186 actually made the
select API _more powerful_, as it can reduce the amount of db calls even
for complex collections with blocks down to 1.

This PR adds a test that verifies this.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210871676349303
2025-07-23 16:48:25 -07:00
Jarrod Flesch
29fb9ee5b4 fix(ui): monomorphic relationship fields should not show relationTo option labels (#13245) 2025-07-23 16:31:05 -04:00
Jacob Fletcher
0eac58ed72 fix(next): prevent base list filters from being injected into the url (#13253)
Prevents base list filters from being injected into the URL.

This is a problem with the multi-tenant plugin, for example, where
changing the tenant adds a `baseListFilter` to the query, but should
never be exposed to the end user.

Introduced in #13200.
2025-07-23 15:19:10 -04:00
Sasha
380ce04d5c perf(db-postgres): avoid including prettier to the bundle (#13251)
This PR optimizes bundle size with drizzle adapters by avoiding
including `prettier` to the production bundle
2025-07-23 19:05:31 +03:00
Alessio Gravili
94f5e790f6 perf(drizzle): single-roundtrip db updates for simple collections (#13186)
Currently, an optimized DB update (simple data => no
delete-and-create-row) does the following:
1. sql UPDATE
2. sql SELECT

This PR reduces this further to one single DB call for simple
collections:
1. sql UPDATE with RETURNING()

This only works for simple collections that do not have any fields that
need to be fetched from other tables. If a collection has fields like
relationship or blocks, we'll need that separate SELECT call to join in
the other tables.

In 4.0, we can remove all "complex" fields from the jobs collection and
replace them with a JSON field to make use of this optimization

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210803039809814
2025-07-23 01:45:55 -07:00
Elliot DeNolf
3f8fb6734c ci: default audit-dependencies script to high severity (#13244)
Default the audit-dependencies workflow to use high severity by default.
2025-07-22 16:44:56 -04:00
Jarrod Flesch
412bf4ff73 fix(ui): select all should reset when params change, page, filter, etc (#12612)
Fixes #11938
Fixes https://github.com/payloadcms/payload/issues/13154

When select-all is checked and you filter or change the page, the
selected documents should reset.
2025-07-22 15:23:02 -04:00
Patrik
246a42b727 chore(plugin-import-export): use debug-level logging for createExport process (#13242)
### What?

Replaces all `payload.logger.info` calls with `payload.logger.debug` in
the `createExport` function.

### Why?

info logs are too verbose. Using debug ensures detailed logs.

### How?

- Updated all logger calls in `createExport` to use `debug` instead of
`info`.
2025-07-22 18:09:04 +00:00
Elliot DeNolf
e7a652f0a8 build: suppress pnpm update notification (#13241)
Suppress pnpm update notification
2025-07-22 13:27:44 -04:00
Elliot DeNolf
77f279e768 docs: remove payload cloud (#13240)
Remove Payload Cloud from docs
2025-07-22 13:12:20 -04:00
Sasha
c1cfceb7dc fix(db-mongodb): handle duplicate unique index error for DocumentDB (#13239)
Currently, with DocumentDB instead of a friendly error like "Value must
be unique" we see a generic "Something went wrong" message.
This PR fixes that by adding a fallback to parse the message instead of
using `error.keyValue` which doesn't exist for responses from
DocumentDB.
2025-07-22 16:53:25 +00:00
fgrsource
0eb8f75946 docs: fix typo, example was not valid JSON (#13224)
### What?
A comma is missing in the example code. This results in not valid JSON.

### Why?
I stumbled upon it, while setting up a Tenant-based Payload for the
first time.

### How?
Adding a comma results in valid JSON.

Fixes #
Added a comma. ;)
2025-07-21 15:18:40 +00:00
Chandler Gonzales
af2ddff203 fix: text field validation for minLength: 1, required: false (#13124)
Fixes #13113

### How?

Does not rely on JS falseyness, instead explicitly checking for null &
undefined


I'm not actually certain this is the approach we want to take. Some
people might interpret "required" as not null, not-undefined and min
length > 1 in the case of strings. If they do, this change to the
behavior in the not-required case will break their expectations
2025-07-21 09:23:44 -04:00
Jessica Rynkar
dce898d7ca fix(ui): ensure publishSpecificLocale works during create operation (#13129)
### What?
This PR ensures that when a document is created using the `Publish in
__` button, it is saved to the correct locale.

### Why?
During document creation, the buttons `Publish` or `Publish in [locale]`
have the same effect. As a result, we overlooked the case where a user
may specifically click `Publish in [locale]` for the first save. In this
scenario, the create operation does not respect the
`publishSpecificLocale` value, so the document was always saved in the
default locale regardless of the intended one.

### How?
Passes the `publishSpecificLocale` value to the create operation,
ensuring the document and version is saved to the correct locale.

**Fixes:** #13117
2025-07-21 09:19:51 -04:00
Jarrod Flesch
7f9de6d101 fix: empty folderType arrays break relational dbs (#13219)
Relational databases were broken with folders because it was querying
on:
```ts
{
  folderType: {
    equals: []
  }
}
```

Which does not work since the select hasMany stores values in a separate
table.
2025-07-21 08:39:18 -04:00
German Jablonski
d6e21adaf0 docs: shorten line length in code snippet comments to avoid horizontal scrolling (#13217)
prettier doesn't seem to cover that, and horizontal scrolling in the
browser is even more annoying than in the IDE.
Regex used in the search engine: `^[ \t]*\* `
2025-07-18 15:28:44 +01:00
Jacob Fletcher
d7a3faa4e9 fix(ui): properly sync search params to user preferences (#13200)
Some search params within the list view do not properly sync to user
preferences, and visa versa.

For example, when selecting a query preset, the `?preset=123` param is
injected into the URL and saved to preferences, but when reloading the
page without the param, that preset is not reactivated as expected.

### Problem 

The reason this wasn't working before is that omitting this param would
also reset prefs. It was designed this way in order to support
client-side resets, e.g. clicking the query presets "x" button.

This pattern would never work, however, because this means that every
time the user navigates to the list view directly, their preference is
cleared, as no param would exist in the query.

Note: this is not an issue with _all_ params, as not all are handled in
the same way.

### Solution

The fix is to use empty values instead, e.g. `?preset=`. When the server
receives this, it knows to clear the pref. If it doesn't exist at all,
it knows to load from prefs. And if it has a value, it saves to prefs.
On the client, we sanitize those empty values back out so they don't
appear in the URL in the end.

This PR also refactors much of the list query context and its respective
provider to be significantly more predictable and easier to work with,
namely:

- The `ListQuery` type now fully aligns with what Payload APIs expect,
e.g. `page` is a number, not a string
- The provider now receives a single `query` prop which matches the
underlying context 1:1
- Propagating the query from the server to the URL is significantly more
predictable
- Any new props that may be supported in the future will automatically
work
- No more reconciling `columns` and `listPreferences.columns`, its just
`query.columns`

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210827129744922
2025-07-18 09:29:26 -04:00
iamacup
46d8a26b0d fix: handle undefined values in afterChange hooks when read:false and create:true on the field level access for parents and siblings (#12664)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

-->

### What?

Fixes a bug where `afterChange` hooks would attempt to access values for
fields that are `read: false` but `create: true`, resulting in
`undefined` values and unexpected behavior.

### Why?

In scenarios where access control allows field creation (`create: true`)
but disallows reading it (`read: false`), hooks like `afterChange` would
still attempt to operate on `undefined` values from `siblingDoc` or
`previousDoc`, potentially causing errors or skipped logic.

### How?

Adds safe optional chaining and fallback object initialization in
`promise.ts` for:
- `previousDoc[field.name]`
- `siblingDoc[field.name]`
- Group, Array, and Block field traversals

This ensures that these values are treated as empty objects or arrays
where appropriate to prevent runtime errors during traversal or hook
execution.

Fixes https://github.com/payloadcms/payload/issues/12660

---------

Co-authored-by: Niall Bambury <niall.bambury@cuckoo.co>
2025-07-18 13:34:54 +01:00
Alessio Gravili
c08b2aea89 feat: scheduling jobs (#12863)
Adds a new `schedule` property to workflow and task configs that can be
used to have Payload automatically _queue_ jobs following a certain
_schedule_.

Docs:
https://payloadcms.com/docs/dynamic/jobs-queue/schedules?branch=feat/schedule-jobs

## API Example

```ts
export default buildConfig({
  // ...
  jobs: {
    // ...
    scheduler: 'manual', // Or `cron` if you're not using serverless. If `manual` is used, then user needs to set up running /api/payload-jobs/handleSchedules or payload.jobs.handleSchedules in regular intervals
    tasks: [
      {
        schedule: [
          {
            cron: '* * * * * *',
            queue: 'autorunSecond',
            // Hooks are optional
            hooks: {
              // Not an array, as providing and calling `defaultBeforeSchedule` would be more error-prone if this was an array
              beforeSchedule: async (args) => {
                // Handles verifying that there are no jobs already scheduled or processing.
                // You can override this behavior by not calling defaultBeforeSchedule, e.g. if you wanted
                // to allow a maximum of 3 scheduled jobs in the queue instead of 1, or add any additional conditions
                const result = await args.defaultBeforeSchedule(args)
                return {
                  ...result,
                  input: {
                    message: 'This task runs every second',
                  },
                }
              },
              afterSchedule: async (args) => {
                await args.defaultAfterSchedule(args) // Handles updating the payload-jobs-stats global
                args.req.payload.logger.info(
                  'EverySecond task scheduled: ' +
                  (args.status === 'success' ? args.job.id : 'skipped or failed to schedule'),
                )
              },
            },
          },
        ],
        slug: 'EverySecond',
        inputSchema: [
          {
            name: 'message',
            type: 'text',
            required: true,
          },
        ],
        handler: ({ input, req }) => {
          req.payload.logger.info(input.message)
          return {
            output: {},
          }
        },
      }
    ]
  }
})
```

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210495300843759
2025-07-18 06:48:27 -04:00
Jake Fell
4ae503d700 fix: exit payload jobs:run process after completion (#13211)
### What?

Exit the process after running jobs.

### Why?

When running the `payload jobs:run` bin script with a postgres database
the process hangs forever.

### How?

Execute `process.exit(0)` after running all jobs.
2025-07-17 19:33:49 +00:00
Elliot DeNolf
a3361356b2 chore(release): v3.48.0 [skip ci] 2025-07-17 14:45:59 -04:00
Patrik
95e373e60b fix(plugin-import-export): disabled flag to cascade to nested fields from parent containers (#13199)
### What?

Fixes the `custom.plugin-import-export.disabled` flag to correctly
disable fields in all nested structures including:
- Groups
- Arrays
- Tabs
- Blocks

Previously, only top-level fields or direct children were respected.
This update ensures nested paths (e.g. `group.array.field1`,
`blocks.hero.title`, etc.) are matched and filtered from exports.

### Why?

- Updated regex logic in both `createExport` and Preview components to
recursively support:
  - Indexed array fields (e.g. `array_0_field1`)
  - Block fields with slugs (e.g. `blocks_0_hero_title`)
  - Nested field accessors with correct part-by-part expansion

### How?

To allow users to disable entire field groups or deeply nested fields in
structured layouts.
2025-07-17 18:12:58 +00:00
Jarrod Flesch
12539c61d4 feat(ui): supports collection scoped folders (#12797)
As discussed in [this
RFC](https://github.com/payloadcms/payload/discussions/12729), this PR
supports collection-scoped folders. You can scope folders to multiple
collection types or just one.

This unlocks the possibility to have folders on a per collection instead
of always being shared on every collection. You can combine this feature
with the `browseByFolder: false` to completely isolate a collection from
other collections.

Things left to do:
- [x] ~~Create a custom react component for the selecting of
collectionSlugs to filter out available options based on the current
folders parameters~~


https://github.com/user-attachments/assets/14cb1f09-8d70-4cb9-b1e2-09da89302995


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210564397815557
2025-07-17 13:24:22 -04:00
Alessio Gravili
6ae730b33b feat(richtext-lexical): export $createLinkNode and $isLinkNode for server use (#13205)
Exports `$createLinkNode`, `$isLinkNode` and the equivalent modules for
autolinks.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210710489889573
2025-07-17 09:24:37 -04:00
Sasha
a20b43624b feat: add findDistinct operation (#13102)
Adds a new operation findDistinct that can give you distinct values of a
field for a given collection
Example:
Assume you have a collection posts with multiple documents, and some of
them share the same title:
```js
// Example dataset (some titles appear multiple times)
[
  { title: 'title-1' },
  { title: 'title-2' },
  { title: 'title-1' },
  { title: 'title-3' },
  { title: 'title-2' },
  { title: 'title-4' },
  { title: 'title-5' },
  { title: 'title-6' },
  { title: 'title-7' },
  { title: 'title-8' },
  { title: 'title-9' },
]
```
You can now retrieve all unique title values using findDistinct:
```js
const result = await payload.findDistinct({
  collection: 'posts',
  field: 'title',
})

console.log(result.values)
// Output:
// [
//   'title-1',
//   'title-2',
//   'title-3',
//   'title-4',
//   'title-5',
//   'title-6',
//   'title-7',
//   'title-8',
//   'title-9'
// ]
```
You can also limit the number of distinct results:
```js
const limitedResult = await payload.findDistinct({
  collection: 'posts',
  field: 'title',
  sortOrder: 'desc',
  limit: 3,
})

console.log(limitedResult.values)
// Output:
// [
//   'title-1',
//   'title-2',
//   'title-3'
// ]
```

You can also pass a `where` query to filter the documents.
2025-07-16 17:18:14 -04:00
Sean Zubrickas
cab7ba4a8a fix: Enhances field-level access controls on Users collection to address s… (#13197)
Enhance field-level access controls on Users collection to address
security concerns

- Restricted read/update access on `email` field to admins and the user
themselves
- Locked down `roles` field so only admins can create, read, or update
it
2025-07-16 15:36:32 -04:00
Elliott W
41cff6d436 fix(db-mongodb): improve compatibility with Firestore database (#12763)
### What?

Adds four more arguments to the `mongooseAdapter`:

```typescript
  useJoinAggregations?: boolean  /* The big one */
  useAlternativeDropDatabase?: boolean
  useBigIntForNumberIDs?: boolean
  usePipelineInSortLookup?: boolean
```

Also export a new `compatabilityOptions` object from
`@payloadcms/db-mongodb` where each key is a mongo-compatible database
and the value is the recommended `mongooseAdapter` settings for
compatability.

### Why?

When using firestore and visiting
`/admin/collections/media/payload-folders`, we get:

```
MongoServerError: invalid field(s) in lookup: [let, pipeline], only lookup(from, localField, foreignField, as) is supported
```

Firestore doesn't support the full MongoDB aggregation API used by
Payload which gets used when building aggregations for populating join
fields.

There are several other compatability issues with Firestore:
- The invalid `pipeline` property is used in the `$lookup` aggregation
in `buildSortParams`
- Firestore only supports number IDs of type `Long`, but Mongoose
converts custom ID fields of type number to `Double`
- Firestore does not support the `dropDatabase` command
- Firestore does not support the `createIndex` command (not addressed in
this PR)

### How?

 ```typescript
useJoinAggregations?: boolean  /* The big one */
```
When this is `false` we skip the `buildJoinAggregation()` pipeline and resolve the join fields through multiple queries. This can potentially be used with AWS DocumentDB and Azure Cosmos DB to support join fields, but I have not tested with either of these databases.

 ```typescript
useAlternativeDropDatabase?: boolean
```
When `true`, monkey-patch (replace) the `dropDatabase` function so that
it calls `collection.deleteMany({})` on every collection instead of
sending a single `dropDatabase` command to the database

 ```typescript
useBigIntForNumberIDs?: boolean
```
When `true`, use `mongoose.Schema.Types.BigInt` for custom ID fields of type `number` which converts to a firestore `Long` behind the scenes

```typescript
  usePipelineInSortLookup?: boolean
```
When `false`, modify the sortAggregation pipeline in `buildSortParams()` so that we don't use the `pipeline` property in the `$lookup` aggregation. Results in slightly worse performance when sorting by relationship properties.

### Limitations

This PR does not add support for transactions or creating indexes in firestore.

### Fixes

Fixed a bug (and added a test) where you weren't able to sort by multiple properties on a relationship field.

### Future work

1. Firestore supports simple `$lookup` aggregations but other databases might not. Could add a `useSortAggregations` property which can be used to disable aggregations in sorting.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2025-07-16 15:17:43 -04:00
Elliot DeNolf
e6da384a43 ci: disable bundle analysis for forks (#13198)
The bundle analysis action requires comment permissions which are not
available to PRs from forks.

This PR disables bundle analysis until we can implement this in a
separate workflow as shown in [the docs
here](https://github.com/exoego/esbuild-bundle-analyzer?tab=readme-ov-file#github-action-setup-for-public-repositories).
2025-07-16 12:56:42 -04:00
Alessio Gravili
7cd682c66a perf(drizzle): further optimize postgres row updates (#13184)
This is a follow-up to https://github.com/payloadcms/payload/pull/13060.

There are a bunch of other db adapter methods that use `upsertRow` for
updates: `updateGlobal`, `updateGlobalVersion`, `updateJobs`,
`updateMany`, `updateVersion`.

The previous PR had the logic for using the optimized row updating logic
inside the `updateOne` adapter. This PR moves that logic to the original
`upsertRow` function. Benefits:
- all the other db methods will benefit from this massive optimization
as well. This will be especially relevant for optimizing postgres job
queue initial updates - we should be able to close
https://github.com/payloadcms/payload/pull/11865 after another follow-up
PR
- easier to read db adapter methods due to less code.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210803039809810
2025-07-16 09:45:02 -07:00
jangir-ritik
be8e8d9c7f docs: fix minor typo (#13185) 2025-07-16 05:46:17 +00:00
Sasha
841bf891d0 feat: atomic number field updates (#13118)
Based on https://github.com/payloadcms/payload/pull/13060 which should
be merged first
This PR adds ability to update number fields atomically, which could be
important with parallel writes. For now we support this only via
`payload.db.updateOne`.

For example:
```js
// increment by 10
const res = await payload.db.updateOne({
  data: {
    number: {
      $inc: 10,
    },
  },
  collection: 'posts',
  where: { id: { equals: post.id } },
})

// decrement by 3
const res2 = await payload.db.updateOne({
  data: {
    number: {
      $inc: -3,
    },
  },
  collection: 'posts',
  where: { id: { equals: post.id } },
})
```
2025-07-15 21:53:45 -07:00
Patrik
2a59c5bf8c fix(plugin-import-export): export field dropdown to properly label and path fields in named/unnamed tabs (#13180)
### What?

Fixes the export field selection dropdown to correctly differentiate
between fields in named and unnamed tabs.

### Why?

Previously, when a `tabs` field contained both named and unnamed tabs,
subfields with the same `name` would appear as duplicates in the
dropdown (e.g. `Tab To CSV`, `Tab To CSV`). Additionally, selecting a
field from a named tab would incorrectly map it to the unnamed version
due to shared labels and missing path prefixes.

### How?

- Updated the `reduceFields` utility to manually construct the field
path and label using the tab’s `name` if present.
- Ensured unnamed tabs treat subfields as top-level and skip prefixing
altogether.
- Adjusted label prefix logic to show `Named Tab > Field Name` when
appropriate.


#### Before
<img width="169" height="79" alt="Screenshot 2025-07-15 at 2 55 14 PM"
src="https://github.com/user-attachments/assets/2ab2d19e-41a3-4be2-8496-1da2a79f88e1"
/>

#### After
<img width="211" height="79" alt="Screenshot 2025-07-15 at 2 50 38 PM"
src="https://github.com/user-attachments/assets/0620e96a-71cd-4eb1-9396-30d461ed47a5"
/>
2025-07-15 16:41:07 -04:00
Alessio Gravili
64d76a3869 fix: cron jobs running when calling bin scripts, leading to db errors (#13135)
Previously, we were always initializing cronjobs when calling
`getPayload` or `payload.init`.

This is undesired in bin scripts - we don't want cron jobs to start
triggering db calls while we're running an initial migration using
`payload migrate` for example. This has previously led to a race
condition, triggering the following, occasional error, if job autoruns
were enabled:

```ts
DrizzleQueryError: Failed query: select "payload_jobs"."id", "payload_jobs"."input", "payload_jobs"."completed_at", "payload_jobs"."total_tried", "payload_jobs"."has_error", "payload_jobs"."error", "payload_jobs"."workflow_slug", "payload_jobs"."task_slug", "payload_jobs"."queue", "payload_jobs"."wait_until", "payload_jobs"."processing", "payload_jobs"."updated_at", "payload_jobs"."created_at", "payload_jobs_log"."data" as "log" from "payload_jobs" "payload_jobs" left join lateral (select coalesce(json_agg(json_build_array("payload_jobs_log"."_order", "payload_jobs_log"."id", "payload_jobs_log"."executed_at", "payload_jobs_log"."completed_at", "payload_jobs_log"."task_slug", "payload_jobs_log"."task_i_d", "payload_jobs_log"."input", "payload_jobs_log"."output", "payload_jobs_log"."state", "payload_jobs_log"."error") order by "payload_jobs_log"."_order" asc), '[]'::json) as "data" from (select * from "payload_jobs_log" "payload_jobs_log" where "payload_jobs_log"."_parent_id" = "payload_jobs"."id" order by "payload_jobs_log"."_order" asc) "payload_jobs_log") "payload_jobs_log" on true where ("payload_jobs"."completed_at" is null and ("payload_jobs"."has_error" is null or "payload_jobs"."has_error" <> $1) and "payload_jobs"."processing" = $2 and ("payload_jobs"."wait_until" is null or "payload_jobs"."wait_until" < $3) and "payload_jobs"."queue" = $4) order by "payload_jobs"."created_at" asc limit $5
params: true,false,2025-07-10T21:25:03.002Z,autorunSecond,100
    at NodePgPreparedQuery.queryWithCache (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/pg-core/session.ts:74:11)
    at processTicksAndRejections (node:internal/process/task_queues:105:5)
    at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/node-postgres/session.ts:154:19
    ... 6 lines matching cause stack trace ...
    at N._trigger (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/croner@9.0.0/node_modules/croner/dist/croner.cjs:1:16806) {
  query: `select "payload_jobs"."id", "payload_jobs"."input", "payload_jobs"."completed_at", "payload_jobs"."total_tried", "payload_jobs"."has_error", "payload_jobs"."error", "payload_jobs"."workflow_slug", "payload_jobs"."task_slug", "payload_jobs"."queue", "payload_jobs"."wait_until", "payload_jobs"."processing", "payload_jobs"."updated_at", "payload_jobs"."created_at", "payload_jobs_log"."data" as "log" from "payload_jobs" "payload_jobs" left join lateral (select coalesce(json_agg(json_build_array("payload_jobs_log"."_order", "payload_jobs_log"."id", "payload_jobs_log"."executed_at", "payload_jobs_log"."completed_at", "payload_jobs_log"."task_slug", "payload_jobs_log"."task_i_d", "payload_jobs_log"."input", "payload_jobs_log"."output", "payload_jobs_log"."state", "payload_jobs_log"."error") order by "payload_jobs_log"."_order" asc), '[]'::json) as "data" from (select * from "payload_jobs_log" "payload_jobs_log" where "payload_jobs_log"."_parent_id" = "payload_jobs"."id" order by "payload_jobs_log"."_order" asc) "payload_jobs_log") "payload_jobs_log" on true where ("payload_jobs"."completed_at" is null and ("payload_jobs"."has_error" is null or "payload_jobs"."has_error" <> $1) and "payload_jobs"."processing" = $2 and ("payload_jobs"."wait_until" is null or "payload_jobs"."wait_until" < $3) and "payload_jobs"."queue" = $4) order by "payload_jobs"."created_at" asc limit $5`,
  params: [ true, false, '2025-07-10T21:25:03.002Z', 'autorunSecond', 100 ],
  cause: error: relation "payload_jobs" does not exist
      at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/pg@8.16.3/node_modules/pg/lib/client.js:545:17
      at processTicksAndRejections (node:internal/process/task_queues:105:5)
      at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/node-postgres/session.ts:161:13
      at NodePgPreparedQuery.queryWithCache (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/pg-core/session.ts:72:12)
      at /Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/drizzle-orm@0.44.2_@libsql+client@0.14.0_bufferutil@4.0.8_utf-8-validate@6.0.5__@opentelemetr_asjmtflojkxlnxrshoh4fj5f6u/node_modules/src/node-postgres/session.ts:154:19
      at find (/Users/alessio/Documents/GitHub/payload2/packages/drizzle/src/find/findMany.ts:162:19)
      at Object.updateMany (/Users/alessio/Documents/GitHub/payload2/packages/drizzle/src/updateJobs.ts:26:16)
      at updateJobs (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/queues/utilities/updateJob.ts:102:37)
      at runJobs (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/queues/operations/runJobs/index.ts:181:25)
      at Object.run (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/queues/localAPI.ts:137:12)
      at N.fn (/Users/alessio/Documents/GitHub/payload2/packages/payload/src/index.ts:866:13)
      at N._trigger (/Users/alessio/Documents/GitHub/payload2/node_modules/.pnpm/croner@9.0.0/node_modules/croner/dist/croner.cjs:1:16806) {
    length: 112,
    severity: 'ERROR',
    code: '42P01',
    detail: undefined,
    hint: undefined,
    position: '406',
    internalPosition: undefined,
    internalQuery: undefined,
    where: undefined,
    schema: undefined,
    table: undefined,
    column: undefined,
    dataType: undefined,
    constraint: undefined,
    file: 'parse_relation.c',
    line: '1449',
    routine: 'parserOpenTable'
  }
}
```

This PR makes running crons opt-in using a new `cron` flag. By default,
no cron jobs will be created.
2025-07-15 13:24:50 -04:00
Jarrod Flesch
5f019533d8 fix: types for RenderField fields prop (#13162)
Fixes #7799 

Fixes a type issue where all fields in RenderFields['fields'] admin
properties were being marked as required since we were using `Pick`.
Adds a helper type to allow extracting properties with correct
optionality.
2025-07-15 09:12:33 -04:00
Jacob Fletcher
277448d9c0 docs: performance (#13068)
Payload is designed with performance in mind, but its customizability
means that there are many ways to configure your app that can impact
performance.

While Payload provides several features and best practices to help you
optimize your app's specific performance needs, these are not currently
well surfaced and can be obscure.

Now:

- A high-level performance doc now exists at `/docs/performance`
- There's a new section on performance within the `/docs/queries` doc
- There's a new section on performance within the `/docs/hooks` doc
- There's a new section on performance within the
`/docs/custom-components` doc

This PR also:

- Restructures and elaborates on the `/docs/queries/pagination` docs
- Adds a new `/docs/database/indexing` doc
- More

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210743577153856
2025-07-14 23:55:16 -04:00
Patrik
5839cb61fa feat(plugin-import-export): adds support for disabling fields (#13166)
### What?

Adds support for excluding specific fields from the import-export plugin
using a custom field config.

### Why?

Some fields should not be included in exports or previews. This feature
allows users to flag those fields directly in the field config.

### How?

- Introduced a `plugin-import-export.disabled: true` custom field
property.
- Automatically collects and stores disabled field accessors in
`collection.admin.custom['plugin-import-export'].disabledFields`.
- Excludes these fields from the export field selector, preview table,
and final export output (CSV/JSON).
2025-07-14 17:10:36 -04:00
Elliot DeNolf
f4d951dd04 templates: bump for v3.47.0 (#13161)
🤖 Automated bump of templates for v3.47.0

Triggered by user: @AlessioGr

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-14 13:42:11 -07:00
Patrik
7294cf561d feat(plugin-import-export): adds support for forcing export format via plugin config (#13160)
### What?

Adds a new `format` option to the `plugin-import-export` config that
allows users to force the export format (`csv` or `json`) and hide the
format dropdown from the export UI.

### Why?

In some use cases, allowing the user to select between CSV and JSON is
unnecessary or undesirable. This new option allows plugin consumers to
lock the format and simplify the export interface.

### How?

- Added a `format?: 'csv' | 'json'` field to `ImportExportPluginConfig`.
- When defined, the `format` field in the export UI is:
  - Hidden via `admin.condition`
  - Pre-filled via `defaultValue`
- Updated `getFields` to accept the plugin config and apply logic
accordingly.

### Example

```ts
importExportPlugin({
  format: 'json',
})
2025-07-14 16:19:52 -04:00
Alessio Gravili
4831bae6b5 docs: fix invalid syntax failing the docs import (#13165)
ts is recognized, typescript is not
2025-07-14 19:39:56 +00:00
Alessio Gravili
4c69f8e205 docs: add section on browser environment variables when using experimental-build-mode (#13164)
Just spent an entire hour trying to figure out why my environment
variables are `undefined` on the client. Turns out, when running `pnpm
next build --experimental-build-mode compile`, it skips the environment
variable inlining step.

This adds a new section to the docs mentioning that you can use `pnpm
next build --experimental-build-mode generate-env` to manually inline
them.
2025-07-14 12:30:23 -07:00
Patrik
de53f689e3 feat(plugin-import-export): adds pluginConfig options to disable Save and Download buttons in export UI (#13158)
### What?

Adds support for two new plugin options in the import-export plugin:
- `disableSave`: disables the "Save" button in the export UI view.
- `disableDownload`: disables the "Download" button in the export UI
view.

### Why?

This allows implementers to control user access to export actions based
on context or feature requirements. For example, some use cases may want
to preview an export without saving it, or disable downloads entirely.

### How?

- Injected `disableSave` and `disableDownload` into `admin.custom` for
the `exports` collection.
- Updated the `ExportSaveButton` component to conditionally render each
button based on the injected flags.
- Defaults to `false` if the values are not explicitly set in
`pluginConfig`.

### Example

```ts
importExportPlugin({
  disableSave: true, // Defaults to false
  disableDownload: true, // Defaults to false
})
2025-07-14 11:16:14 -07:00
Alessio Gravili
edd1f02eb5 ci: fix post-release-templates workflow (#13159)
Fixes the post-release-templates workflow by building payload before
running the gen templates script

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210784057163370
2025-07-14 14:03:50 -04:00
Patrick Roelofs
d213c9150d fix(plugin-seo): add localized property to MetaTitleComponent (#12751)
### What?
I noticed the plugin-seo exported field component MetaTitleComponent was
missing a localized field property in the UI. Localization is working,
it just wasnt represented in the UI

### Why?
This improves the localization UI for plugin-seo

### How?
The localized prop wasn't being passed along to the Label component

This implementation is a direct copy of the implementation in the
MetaDescription

https://github.com/payloadcms/payload/blob/main/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx

Screenshot of issue:

![image](https://github.com/user-attachments/assets/fd3fcd2e-169a-4ca0-915d-0ed8d85e6abf)

Co-authored-by: Patrick Roelofs <patrick.roelofs@iquality.nl>
2025-07-13 12:39:30 +00:00
Jarrod Flesch
a57faebfd7 test: move i18n list tests to i18n suite (#13143) 2025-07-12 07:03:26 -04:00
Jarrod Flesch
53ad02a8e8 test: fix flaky uploads test (#13141) 2025-07-12 07:03:12 -04:00
Jarrod Flesch
e5755110a9 fix(plugin-multi-tenant): selector could become hidden (#13134)
The selector could become hidden by:
- logging in with a user that only has 1 tenant
- logging out
- logging in with a user that has more than 1 tenant

Simplifies useEffect usage. Adds e2e test for this case.
2025-07-11 16:34:55 -04:00
Elliot DeNolf
e5f64f7952 chore(release): v3.47.0 [skip ci] 2025-07-11 15:43:44 -04:00
Jarrod Flesch
2cafe494cc fix(ui): disabled and styles add row button correctly (#13140)
Disables add row button using disabled prop from useField - i.e. when
the form is processing or initializing.

This fixes a flaky array test that clicks the button before the form has
finished initializing/processing.

Also corrects the add row button color styles with specificity.
2025-07-11 14:07:51 -04:00
German Jablonski
576644d0b5 docs(richtext-lexical): add documentation page about official features (#13132)
It was evident from the number of users asking questions about how to
use the features that a dedicated page was needed.

Preview:

https://payloadcms.com/docs/dynamic/rich-text/official-features?branch=features-docs
2025-07-11 10:02:06 -07:00
Aaron Claes
8a3b97c643 feat(ui): add API key visibility toggle (#13110) 2025-07-11 12:50:38 -04:00
Patrik
06ef798653 fix(ui): ensure buildFormStateHandler throws error instead of returning null for unauthorized requests (#13123)
### What?

Prevents `buildFormStateHandler` from returning `null` in unauthorized
scenarios by throwing an explicit `Error` instead.

### Why?

The `BuildFormStateResult` type does not include `null`, but previously
the handler returned `null` when access was unauthorized. This caused
runtime type mismatches and forced client-side workarounds (e.g.
guarding destructures).

By always throwing instead of returning `null`, the client code can
safely assume a valid result or catch errors.

<img width="1772" height="723" alt="Screenshot_2025-07-10_185618"
src="https://github.com/user-attachments/assets/d65344e3-a2cb-4ec5-91bf-a353b5b7dd14"
/>

### How?

- Replaced the `return null` with `throw new Error('Unauthorized')` in
`buildFormStateHandler`.
- Client code no longer needs to handle `null` responses from
`getFormState`.
2025-07-11 12:19:11 -04:00
Jessica Rynkar
5695d22a46 fix: execute mimetype validation on the file buffer data (#13117)
### What
Introduces an additional `mimeType` validation based on the actual file
data to ensure the uploaded file matches the allowed `mimeTypes` defined
in the upload config.

### Why?
The current validation relies on the file extension, which can be easily
manipulated. For example, if only PDFs are allowed, a JPEG renamed to
`image.pdf` would bypass the check and be accepted. This change prevents
such cases by verifying the true MIME type.

### How?
Performs a secondary validation using the file’s binary data (buffer),
providing a more reliable MIME type check.

Fixes #12905
2025-07-11 16:56:55 +01:00
Jarrod Flesch
19a3367972 fix(ui): monomorphic joins tables not fetching draft documents (#13139)
Monomorphic join fields were not using the `draft` argument when
fetching documents to display in the table. This change makes the join
field treatment of drafts consistent with the `relationship` type
fields.

Added e2e test to cover.
2025-07-11 14:26:48 +00:00
Patrik
c1bad0115a fix(plugin-import-export): sync export field selection with list view columns from query columns (#13131)
### What?

Updated the `FieldsToExport` component to use the current list view
query (`query.columns`) instead of saved preferences to determine which
fields to export.

### Why?

Previously, the export field selection was based on collection
preferences, which are only updated on page reload. This caused stale or
incorrect field sets to be exported if the user changed visible columns
without refreshing.

### How?

- Replaced `getPreference` usage with `useListQuery` to access
`query.columns`
- Filtered out excluded fields (those prefixed with `-`) to get only the
visible columns
- Fallbacks to `defaultColumns` if `query.columns` is not available
2025-07-10 19:02:05 +00:00
Patrik
b3a994ed6f feat(plugin-import-export): show delayed toast when export download takes time (#13126)
### What?

Added a delayed toast message to indicate when an export is being
processed, and disabled the download button unless the export form has
been modified.

### Why?

Previously, there was no feedback during longer export operations, which
could confuse users if the request took time to complete. Also, the
download button was always enabled, even when the form had not been
modified — which could lead to unnecessary exports.

### How?

- Introduced a 200ms delay before showing a "Your export is being
processed..." toast
- Automatically dismisses the toast once the download completes or fails
- Hooked into `useFormModified` to:
  - Track whether the export form has been changed
  - Disable the download button when the form is unmodified
  - Reset the modified state after triggering a download
2025-07-10 14:55:46 -04:00
Paul
f63dfad565 fix(ui): ensure that schedule publishing time picker can only be in the future (#13128)
Previously you could've selected a date and time in the past to schedule
publish.

Now we ensure that there is a minimum time and date for scheduled
publishing date picker.


Additionally updated the disabled items to be more visually obvious that
they are disabled:
<img width="404" height="336" alt="image"
src="https://github.com/user-attachments/assets/1f4ea36a-267e-4ae5-91e4-92bb84d7889c"
/>
2025-07-10 11:01:55 -07:00
Paul
2d91cb613c feat: allow joins, select, populate, depth and draft to /me REST API operation (#13116)
While we can use `joins`, `select`, `populate`, `depth` or `draft` on
auth collections when finding or finding by ID, these arguments weren't
supported for `/me` which meant that in some situations like in our
ecommerce template we couldn't optimise these calls.

A workaround would be to make a call to `/me` and then get the user ID
to then use for a `findByID` operation.
2025-07-10 17:44:05 +00:00
Jarrod Flesch
0c2b1054e2 fix: login operation not returning collection and _strategy (#13119)
The login operation with sessions enabled calls updateOne, in mongodb,
data that does not match the schema is removed. `collection` and
`_strategy` are not part of the schema so they need to be reassigned
after the user is updated.

Adds int test.
2025-07-10 12:13:01 -04:00
Paul
cb6a73e1b4 feat(storage-*): include modified headers into the response headers of files when using adapters (#12096)
This PR makes it so that `modifyResponseHeaders` is supported in our
adapters when set on the collection config. Previously it would be
ignored.

This means that users can now modify or append new headers to what's
returned by each service.

```ts
import type { CollectionConfig } from 'payload'

export const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    modifyResponseHeaders: ({ headers }) => {
      const newHeaders = new Headers(headers) // Copy existing headers
      newHeaders.set('X-Frame-Options', 'DENY') // Set new header

      return newHeaders
    },
  },
}
```

Also adds support for `void` return on the `modifyResponseHeaders`
function in the case where the user just wants to use existing headers
and doesn't need more control.

eg:

```ts
import type { CollectionConfig } from 'payload'

export const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    modifyResponseHeaders: ({ headers }) => {
      headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning
    },
  },
}
```

Manual testing checklist (no CI e2es setup for these envs yet):
- [x] GCS
- [x] S3
- [x] Azure
- [x] UploadThing
- [x] Vercel Blob

---------

Co-authored-by: James <james@trbl.design>
2025-07-10 08:00:26 -07:00
Sasha
055cc4ef12 perf(db-postgres): simplify db.updateOne to a single DB call with if the passed data doesn't include nested fields (#13060)
In case, if `payload.db.updateOne` received simple data, meaning no:
* Arrays / Blocks
* Localized Fields
* `hasMany: true` text / select / number / relationship fields
* relationship fields with `relationTo` as an array

This PR simplifies the logic to a single SQL `set` call. No any extra
(useless) steps with rewriting all the arrays / blocks / localized
tables even if there were no any changes to them. However, it's good to
note that `payload.update` (not `payload.db.updateOne`) as for now
passes all the previous data as well, so this change won't have any
effect unless you're using `payload.db.updateOne` directly (or for our
internal logic that uses it), in the future a separate PR with
optimization for `payload.update` as well may be implemented.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210710489889576
2025-07-10 16:49:12 +03:00
Jarrod Flesch
c77b39c3b4 fix(ui): hidden input should wait for form initialization (#13114) 2025-07-10 06:15:09 -04:00
Germán Jabloñski
5e82f9ff41 feat(next): redirect non-existent documents to list view with banner (#13062)
Currently, when a nonexistent document is accessed via the URL, a
`NotFound` page is displayed with a button to return to the dashboard.

In most cases, the next step the user will take is to navigate to the
list of documents in that collection. If we automatically redirect users
to the list view and display the error in a banner, we can save them a
couple of redirects.

This is a very common scenario when writing tests or restarting the
local environment.


## Before


![image](https://github.com/user-attachments/assets/ea7af410-5567-4dd2-b44b-67177aa795e6)


## After

![image](https://github.com/user-attachments/assets/72b38d2f-63f2-4a2b-94c4-76ea90d80c24)
2025-07-10 03:10:37 -07:00
Patrik
c6105f1e0d fix(plugin-import-export): flattening logic for polymorphic relationships in CSV exports (#13094)
### What?

Improves the flattening logic used in the import-export plugin to
correctly handle polymorphic relationships (both `hasOne` and `hasMany`)
when generating CSV columns.

### Why?

Previously, `hasMany` polymorphic relationships would flatten their full
`value` object recursively, resulting in unwanted keys like `createdAt`,
`title`, `email`, etc. This change ensures that only the `id` and
`relationTo` fields are included, matching how `hasOne` polymorphic
fields already behave.

### How?

- Updated `flattenObject` to special-case `hasMany` polymorphic
relationships and extract only `relationTo` and `id` per index.
- Refined `getFlattenedFieldKeys` to return correct column keys for
polymorphic fields:
  - `hasMany polymorphic → name_0_relationTo`, `name_0_id`
  - `hasOne polymorphic → name_relationTo`, `name_id`
  - `monomorphic → name` or `name_0`
- **Added try/catch blocks** around `toCSVFunctions` calls in
`flattenObject`, with descriptive error messages including the column
path and input value. This improves debuggability if a custom `toCSV`
function throws.
2025-07-09 15:46:48 -04:00
Patrik
0806ee1762 fix(plugin-import-export): selectionToUse field to dynamically show valid export options (#13092)
### What?

Updated the `selectionToUse` export field to properly render a radio
group with dynamic options based on current selection state and applied
filters.

- Fixed an edge case where `currentFilters` would appear as an option
even when the `where` clause was empty (e.g. `{ or: [] }`).

### Why?

Previously, the `selectionToUse` field displayed all options (current
selection, current filters, all documents) regardless of context. This
caused confusion when only one of them was applicable.

### How?

- Added a custom field component that dynamically computes available
options based on:
  - Current filters from `useListQuery`
  - Selection state from `useSelection`
- Injected the dynamic `field` prop into `RadioGroupField` to enable
rendering.
- Ensured the `where` field updates automatically in sync with the
selected radio.
- Added `isWhereEmpty` utility to avoid showing `currentFilters` when
`query.where` contains no meaningful conditions (e.g. `{ or: [] }`).
2025-07-09 15:44:22 -04:00
Alessio Gravili
e99c67f5f9 fix: ensure we perform ssrf check within dispatcher (#13078)
Previously, we were performing this check before calling the fetch
function. This changes it to perform the check within the dispatcher.

It adjusts the int tests to both trigger the dispatcher lookup function
(which is only triggered when not already passing a valid IP) and the
check before calling fetch

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210733180484570
2025-07-09 12:42:26 -07:00
Patrik
1c6a79bb57 fix(plugin-import-export): sync field select dropdown with form value (#13103)
### What?

Fixes a sync issue between the "Fields to Export" `<ReactSelect />`
dropdown and the underlying form state in the import-export plugin.

### Why?

Previously, the dropdown displayed outdated selections until an extra
click occurred. This was caused by an unnecessary `useState`
(`displayedValue`) that fell out of sync with the `useField` form value.

### How?

- Removed the separate `displayedValue` state
- Derived the selected values directly from the form field value using
inline mapping
2025-07-09 15:42:06 -04:00
Germán Jabloñski
a7a05012fb feat(next): add redirect from ${adminRoute}/collections to ${adminRoute} (#13061)
Occasionally, I find myself on a URL like
`https://domain.com/admin/collections/myCollection/docId` and I modify
the URL with the intention of going to the admin panel, but I shorten it
in the wrong place: `https://domain.com/admin/collections`.

The confusion arises because the admin panel basically displays the
collections.

I think this redirect is a subtle but nice touch, since `/collections`
is a URL that doesn't exist.

EDIT: now I'm doing also the same thing for `/globals`
2025-07-09 10:39:02 -04:00
Said Akhrarov
1d6ffcb80e feat(ui): adds support for copy pasting complex fields (#11513)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR introduces support for copy + pasting complex fields such as
Arrays and Blocks. These changes introduce a new `ClipboardAction`
component that houses logic for copy + pasting to and from the clipboard
to supported fields. I've scoped this PR to include only Blocks &
Arrays, however the structure of the components introduced lend
themselves to be easily extended to other field types. I've limited the
scope because there may be design & functional blockers that make it
unclear how to add actions to particular fields.

Supported fields:
- Arrays
([Demo](https://github.com/user-attachments/assets/523916f6-77d0-43e2-9a11-a6a9d8c1b71c))
- Array Rows
([Demo](https://github.com/user-attachments/assets/0cd01a1f-3e5e-4fea-ac83-8c0bba8d1aac))
- Blocks
([Demo](https://github.com/user-attachments/assets/4c55ac2b-55f4-4793-9b53-309b2e090dd9))
- Block Rows
([Demo](https://github.com/user-attachments/assets/1b4d2bea-981a-485b-a6c4-c59a77a50567))

Fields that may be supported in the future with minimal effort by
adopting the changes introduced here:
- Tabs
- Groups
- Collapsible
- Relationships

This PR also encompasses e2e tests that check both field and row-level
copy/pasting.

### Why?
To make it simpler and faster to copy complex fields over between
documents and rows within those docs.

### How?
Introduces a new `ClipboardAction` component with helper utilities to
aid in copy/pasting and validating field data.

Addresses #2977 & #10703

Notes:
- There seems to be an issue with Blocks & Arrays that contain RichText
fields where the RichText field dissappears from the dom upon replacing
form state. These fields are resurfaced after either saving the data or
dragging/dropping the row containing them.
- Copying a Row and then pasting it at the field-level will overwrite
the field to include only that one row. This is intended however can be
changed if requested.
- Clipboard permissions are required to use this feature. [See Clipboard
API caniuse](https://caniuse.com/async-clipboard).

#### TODO
- [x] ~~I forgot BlockReferences~~
- [x] ~~Fix tests failing due to new buttons causing locator conflicts~~
- [x] ~~Ensure deeply nested structures work~~
- [x] ~~Add missing translations~~
- [x] ~~Implement local storage instead of clipboard api~~
- [x] ~~Improve tests~~

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-07-09 13:59:22 +00:00
Elliot DeNolf
dde9681089 ci: use .tool-versions file in setup (#13093)
Parse `.tool-versions` file in the composite node setup action. This
will make it the source of truth and easier to bump node/pnpm versions
in the future.
2025-07-08 21:20:31 +00:00
Elliot DeNolf
c876ddf858 ci: audit-dependencies workflow (#13090)
Add weekly check for dependency vulnerabilities.

Asana:
https://app.asana.com/1/10497086658021/project/1210456585958356/task/1210561338171143
2025-07-08 14:42:24 -04:00
Jarrod Flesch
855a320474 fix: ensure default values are not shown when value is hidden (#13074)
Fixes #12834 

`loginAttempts` was being shown in the admin panel when it should be
hidden. The field is set to `hidden: true` therefore the value is
removed from siblingData and passes the `allowDefaultValue` check -
showing inconsistent data.

This PR ensures the default value is not returned if the field has a
value but was removed due to the field being hidden.
2025-07-08 13:34:10 -04:00
Jarrod Flesch
aa97f3cddb fix: correctly reset login attempts (#13075)
Login attempts were not being reset correctly which led to situations
where a failed login attempt followed by a successful login attempt
would keep the loginAttempts at 1.


### Before 
Example with maxAttempts of 2:
- failed login -> `loginAttempts: 1`
- successful login -> `loginAttempts: 1`
- failed login -> `loginAttempts: 2`
- successful login -> `"This user is locked due to having too many
failed login attempts."`

### After 
Example with maxAttempts of 2:
- failed login -> `loginAttempts: 1`
- successful login -> `loginAttempts: 0`
- failed login -> `loginAttempts: 1`
- successful login -> `loginAttempts: 0`
2025-07-08 13:32:16 -04:00
Jacob Fletcher
0b88466de6 fix(next): prevent live preview url functions from firing unnecessarily (#13088)
Ensures Live Preview url functions aren't fired during create or on
collections that do not have Live Preview enabled.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210743577153852
2025-07-08 13:02:54 -04:00
Elliot DeNolf
fee33b59ee ci: blank audit-dependencies workflow [skip ci] 2025-07-08 12:28:53 -04:00
Elliot DeNolf
417b70e16c chore(deps): bump deps to resolve all high severity (#13002)
Bumps dependencies to resolve all `high` severity vulnerabilities
2025-07-08 11:42:41 -04:00
Jessica Rynkar
9f1bff57c1 feat: exports new sanitizeUserDataForEmail function (#13029)
### What?

Adds a new `sanitizeUserDataForEmail` function, exported from
`payload/shared`.
This function sanitizes user data passed to email templates to prevent
injection of HTML, executable code, or other malicious content.

### Why?

In the existing `email` example, we directly insert `user.name` into the
generated email content. Similarly, the `newsletter` collection uses
`doc.name` directly in the email content. A security report identified
this as a potential vulnerability that could be exploited and used to
inject executable or malicious code.

Although this issue does not originate from Payload core, developers
using our examples may unknowingly introduce this vulnerability into
their own codebases.

### How?

Introduces the pre-built `sanitizeUserDataForEmail` function and updates
relevant email examples to use it.

**Fixes `CMS2-1225-14`**
2025-07-08 12:47:34 +01:00
Dani Calero
4c25357831 fix(ui): improve alignment of clear and dropdown indicator buttons in select based fields (#12995) 2025-07-08 07:06:46 -04:00
Jessica Rynkar
8a5cb27463 fix(ui): prevent error crashing UI when relationship assigned as useAsTitle (#12981)
### What?

- Updates the `RenderTitle` component to check that the `title` is a
string before returning it.
- Adds note to docs that **Relationship** and **Join** fields cannot be
assigned to `useAsTitle`, a **virtual** field should be used instead.

### Why?
When autosave is enabled and the `useAsTitle` points to a relationship
field, the autosave process returns an `object` for the title, this gets
passed to the `RenderTitle` component and throws an error which crashes
the UI.

### How?
Safely checks that `title` is a string before rendering it in
`RenderTitle` and updates docs to clarify that Relationship/Joins are
not compatible with `useAsTitle`.

Fixes #12960
2025-07-08 10:55:04 +01:00
Dan Ribbens
9c453210f8 fix: payload auth api-key algorithm compatibility (#13076)
When saving api-keys in prior versions you can have sha1 generated
lookup keys. This ensures compatibility with newer sha256 lookups.
2025-07-07 21:23:02 -04:00
Adam Klingbaum
96c24a22da docs(templates): fix grammar in README (#13027)
## Summary

Fixed a grammatical error in the README files for the website templates.

## Changes

- Fixed grammar in the on-demand revalidation section: changed "or
footer or header, change they will" to "footer, or header changes will"

## Files Changed

- `templates/website/README.md`
- `templates/with-vercel-website/README.md`

## Type of Change

- [x] Documentation fix/improvement
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change

This fixes a typo that was making the sentence grammatically incorrect
and hard to read.
2025-07-07 17:41:44 -04:00
Elliot DeNolf
14612b4db8 chore(release): v3.46.0 [skip ci] 2025-07-07 16:10:10 -04:00
Patrik
e6f8ca6fd0 fix: deduplicate custom array id fields (#13064)
When adding a custom ID field to an array's config, both the default
field provided by Payload, and the custom ID field, exist in the
resulting config. This can lead to problems when the looking up the
field's config, where either one or the other will be returned.

Fixes #12978
2025-07-07 13:06:31 -07:00
Kendell
ba660fdea2 feat: adds restricted file check (#12989)
Adds `restrictedFileTypes` (default: `false`) to upload collections
which prevents files on a restricted list from being uploaded.

To skip this check:
- set `[Collection].upload.restrictedFileTypes` to `true`
- set `[Collection].upload.mimeType` to any type(s)
2025-07-07 16:04:34 -04:00
Alessio Gravili
af9837de44 ci: analyze bundle size (#13071)
This adds a new `analyze` step to our CI that analyzes the bundle size
for our `payload`, `@payloadcms/ui`, `@payloadcms/next` and
`@payloadcms/richtext-lexical` packages.

It does so using a new `build:bundle-for-analysis` script that packages
can add if the normal build step does not output an esbuild-bundled
version suitable for analyzing. For example, `ui` already runs esbuild,
but we run it again using `build:bundle-for-analysis` because we do not
want to split the bundle.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210692087147570
2025-07-07 20:00:02 +00:00
Jessica Rynkar
f4f13a26c7 fix(next): adds token to reset password initialState (#13067)
### What?
Adds `token` into the `initialState` for the reset password form to
ensure `token` does not get passed through as `undefined`.

### Why?
Currently the reset password UI is broken because `token` is not getting
passed to the form state.

### How?
Adds `token` to `initialState`

Fixes #13040
2025-07-07 19:09:11 +00:00
Jarrod Flesch
6d5cc843a2 fix(db-mongodb): updateOne mutates the data object and does not transform it for read (#13065)
Fixes https://github.com/payloadcms/payload/issues/13045

`updateOne` when returning is `false` mutates the data object for write
operations on the DB, but that causes an issue when using that data
object later on since all of the id's are mutated to objectIDs and never
transformed back into read id's.

This fix ensures that the transform happens even when the result is not
returned.
2025-07-07 14:50:01 -04:00
Jarrod Flesch
34920a7ec0 test: fix tests that rely on remote urls (#13073) 2025-07-07 14:02:55 -04:00
Germán Jabloñski
2650eb7d44 fix(ui): increase timeout for opening list drawer in RelationshipInput (#13031)
As stated in #12529, the setTimeout was defined through trial and error
as it wasn't possible to reproduce the bug with the devtools open and
therefore with the CPU throttled. One user reported still experiencing
the bug.

I'm increasing the timeout to 100ms, which seems acceptable enough to
keep postponing a better fix, considering the bug isn't that critical.

If we find it keeps happening, we'll probably need to investigate the
root cause.
2025-07-07 09:30:09 -04:00
Jessica Rynkar
50c2f8bec2 fix(plugin-redirects): make 'from' field unique to prevent errors in redirect logic (#12964)
### What?
This PR updates the `from` field in `plugin-redirects` to add `unique:
true`.

### Why?
If you create multiple redirects with the same `from` URL — the
application won't know which one to follow, which causes errors and
unpredictable behavior.

### How?
Adds `unique: true` to the plugin injected `from` field.

### Migration Required
This change will require a migration. Projects already using this plugin
will need to:
- Ensure there are no duplicate `from` values in their existing
redirects collection.
- Remove or modify any duplicate entries before applying this update.

Fixes #12959
2025-07-07 11:21:40 +01:00
Jacob Fletcher
f49eeb1a63 fix(next): respect collection-level live preview config (#13036)
Fixes #13035.

We broke collection-level live preview configs in #12860.
2025-07-03 21:47:16 +00:00
Jarrod Flesch
1d9ad6f2f1 fix(ui): change password button is hidden when user has full field access (#12988) 2025-07-03 13:59:22 -04:00
Kendell
30fc7e3012 fix: check hostname of upload url (#13018)
Adds:
```ts
import { lookup } from 'dns/promises'
// ...
const { address } = await lookup(hostname)
// ...
return isSafeIp(address)
```

To ensure that an `ip` address is being verified. Previously, hostnames
were being verified by `isSafeIp`.


Fixes: https://github.com/payloadcms/payload/issues/12876
2025-07-03 10:50:31 -04:00
Elliot DeNolf
1ccd7ef074 chore(release): v3.45.0 [skip ci] 2025-07-03 09:23:23 -04:00
Patrik
34c3a5193b fix(plugin-import-export): pre-scan columns before streaming CSV export (#13009)
### What?

Fixes an issue where only the fields from the first batch of documents
were used to generate CSV column headers during streaming exports.

### Why?

Previously, columns were determined during the first streaming batch. If
a field appeared only in later documents, it was omitted from the CSV
entirely — leading to incomplete exports when fields were sparsely
populated across the dataset.

### How?

- Adds a **pre-scan step** before streaming begins to collect all column
keys across all pages
- Uses this superset of keys to define the final CSV header
- Ensures every row is padded to match the full column set

This matches the behavior of non-streamed exports and guarantees that
the streamed CSV output includes all relevant fields, regardless of when
they appear in pagination.
2025-07-03 08:53:02 -04:00
Sasha
81532cb9c9 fix(db-mongodb): nested sorting by ID (#13016)
Fixes sorting when the `sort` path contains a relationship and ends with
`id`, for example `sort: 'post.category.id'`.
2025-07-03 08:51:45 -04:00
Sebastian Blank
f70c6fe3e7 fix(templates): wrong link in demo content (custom components) (#13024)
### What?

The "custom component" link in the dashboard of the website demo is
wrong:

![image](https://github.com/user-attachments/assets/ee716a87-c515-4561-932d-f1c1fcccfd5e)
2025-07-03 12:07:19 +00:00
Alessio Gravili
e6b664284f chore: fix payload bundle script (#13022)
This fixes the payload bundle script. While not run by default, it's
useful for checking the payload bundle size by manually running `cd
packages/payload && node bundle.js`.
2025-07-03 04:37:44 -07:00
Alessio Gravili
fafaa04e1a fix(drizzle): ensure updateOne does not create new document if where query has no results (#12991)
Previously, `db.updateOne` calls with `where` queries that lead to no
results would create new rows on drizzle. Essentially, `db.updateOne`
behaved like `db.upsertOne` on drizzle
2025-07-02 13:56:59 -07:00
Germán Jabloñski
babcd599da fix(ui): save nested richtext inside inlineBlock (#12773)
Removing the `setTimeout` not only doesn't break any tests, but it also
fixes the linked issue.

The long comment above the if statement was added in
https://github.com/payloadcms/payload/pull/5460 and explains why the if
statement is necessary GIVEN the existence of the `setTimeout`, but the
`setTimeout` was introduced [earlier because the button apparently
didn't work](https://github.com/payloadcms/payload/issues/1414).

It seems to work now without the `setTimeout`, because otherwise the
tests wouldn't even pass. I also tested it manually, and it works fine.


Fixes #12687
2025-07-02 19:43:48 +00:00
Jessica Rynkar
ac19b78968 style(richtext-lexical): ensure error state is shown at small-break (#12827)
### What?
Shows error state (red left border) on small screens.

### Why?
The current error state disappears at small-break screen width.

### How?
Updates small-break error state to match the desktop error state for the
Lexical field.

##### Reported by client.
2025-07-02 12:16:50 -07:00
Jacob Fletcher
b40c581a27 fix(ui): autosave infinite loop within document drawer (#13007)
Required for #13005.

Opening an autosave-enabled document within a drawer triggers an
infinite loop when the root document is also autosave-enabled.

This was for two reasons:

1. Autosave would run and change the `updatedAt` timestamp. This would
trigger another run of autosave, and so on. The timestamp is now removed
before comparison to ensure that sequential autosave runs are skipped.

2. The `dequal()` call was not being given the `.current` property off
the ref object. This meant that is was never evaluate to `true` and
therefore never skip unnecessary autosaves to begin with.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210697235723932
2025-07-02 15:11:38 -04:00
Patrik
335af1b8c9 fix(plugin-import-export): preview table to include all selected columns regardless of populated data (#12985)
### What?

Ensure the export preview table includes all field keys as columns, even
if those fields are not populated in any of the returned documents.

### Why?

Previously, if none of the documents in the preview result had a value
for a given field, that column would be missing entirely from the
preview table.

### How?

- Introduced a `getFlattenedFieldKeys` utility that recursively extracts
all missing flattened field accessors from the collection’s config that
are undefined

- Updates the preview UI logic to build columns from all flattened keys,
not just the first document
2025-07-02 09:28:21 -07:00
Alessio Gravili
583a733334 feat(drizzle): support half-precision, binary, and sparse vectors column types (#12491)
Adds support for `halfvec` and `sparsevec` and `bit` (binary vector)
column types. This is required for supporting indexing of embeddings >
2000 dimensions on postgres using the pg-vector extension.
2025-07-02 19:24:53 +03:00
Jessica Rynkar
6e5ddc8873 fix(examples): only allow super admins to create users with super admin role (#13015)
### What?

This PR updates the `create` access control on the `users` collection in
the `multi-tenant` example to prevent unauthorized creation of
`super-admin` users.

### Why?

Previously, any authenticated user could create a new user and assign
them the `super-admin` role — even if they didn’t have that role
themselves. This bypassed role-based restrictions and introduced a
security vulnerability, allowing users to escalate their own privileges
by working around role restrictions during user creation.

### How?

The `create` access function now checks whether the current user has the
`super-admin` role before allowing the creation of another
`super-admin`. If not, the request is denied.


**Fixes:** `CMS2-Q225-01`
2025-07-02 15:42:55 +01:00
Jarrod Flesch
9ba740e472 fix(ui): field bulk upload showing stale data (#13006) 2025-07-02 10:11:51 -04:00
Jessica Rynkar
50029532aa fix(examples): checks requested tenant matches user tenant permissions (#13012)
### What

This PR updates the `create` access control functions in the
`multi-tenant` example to ensure that any `tenant` specified in a create
request matches a tenant the user has admin access to.

### Why

Previously, while the admin panel UI restricted the tenant selection, it
was still possible to bypass this by making a request directly to the
API with a different `tenant`. This allowed users to create documents
under tenants they shouldn't have access to.

### How

The `access` functions on the `users` and `pages` collections now
explicitly check whether the tenant(s) in the request are included in
the user's tenant permissions. If not, access is denied by returning
`false`.

**Fixes: CMS2-Q225-03**
2025-07-02 14:30:47 +01:00
Jacob Fletcher
c80b6e92c4 fix(ui): prevent document drawer from remounting on save (#13005)
Supersedes #12992. Partially closes #12975.

Right now autosave-enabled documents opened within a drawer will
unnecessarily remount on every autosave interval, causing loss of input
focus, etc. This makes it nearly impossible to edit these documents,
especially if the interval is very short.

But the same is true for non-autosave documents when "manually" saving,
e.g. pressing the "save draft" or "publish changes" buttons. This has
gone largely unnoticed, however, as the user has already lost focus of
the form to interact with these controls, and they somewhat expect this
behavior or at least accept it.

Now, the form remains mounted across autosave events and the user's
cursor never loses focus. Much better.

Before:


https://github.com/user-attachments/assets/a159cdc0-21e8-45f6-a14d-6256e53bc3df

After:


https://github.com/user-attachments/assets/cd697439-1cd3-4033-8330-a5642f7810e8

Related: #12842

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210689077645986
2025-07-02 09:07:08 -04:00
Jarrod Flesch
a9580e05ac fix: disable graphql introspection queries when disableIntrospectionInProduction is true (#12982) 2025-07-02 08:33:20 -04:00
Jarrod Flesch
57d00ad2e9 test: reduce queue test amount (#13008) 2025-07-01 15:55:16 -04:00
Jarrod Flesch
a9ad7c771e fix(ui): bulk upload redirecting to relationship documents when added (#13001)
Fixes https://github.com/payloadcms/payload/issues/12786
2025-07-01 15:23:11 -04:00
Patrik
7a40a9fc06 fix(ui): skip disabled fields when adding OR filter conditions in list view (#13004)
### What?

Fixes a bug where adding an additional OR filter condition in the list
view selects a field with `admin.disableListFilter: true`, causing all
filter fields to appear disabled.

### Why?

When the first field in a collection has `disableListFilter` set to
`true`, adding a second OR condition defaults to using that field. This
leads to a broken filter UI where no valid fields are selectable.

### How?

Replaces the hardcoded usage of `reducedFields[0]` with a call to
`reducedFields.find(...) `that skips fields with `disableListFilter:
true`, consistent with the logic already used when adding the first
filter condition.

Fixes #12993
2025-07-01 11:35:48 -07:00
Patrik
b1ae749311 fix(ui): render preview sizes button when adjustments are disabled but image sizes are defined (#12999)
### What?

The "Preview Sizes" button in the file upload UI was not showing up if:
- `crop` and `focalPoint` were both `false`
- No `customUploadActions` were provided
- But image sizes were configured

### Why?

This happened because `UploadActions` wasn’t rendered at all unless
adjustments or custom actions were present.

### How?

Update the conditional in `StaticFileDetails` to also render
`UploadActions` when:
- `hasImageSizes` is `true` and the document has a `filename`

Fixes #12832
2025-07-01 07:44:48 -07:00
854 changed files with 37497 additions and 7422 deletions

View File

@@ -4,24 +4,17 @@ description: |
inputs:
node-version:
description: Node.js version
required: true
default: 23.11.0
description: Node.js version override
pnpm-version:
description: Pnpm version
required: true
default: 9.7.1
description: Pnpm version override
pnpm-run-install:
description: Whether to run pnpm install
required: false
default: true
pnpm-restore-cache:
description: Whether to restore cache
required: false
default: true
pnpm-install-cache-key:
description: The cache key for the pnpm install cache
default: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
description: The cache key override for the pnpm install cache
outputs:
pnpm-store-path:
@@ -37,15 +30,44 @@ runs:
shell: bash
run: sudo ethtool -K eth0 tx off rx off
- name: Get versions from .tool-versions or use overrides
shell: bash
run: |
# if node-version input is provided, use it; otherwise, read from .tool-versions
if [ "${{ inputs.node-version }}" ]; then
echo "Node version override provided: ${{ inputs.node-version }}"
echo "NODE_VERSION=${{ inputs.node-version }}" >> $GITHUB_ENV
elif [ -f .tool-versions ]; then
NODE_VERSION=$(grep '^nodejs ' .tool-versions | awk '{print $2}')
echo "NODE_VERSION=$NODE_VERSION" >> $GITHUB_ENV
echo "Node version resolved to: $NODE_VERSION"
else
echo "No .tool-versions file found and no node-version input provided. Invalid configuration."
exit 1
fi
# if pnpm-version input is provided, use it; otherwise, read from .tool-versions
if [ "${{ inputs.pnpm-version }}" ]; then
echo "Pnpm version override provided: ${{ inputs.pnpm-version }}"
echo "PNPM_VERSION=${{ inputs.pnpm-version }}" >> $GITHUB_ENV
elif [ -f .tool-versions ]; then
PNPM_VERSION=$(grep '^pnpm ' .tool-versions | awk '{print $2}')
echo "PNPM_VERSION=$PNPM_VERSION" >> $GITHUB_ENV
echo "Pnpm version resolved to: $PNPM_VERSION"
else
echo "No .tool-versions file found and no pnpm-version input provided. Invalid configuration."
exit 1
fi
- name: Setup Node@${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ inputs.pnpm-version }}
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Get pnpm store path
@@ -55,14 +77,25 @@ runs:
echo "STORE_PATH=$STORE_PATH" >> $GITHUB_ENV
echo "Pnpm store path resolved to: $STORE_PATH"
- name: Compute Cache Key
shell: bash
run: |
if [ -n "${{ inputs.pnpm-install-cache-key }}" ]; then
PNPM_INSTALL_CACHE_KEY="${{ inputs.pnpm-install-cache-key }}"
else
PNPM_INSTALL_CACHE_KEY="pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}"
fi
echo "Computed PNPM_INSTALL_CACHE_KEY: $PNPM_INSTALL_CACHE_KEY"
echo "PNPM_INSTALL_CACHE_KEY=$PNPM_INSTALL_CACHE_KEY" >> $GITHUB_ENV
- name: Restore pnpm install cache
if: ${{ inputs.pnpm-restore-cache == 'true' }}
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ inputs.pnpm-install-cache-key }}
key: ${{ env.PNPM_INSTALL_CACHE_KEY }}
restore-keys: |
pnpm-store-${{ inputs.pnpm-version }}-
pnpm-store-${{ env.PNPM_VERSION }}-
pnpm-store-
- name: Run pnpm install
@@ -72,5 +105,5 @@ runs:
# Set the cache key output
- run: |
echo "pnpm-install-cache-key=${{ inputs.pnpm-install-cache-key }}" >> $GITHUB_ENV
echo "pnpm-install-cache-key=${{ env.PNPM_INSTALL_CACHE_KEY }}" >> $GITHUB_OUTPUT
shell: bash

36
.github/workflows/audit-dependencies.sh vendored Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
severity=${1:-"high"}
output_file="audit_output.json"
echo "Auditing for ${severity} vulnerabilities..."
audit_json=$(pnpm audit --prod --json)
echo "${audit_json}" | jq --arg severity "${severity}" '
.advisories | to_entries |
map(select(.value.patched_versions != "<0.0.0" and (.value.severity == $severity or ($severity == "high" and .value.severity == "critical"))) |
{
package: .value.module_name,
vulnerable: .value.vulnerable_versions,
fixed_in: .value.patched_versions,
findings: .value.findings
}
)
' >$output_file
audit_length=$(jq 'length' $output_file)
if [[ "${audit_length}" -gt "0" ]]; then
echo "Actionable vulnerabilities found in the following packages:"
jq -r '.[] | "\u001b[1m\(.package)\u001b[0m vulnerable in \u001b[31m\(.vulnerable)\u001b[0m fixed in \u001b[32m\(.fixed_in)\u001b[0m"' $output_file | while read -r line; do echo -e "$line"; done
echo ""
echo "Output written to ${output_file}"
cat $output_file
echo ""
echo "This script can be rerun with: './.github/workflows/audit-dependencies.sh $severity'"
exit 1
else
echo "No actionable vulnerabilities"
exit 0
fi

View File

@@ -0,0 +1,53 @@
name: audit-dependencies
on:
# Sundays at 2am EST
schedule:
- cron: '0 7 * * 0'
workflow_dispatch:
inputs:
audit-level:
description: The level of audit to run (low, moderate, high, critical)
required: false
default: high
debug:
description: Enable debug logging
required: false
default: false
env:
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
audit:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Run audit dependencies script
id: audit_dependencies
run: ./.github/workflows/audit-dependencies.sh ${{ inputs.audit-level }}
- name: Slack notification on failure
if: failure()
uses: slackapi/slack-github-action@v2.1.0
with:
webhook: ${{ inputs.debug == 'true' && secrets.SLACK_TEST_WEBHOOK_URL || secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"username": "GitHub Actions Bot",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "🚨 Actionable vulnerabilities found: <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Script Run Details>"
}
},
]
}

View File

@@ -17,8 +17,6 @@ concurrency:
cancel-in-progress: true
env:
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
@@ -71,10 +69,6 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Lint
run: pnpm lint -- --quiet
@@ -89,10 +83,6 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm run build:all
env:
@@ -114,11 +104,8 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
@@ -141,11 +128,8 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
@@ -169,6 +153,7 @@ jobs:
matrix:
database:
- mongodb
- firestore
- postgres
- postgres-custom-schema
- postgres-uuid
@@ -187,7 +172,8 @@ jobs:
services:
postgres:
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'postgis/postgis:16-3.4' || '' }}
# Custom postgres 17 docker image that supports both pg-vector and postgis: https://github.com/payloadcms/postgis-vector
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'ghcr.io/payloadcms/postgis-vector:latest' || '' }}
env:
# must specify password for PG Docker container image, see: https://registry.hub.docker.com/_/postgres?tab=description&page=1&name=10
POSTGRES_USER: ${{ env.POSTGRES_USER }}
@@ -204,11 +190,8 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
@@ -301,6 +284,8 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- group-by
- folders
- hooks
- lexical__collections__Lexical__e2e__main
- lexical__collections__Lexical__e2e__blocks
@@ -319,6 +304,7 @@ jobs:
- plugin-nested-docs
- plugin-seo
- sort
- trash
- versions
- uploads
env:
@@ -329,11 +315,8 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
@@ -438,6 +421,8 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- group-by
- folders
- hooks
- lexical__collections__Lexical__e2e__main
- lexical__collections__Lexical__e2e__blocks
@@ -456,6 +441,7 @@ jobs:
- plugin-nested-docs
- plugin-seo
- sort
- trash
- versions
- uploads
env:
@@ -466,11 +452,8 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
@@ -572,11 +555,8 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
@@ -672,11 +652,8 @@ jobs:
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
@@ -720,3 +697,36 @@ jobs:
- run: |
echo github.ref: ${{ github.ref }}
echo isV3: ${{ github.ref == 'refs/heads/main' }}
analyze:
runs-on: ubuntu-latest
needs: [changes, build]
timeout-minutes: 5
permissions:
contents: read # for checkout repository
actions: read # for fetching base branch bundle stats
pull-requests: write # for comments
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- run: pnpm run build:bundle-for-analysis # Esbuild packages that haven't already been built in the build step for the purpose of analyzing bundle size
env:
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
- name: Analyze esbuild bundle size
# Temporarily disable this for community PRs until this can be implemented in a separate workflow
if: github.event.pull_request.head.repo.fork == false
uses: exoego/esbuild-bundle-analyzer@v1
with:
metafiles: 'packages/payload/meta_index.json,packages/payload/meta_shared.json,packages/ui/meta_client.json,packages/ui/meta_shared.json,packages/next/meta_index.json,packages/richtext-lexical/meta_client.json'

View File

@@ -7,8 +7,6 @@ on:
workflow_dispatch:
env:
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
@@ -60,9 +58,6 @@ jobs:
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Start PostgreSQL
uses: CasperWA/postgresql-action@v1.2
@@ -87,6 +82,11 @@ jobs:
with:
mongodb-version: 6.0
# The template generation script runs import map generation which needs the built payload bin scripts
- run: pnpm run build:all
env:
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
- name: Update template lockfiles and migrations
run: pnpm script:gen-templates

View File

@@ -12,8 +12,6 @@ on:
default: ''
env:
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

View File

@@ -7,8 +7,6 @@ on:
workflow_dispatch:
env:
NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
@@ -23,9 +21,6 @@ jobs:
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Load npm token
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:

7
.gitignore vendored
View File

@@ -22,6 +22,13 @@ meta_server.json
meta_index.json
meta_shared.json
packages/payload/esbuild
packages/ui/esbuild
packages/next/esbuild
packages/richtext-lexical/esbuild
audit_output.json
.turbo
# Ignore test directory media folder/files

7
.vscode/launch.json vendored
View File

@@ -139,6 +139,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts trash",
"cwd": "${workspaceFolder}",
"name": "Run Dev Trash",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts uploads",
"cwd": "${workspaceFolder}",

View File

@@ -77,7 +77,7 @@ All auto-generated files will contain the following comments at the top of each
## Admin Options
All options for the Admin Panel are defined in your [Payload Config](../configuration/overview) under the `admin` property:
All root-level options for the Admin Panel are defined in your [Payload Config](../configuration/overview) under the `admin` property:
```ts
import { buildConfig } from 'payload'

View File

@@ -114,7 +114,12 @@ const MyComponent: React.FC = () => {
## useAllFormFields
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Your component will re-render when _any_ field changes, so use this hook only if you absolutely need to. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
<Banner type="warning">
**Warning:** Your component will re-render when _any_ field changes, so use
this hook only if you absolutely need to.
</Banner>
You can do lots of powerful stuff by retrieving the full form state, like using built-in helper functions to reduce field state to values only, or to retrieve sibling data by path.

View File

@@ -1,62 +0,0 @@
---
title: Project Configuration
label: Configuration
order: 20
desc: Quickly configure and deploy your Payload Cloud project in a few simple steps.
keywords: configuration, config, settings, project, cloud, payload cloud, deploy, deployment
---
## Select your plan
Once you have created a project, you will need to select your plan. This will determine the resources that are allocated to your project and the features that are available to you.
<Banner type="success">
Note: All Payload Cloud teams that deploy a project require a card on file.
This helps us prevent fraud and abuse on our platform. If you select a plan
with a free trial, you will not be charged until your trial period is over.
Well remind you 7 days before your trial ends and you can cancel anytime.
</Banner>
## Project Details
| Option | Description |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Region** | Select the region closest to your audience. This will ensure the fastest communication between your data and your client. |
| **Project Name** | A name for your project. You can change this at any time. |
| **Project Slug** | Choose a unique slug to identify your project. This needs to be unique for your team and you can change it any time. |
| **Team** | Select the team you want to create the project under. If this is your first project, a personal team will be created for you automatically. You can modify your team settings and invite new members at any time from the Team Settings page. |
## Build Settings
If you are deploying a new project from a template, the following settings will be automatically configured for you. If you are using your own repository, you need to make sure your build settings are accurate for your project to deploy correctly.
| Option | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Root Directory** | The folder where your `package.json` file lives. |
| **Install Command** | The command used to install your modules, for example: `yarn install` or `npm install` |
| **Build Command** | The command used to build your application, for example: `yarn build` or `npm run build` |
| **Serve Command** | The command used to serve your application, for example: `yarn serve` or `npm run serve` |
| **Branch to Deploy** | Select the branch of your repository that you want to deploy from. This is the branch that will be used to build your project when you commit new changes. |
| **Default Domain** | Set a default domain for your project. This must be unique and you will not able to change it. You can always add a custom domain later in your project settings. |
## Environment Variables
Any of the features in Payload Cloud that require environment variables will automatically be provided to your application. If your app requires any custom environment variables, you can set them here.
<Banner type="warning">
Note: For security reasons, any variables you wish to provide to the [Admin
Panel](../admin/overview) must be prefixed with `NEXT_PUBLIC_`.  Learn more
[here](../configuration/environment-vars).
</Banner>
## Payment
Payment methods can be set per project and can be updated any time. You can use teams default payment method, or add a new one. Modify your payment methods in your Project settings / Team settings.
<Banner type="success">
**Note:** All Payload Cloud teams that deploy a project require a card on
file. This helps us prevent fraud and abuse on our platform. If you select a
plan with a free trial, you will not be charged until your trial period is
over. Well remind you 7 days before your trial ends and you can cancel
anytime.
</Banner>

View File

@@ -1,53 +0,0 @@
---
title: Getting Started
label: Getting Started
order: 10
desc: Get started with Payload Cloud, a deployment solution specifically designed for Node + MongoDB applications.
keywords: cloud, hosted, database, storage, email, deployment, serverless, node, mongodb, s3, aws, cloudflare, atlas, resend, payload, cms
---
A deployment solution specifically designed for Node.js + MongoDB applications, offering seamless deployment of your entire stack in one place. You can get started in minutes with a one-click template or bring your own codebase with you.
Payload Cloud offers various plans tailored to meet your specific needs, including a MongoDB Atlas database, S3 file storage, and email delivery powered by [Resend](https://resend.com). To see a full breakdown of features and plans, see our [Cloud Pricing page](https://payloadcms.com/cloud-pricing).
To get started, you first need to create an account. Head over to [the login screen](https://payloadcms.com/login) and **Register for Free**.
<Banner type="success">
To create your first project, you can either select [a
template](#starting-from-a-template) or [import an existing
project](#importing-from-an-existing-codebase) from GitHub.
</Banner>
## Starting from a Template
Templates come preconfigured and provide a one-click solution to quickly deploy a new application.
![Screen for creating a new project from a template](https://payloadcms.com/images/docs/cloud/create-from-template.jpg)
_Creating a new project from a template._
After creating an account, select your desired template from the Projects page. At this point, you need to connect to authorize the Payload Cloud application with your GitHub account. Click Continue with GitHub and follow the prompts to authorize the app.
Next, select your `GitHub Scope`. If you belong to multiple organizations, they will show up here. If you do not see the organization you are looking for, you may need to adjust your GitHub app permissions.
After selecting your scope, create a unique `repository name` and select whether you want your repository to be public or private on GitHub.
<Banner type="warning">
**Note:** Public repositories can be accessed by anyone online, while private
repositories grant access only to you and anyone you explicitly authorize.
</Banner>
Once you are ready, click **Create Project**. This will clone the selected template to a new repository in your GitHub account, and take you to the configuration page to set up your project for deployment.
## Importing from an Existing Codebase
Payload Cloud works for any Node.js + MongoDB app. From the New Project page, select **import an existing Git codebase**. Choose the organization and select the repository you want to import. From here, you will be taken to the configuration page to set up your project for deployment.
![Screen for creating a new project from an existing repository](https://payloadcms.com/images/docs/cloud/create-from-existing.jpg)
_Creating a new project from an existing repository._
<Banner type="warning">
**Note:** In order to make use of the features of Payload Cloud in your own
codebase, you will need to add the [Cloud
Plugin](https://github.com/payloadcms/payload/tree/main/packages/payload-cloud)
to your Payload app.
</Banner>

View File

@@ -1,137 +0,0 @@
---
title: Cloud Projects
label: Projects
order: 40
desc: Manage your Payload Cloud projects.
keywords: cloud, payload cloud, projects, project, overview, database, file storage, build settings, environment variables, custom domains, email, developing locally
---
## Overview
<Banner>
The overview tab shows your most recent deployment, along with build and
deployment logs. From here, you can see your live URL, deployment details like
timestamps and commit hash, as well as the status of your deployment. You can
also trigger a redeployment manually, which will rebuild your project using
the current configuration.
</Banner>
![Payload Cloud Overview Page](https://payloadcms.com/images/docs/cloud/overview-page.jpg)
_A screenshot of the Overview page for a Cloud project._
## Database
Your Payload Cloud project comes with a MongoDB serverless Atlas DB instance or a Dedicated Atlas cluster, depending on your plan. To interact with your cloud database, you will be provided with a MongoDB connection string. This can be found under the **Database** tab of your project.
`mongodb+srv://your_connection_string`
## File Storage
Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this plugin extends Payload so that all of your media will be stored in S3 rather than locally.
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/payload/tree/main/packages/payload-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
### Accessing Files Outside of Payload Cloud
If you'd like to access your files outside of Payload Cloud, you'll need to retrieve some values from your project's settings and put them into your environment variables. In Payload Cloud, navigate to the File Storage tab and copy the values using the copy button. Put these values in your .env file. Also copy the Cognito Password value separately and put into your .env file as well.
When you are done, you should have the following values in your .env file:
```env
PAYLOAD_CLOUD=true
PAYLOAD_CLOUD_ENVIRONMENT=prod
PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID=
PAYLOAD_CLOUD_COGNITO_USER_POOL_ID=
PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID=
PAYLOAD_CLOUD_PROJECT_ID=
PAYLOAD_CLOUD_BUCKET=
PAYLOAD_CLOUD_BUCKET_REGION=
PAYLOAD_CLOUD_COGNITO_PASSWORD=
```
The plugin will pick up these values and use them to access your files.
## Build Settings
You can update settings from your Projects Settings tab. Changes to your build settings will trigger a redeployment of your project.
## Environment Variables
From the Environment Variables page of the Settings tab, you can add, update and delete variables for use in your project. Like build settings, these changes will trigger a redeployment of your project.
<Banner>
Note: For security reasons, any variables you wish to provide to the [Admin
Panel](../admin/overview) must be prefixed with `NEXT_PUBLIC_`. [More
details](../configuration/environment-vars).
</Banner>
## Custom Domains
With Payload Cloud, you can add custom domain names to your project. To do so, first go to the Domains page of the Settings tab of your project. Here you can see your default domain. To add a new domain, type in the domain name you wish to use.
<Banner>
Note: do not include the protocol (http:// or https://) or any paths (/page).
Only include the domain name and extension, and optionally a subdomain. -
your-domain.com - backend.your-domain.com
</Banner>
Once you click save, a DNS record will be generated for your domain name to point to your live project. Add this record into your DNS providers records, and once the records are resolving properly (this can take 1hr to 48hrs in some cases), your domain will now to point to your live project.
You will also need to configure your Payload project to use your specified domain. In your `payload.config.ts` file, specify your `serverURL` with your domain:
```ts
export default buildConfig({
serverURL: 'https://example.com',
// the rest of your config,
})
```
## Email
Powered by [Resend](https://resend.com), Payload Cloud comes with integrated email support out of the box. No configuration is needed, and you can use `payload.sendEmail()` to send email right from your Payload app. To learn more about sending email with Payload, checkout the [Email Configuration](../email/overview) overview.
If you are on the Pro or Enterprise plan, you can add your own custom Email domain name. From the Email page of your projects Settings, add the domain you wish to use for email delivery. This will generate a set of DNS records. Add these records to your DNS provider and click verify to check that your records are resolving properly. Once verified, your emails will now be sent from your custom domain name.
## Developing Locally
To make changes to your project, you will need to clone the repository defined in your project settings to your local machine. In order to run your project locally, you will need configure your local environment first. Refer to your repositorys `README.md` file to see the steps needed for your specific template.
From there, you are ready to make updates to your project. When you are ready to make your changes live, commit your changes to the branch you specified in your Project settings, and your application will automatically trigger a redeploy and build from your latest commit.
## Cloud Plugin
Projects generated from a template will come pre-configured with the official Cloud Plugin, but if you are using your own repository you will need to add this into your project. To do so, add the plugin to your Payload Config:
`pnpm add @payloadcms/payload-cloud`
```js
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
import { buildConfig } from 'payload'
export default buildConfig({
plugins: [payloadCloudPlugin()],
// rest of config
})
```
<Banner type="warning">
**Note:** If your Payload Config already has an email with transport, this
will take precedence over Payload Cloud's email service.
</Banner>
<Banner type="info">
Good to know: the Payload Cloud Plugin was previously named
`@payloadcms/plugin-cloud`. If you are using this plugin, you should update to
the new package name.
</Banner>
#### **Optional configuration**
If you wish to opt-out of any Payload cloud features, the plugin also accepts options to do so.
```js
payloadCloud({
storage: false, // Disable file storage
email: false, // Disable email delivery
})
```

View File

@@ -1,35 +0,0 @@
---
title: Cloud Teams
label: Teams
order: 30
desc: Manage your Payload Cloud team and billing settings.
keywords: team, teams, billing, subscription, payment, plan, plans, cloud, payload cloud
---
<Banner>
Within Payload Cloud, the team management feature offers you the ability to
manage your organization, team members, billing, and subscription settings.
</Banner>
![Payload Cloud Team Settings](https://payloadcms.com/images/docs/cloud/team-settings.jpg)
_A screenshot of the Team Settings page._
## Members
Each team has members that can interact with your projects. You can invite multiple people to your team and each individual can belong to more than one team. You can assign them either `owner` or `user` permissions. Owners are able to make admin-only changes, such as deleting projects, and editing billing information.
## Adding Members
To add a new member to your team, visit your Teams Settings page, and click “Invite Teammate”. You can then add their email address, and assign their role. Press “Save” to send the invitations, which will send an email to the invited team member where they can create a new account.
## Billing
Users can update billing settings and subscriptions for any teams where they are designated as an `owner`. To make updates to the teams payment methods, visit the Billing page under the Team Settings tab. You can add new cards, delete cards, and set a payment method as a default. The default payment method will be used in the event that another payment method fails.
## Subscriptions
From the Subscriptions page, a team owner can see all current plans for their team. From here, you can see the price of each plan, if there is an active trial, and when you will be billed next.
## Invoices
The Invoices page will you show you the invoices for your account, as well as the status on their payment.

View File

@@ -79,6 +79,7 @@ The following options are available:
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `trash` | A boolean to enable soft deletes for this collection. Defaults to `false`. [More details](../trash/overview). |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
@@ -121,26 +122,34 @@ export const MyCollection: CollectionConfig = {
The following options are available:
| Option | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
| `hooks` | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title, unless it's linked to a relationship'. |
| `description` | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this Collection's List View. |
| `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
| Option | Description |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
| `hooks` | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. |
| `description` | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this Collection's List View. |
| `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. |
| `groupBy` | Beta. Enable grouping by a field in the list view. |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
<Banner type="warning">
**Note:** If you set `useAsTitle` to a relationship or join field, it will use
only the ID of the related document(s) as the title. To display a specific
field (i.e. title) from the related document instead, create a virtual field
that extracts the desired data, and set `useAsTitle` to that virtual field.
</Banner>
### Custom Components

View File

@@ -1,7 +1,7 @@
---
title: Environment Variables
label: Environment Variables
order: 100
order: 60
desc: Learn how to use Environment Variables in your Payload project
---
@@ -72,7 +72,7 @@ const MyClientComponent = () => {
}
```
For more information, check out the [Next.js Documentation](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables).
For more information, check out the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables).
## Outside of Next.js

View File

@@ -110,7 +110,7 @@ _\* An asterisk denotes that a property is required._
details](../custom-components/overview#accessing-the-payload-config).
</Banner>
### Typescript Config
### TypeScript Config
Payload exposes a variety of TypeScript settings that you can leverage. These settings are used to auto-generate TypeScript interfaces for your [Collections](./collections) and [Globals](./globals), and to ensure that Payload uses your [Generated Types](../typescript/overview) for all [Local API](../local-api/overview) methods.
@@ -121,10 +121,11 @@ import { buildConfig } from 'payload'
export default buildConfig({
// ...
// highlight-start
typescript: {
// highlight-line
// ...
},
// highlight-end
})
```
@@ -227,7 +228,9 @@ import { buildConfig } from 'payload'
export default buildConfig({
// ...
cors: '*', // highlight-line
// highlight-start
cors: '*',
// highlight-end
})
```

View File

@@ -505,3 +505,51 @@ Payload also exports its [SCSS](https://sass-lang.com) library for reuse which i
**Note:** You can also drill into Payload's own component styles, or easily
apply global, app-wide CSS. More on that [here](../admin/customizing-css).
</Banner>
## Performance
An often overlooked aspect of Custom Components is performance. If unchecked, Custom Components can lead to slow load times of the Admin Panel and ultimately a poor user experience.
This is different from front-end performance of your public-facing site.
<Banner type="success">
For more performance tips, see the [Performance
documentation](../performance/overview).
</Banner>
### Follow React and Next.js best practices
All Custom Components are built using [React](https://react.dev). For this reason, it is important to follow React best practices. This includes using memoization, streaming, caching, optimizing renders, using hooks appropriately, and more.
To learn more, see the [React documentation](https://react.dev/learn).
The Admin Panel itself is a [Next.js](https://nextjs.org) application. For this reason, it is _also_ important to follow Next.js best practices. This includes bundling, when to use layouts vs pages, where to place the server/client boundary, and more.
To learn more, see the [Next.js documentation](https://nextjs.org/docs).
### Reducing initial HTML size
With Server Components, be aware of what is being sent to through the server/client boundary. All props are serialized and sent through the network. This can lead to large HTML sizes and slow initial load times if too much data is being sent to the client.
To minimize this, you must be explicit about what props are sent to the client. Prefer server components and only send the necessary props to the client. This will also offset some of the JS execution to the server.
<Banner type="success">
**Tip:** Use [React Suspense](https://react.dev/reference/react/Suspense) to
progressively load components and improve perceived performance.
</Banner>
### Prevent unnecessary re-renders
If subscribing your component to form state, it may be re-rendering more often than necessary.
To do this, use the [`useFormFields`](../admin/react-hooks) hook instead of `useFields` when you only need to access specific fields.
```ts
'use client'
import { useFormFields } from '@payloadcms/ui'
const MyComponent: TextFieldClientComponent = ({ path }) => {
const value = useFormFields(([fields, dispatch]) => fields[path])
// ...
}
```

65
docs/database/indexes.mdx Normal file
View File

@@ -0,0 +1,65 @@
---
title: Indexes
label: Indexes
order: 40
keywords: database, indexes
desc: Index fields to produce faster queries.
---
Database indexes are a way to optimize the performance of your database by allowing it to quickly locate and retrieve data. If you have a field that you frequently query or sort by, adding an index to that field can significantly improve the speed of those operations.
When your query runs, the database will not scan the entire document to find that one field, but will instead use the index to quickly locate the data.
To index a field, set the `index` option to `true` in your field's config:
```ts
import type { CollectionConfig } from 'payload'
export MyCollection: CollectionConfig = {
// ...
fields: [
// ...
{
name: 'title',
type: 'text',
// highlight-start
index: true,
// highlight-end
},
]
}
```
<Banner type="info">
**Note:** The `id`, `createdAt`, and `updatedAt` fields are indexed by
default.
</Banner>
<Banner type="success">
**Tip:** If you're using MongoDB, you can use [MongoDB
Compass](https://www.mongodb.com/products/compass) to visualize and manage
your indexes.
</Banner>
## Compound Indexes
In addition to indexing single fields, you can also create compound indexes that index multiple fields together. This can be useful for optimizing queries that filter or sort by multiple fields.
To create a compound index, use the `indexes` option in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
fields: [
// ...
],
indexes: [
{
fields: ['title', 'createdAt'],
unique: true, // Optional, if you want the combination of fields to be unique
},
],
}
```

View File

@@ -1,7 +1,7 @@
---
title: MongoDB
label: MongoDB
order: 40
order: 50
desc: Payload has supported MongoDB natively since we started. The flexible nature of MongoDB lends itself well to Payload's powerful fields.
keywords: MongoDB, documentation, typescript, Content Management System, cms, headless, javascript, node, react, nextjs
---
@@ -30,18 +30,22 @@ export default buildConfig({
## Options
| Option | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| `disableFallbackSort` | Set to `true` to disable the adapter adding a fallback sort when sorting by non-unique fields, this can affect performance in some cases but it ensures a consistent order of results. |
| Option | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| `disableFallbackSort` | Set to `true` to disable the adapter adding a fallback sort when sorting by non-unique fields, this can affect performance in some cases but it ensures a consistent order of results. |
| `useAlternativeDropDatabase` | Set to `true` to use an alternative `dropDatabase` implementation that calls `collection.deleteMany({})` on every collection instead of sending a raw `dropDatabase` command. Payload only uses `dropDatabase` for testing purposes. Defaults to `false`. |
| `useBigIntForNumberIDs` | Set to `true` to use `BigInt` for custom ID fields of type `'number'`. Useful for databases that don't support `double` or `int32` IDs. Defaults to `false`. |
| `useJoinAggregations` | Set to `false` to disable join aggregations (which use correlated subqueries) and instead populate join fields via multiple `find` queries. Defaults to `true`. |
| `usePipelineInSortLookup` | Set to `false` to disable the use of `pipeline` in the `$lookup` aggregation in sorting. Defaults to `true`. |
## Access to Mongoose models
@@ -56,9 +60,21 @@ You can access Mongoose models as follows:
## Using other MongoDB implementations
Limitations with [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db):
You can import the `compatabilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
- For Azure Cosmos DB you must pass `transactionOptions: false` to the adapter options. Azure Cosmos DB does not support transactions that update two and more documents in different collections, which is a common case when using Payload (via hooks).
- For Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
- The [Join Field](../fields/join) is not supported in DocumentDB and Azure Cosmos DB, as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future.
- For DocumentDB pass `disableIndexHints: true` to disable hinting to the DB to use `id` as index which can cause problems with DocumentDB.
```ts
import { mongooseAdapter, compatabilityOptions } from '@payloadcms/db-mongodb'
export default buildConfig({
db: mongooseAdapter({
url: process.env.DATABASE_URI,
// For example, if you're using firestore:
...compatabilityOptions.firestore,
}),
})
```
We export compatability options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
- Azure Cosmos DB does not support transactions that update two or more documents in different collections, which is a common case when using Payload (via hooks).
- Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.

View File

@@ -1,7 +1,7 @@
---
title: Postgres
label: Postgres
order: 50
order: 60
desc: Payload supports Postgres through an officially supported Drizzle Database Adapter.
keywords: Postgres, documentation, typescript, Content Management System, cms, headless, javascript, node, react, nextjs
---

View File

@@ -1,7 +1,7 @@
---
title: SQLite
label: SQLite
order: 60
order: 70
desc: Payload supports SQLite through an officially supported Drizzle Database Adapter.
keywords: SQLite, documentation, typescript, Content Management System, cms, headless, javascript, node, react, nextjs
---

View File

@@ -41,17 +41,17 @@ export const MyArrayField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) or an object with keys for each language. Auto-generated from name if not defined. |
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More details](/docs/fields/overview#validation). |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. |
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |

View File

@@ -41,17 +41,17 @@ export const MyBlocksField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as the heading in the Admin Panel or an object with keys for each language. Auto-generated from name if not defined. |
| **`blocks`** \* | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin Panel. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |

View File

@@ -30,15 +30,15 @@ export const MyCheckboxField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](./overview#admin-options). |

View File

@@ -31,18 +31,18 @@ export const MyBlocksField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |

View File

@@ -30,15 +30,15 @@ export const MyDateField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -30,16 +30,16 @@ export const MyEmailField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -35,15 +35,15 @@ export const MyGroupField: Field = {
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Defaults to the field name, if defined. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |

View File

@@ -135,7 +135,7 @@ powerful Admin UI.
| Option | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
| **`name`** \* | To be used as the property name when retrieved from the database. [More details](./overview#field-names). |
| **`collection`** \* | The `slug`s having the relationship field or an array of collection slugs. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections |
| **`orderable`** | If true, enables custom ordering and joined documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |

View File

@@ -31,17 +31,17 @@ export const MyJSONField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -30,7 +30,7 @@ export const MyNumberField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
@@ -38,13 +38,13 @@ export const MyNumberField: Field = {
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -157,6 +157,7 @@ The following field names are forbidden and cannot be used:
- `salt`
- `hash`
- `file`
- `status` - with Postgres Adapter and when drafts are enabled
### Field-level Hooks
@@ -303,7 +304,7 @@ The following additional properties are provided in the `ctx` object:
| `path` | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. |
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#validation-performance). |
#### Localized and Built-in Error Messages
@@ -365,11 +366,11 @@ import {
} from 'payload/shared'
```
#### Async Field Validations
#### Validation Performance
Custom validation functions can also be asynchronous depending on your needs. This makes it possible to make requests to external services or perform other miscellaneous asynchronous logic.
When writing async or computationally heavy validation functions, it is important to consider the performance implications. Within the Admin Panel, validations are executed on every change to the field, so they should be as lightweight as possible and only run when necessary.
When writing async validation functions, it is important to consider the performance implications. Validations are executed on every change to the field, so they should be as lightweight as possible. If you need to perform expensive validations, such as querying the database, consider using the `event` property in the `ctx` object to only run the validation on form submission.
If you need to perform expensive validations, such as querying the database, consider using the `event` property in the `ctx` object to only run that particular validation on form submission.
To write asynchronous validation functions, use the `async` keyword to define your function:
@@ -403,6 +404,11 @@ export const Orders: CollectionConfig = {
}
```
<Banner type="success">
For more performance tips, see the [Performance
documentation](../performance/overview).
</Banner>
## Custom ID Fields
All [Collections](../configuration/collections) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This field should either be required or have a hook to generate the ID dynamically.

View File

@@ -34,16 +34,16 @@ export const MyPointField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Used as a field label in the Admin Panel and to name the generated GraphQL type. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](./overview#admin-options). |

View File

@@ -35,16 +35,16 @@ export const MyRadioField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -39,22 +39,22 @@ export const MyRelationshipField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-relationship-options). |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. |
| **`maxDepth`** | Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/queries/depth#max-depth) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
@@ -93,7 +93,7 @@ The Relationship Field inherits all of the default admin options from the base [
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany` is set to `true`). |
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More details](#sort-options) |
| **`placeholder`** | Define a custom text or function to replace the generic default placeholder |
| **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. |

View File

@@ -23,14 +23,14 @@ Instead, you can invest your time and effort into learning the underlying open-s
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](./overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](./overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](./overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](./overview#validation). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](./overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](./overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -35,18 +35,18 @@ export const MySelectField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-options) for more details. |

View File

@@ -45,7 +45,7 @@ Each tab must have either a `name` or `label` and the required `fields` array. Y
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** | Groups field data into an object when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** | Groups field data into an object when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | The label to render on the tab itself. Required when name is undefined, defaults to name converted to words. |
| **`fields`** \* | The fields to render within this tab. |
| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. |

View File

@@ -30,18 +30,18 @@ export const MyTextField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -30,18 +30,18 @@ export const MyTextareaField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |

View File

@@ -32,8 +32,8 @@ export const MyUIField: Field = {
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **`name`** \* | A unique identifier for this field. |
| **`label`** | Human-readable label for this UI field. |
| **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More](./overview#field) |
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](./overview#cell) |
| **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More details](./overview#field). |
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More details](./overview#cell). |
| **`admin.disableListColumn`** | Set `disableListColumn` to `true` to prevent the UI field from appearing in the list view column selector. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |

View File

@@ -46,23 +46,23 @@ export const MyUploadField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. **Note: the related collection must be configured to support Uploads.** |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-upload-options). |
| **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with hasMany. |
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/fields/overview#validation). |
| **`index`** | Build an [index](../database/indexes) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
| **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/fields/overview#default-values). |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More details](/docs/upload/overview#collection-upload-options). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [Admin Options](./overview#admin-options). |

View File

@@ -16,14 +16,15 @@ The labels you provide for your Collections and Globals are used to name the Gra
At the top of your Payload Config you can define all the options to manage GraphQL.
| Option | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
| Option | Description |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground in production environments, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disableIntrospectionInProduction` | A boolean that if false will enable the GraphQL introspection in production environments, defaults to true. |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
## Collections

View File

@@ -93,12 +93,108 @@ All Hooks can be written as either synchronous or asynchronous functions. Choosi
#### Asynchronous
If the Hook should modify data before a Document is updated or created, and it relies on asynchronous actions such as fetching data from a third party, it might make sense to define your Hook as an asynchronous function. This way you can be sure that your Hook completes before the operation's lifecycle continues. Async hooks are run in series - so if you have two async hooks defined, the second hook will wait for the first to complete before it starts.
If the Hook should modify data before a Document is updated or created, and it relies on asynchronous actions such as fetching data from a third party, it might make sense to define your Hook as an asynchronous function. This way you can be sure that your Hook completes before the operation's lifecycle continues.
Async hooks are run in series - so if you have two async hooks defined, the second hook will wait for the first to complete before it starts.
<Banner type="success">
**Tip:** If your hook executes a long-running task that doesn't affect the
response in any way, consider [offloading it to the job
queue](#offloading-long-running-tasks). That will free up the request to
continue processing without waiting for the task to complete.
</Banner>
#### Synchronous
If your Hook simply performs a side-effect, such as updating a CRM, it might be okay to define it synchronously, so the Payload operation does not have to wait for your hook to complete.
If your Hook simply performs a side-effect, such as mutating document data, it might be okay to define it synchronously, so the Payload operation does not have to wait for your hook to complete.
## Server-only Execution
Hooks are only triggered on the server and are automatically excluded from the client-side bundle. This means that you can safely use sensitive business logic in your Hooks without worrying about exposing it to the client.
## Performance
Hooks are a powerful way to customize the behavior of your APIs, but some hooks are run very often and can add significant overhead to your requests if not optimized.
When building hooks, combine together as many of these strategies as possible to ensure your hooks are as performant as they can be.
<Banner type="success">
For more performance tips, see the [Performance
documentation](../performance/overview).
</Banner>
### Writing efficient hooks
Consider when hooks are run. One common pitfall is putting expensive logic in hooks that run very often.
For example, the `read` operation runs on every read request, so avoid putting expensive logic in a `beforeRead` or `afterRead` hook.
```ts
{
hooks: {
beforeRead: [
async () => {
// This runs on every read request - avoid expensive logic here
await doSomethingExpensive()
return data
},
],
},
}
```
Instead, you might want to use a `beforeChange` or `afterChange` hook, which only runs when a document is created or updated.
```ts
{
hooks: {
beforeChange: [
async ({ context }) => {
// This is more acceptable here, although still should be mindful of performance
await doSomethingExpensive()
// ...
},
]
},
}
```
### Using Hook Context
Use [Hook Context](./context) avoid prevent infinite loops or avoid repeating expensive operations across multiple hooks in the same request.
```ts
{
hooks: {
beforeChange: [
async ({ context }) => {
const somethingExpensive = await doSomethingExpensive()
context.somethingExpensive = somethingExpensive
// ...
},
],
},
}
```
To learn more, see the [Hook Context documentation](./context).
### Offloading to the jobs queue
If your hooks perform any long-running tasks that don't direct affect request lifecycle, consider offloading them to the [jobs queue](../jobs-queue/overview). This will free up the request to continue processing without waiting for the task to complete.
```ts
{
hooks: {
afterChange: [
async ({ doc, req }) => {
// Offload to job queue
await req.payload.jobs.queue(...)
// ...
},
],
},
}
```
To learn more, see the [Job Queue documentation](../jobs-queue/overview).

View File

@@ -34,20 +34,20 @@ npm i @payloadcms/plugin-csm
Then in the `plugins` array of your Payload Config, call the plugin and enable any collections that require Content Source Maps.
```ts
import { buildConfig } from "payload/config"
import contentSourceMaps from "@payloadcms/plugin-csm"
import { buildConfig } from 'payload/config'
import contentSourceMaps from '@payloadcms/plugin-csm'
const config = buildConfig({
collections: [
{
slug: "pages",
slug: 'pages',
fields: [
{
name: 'slug',
type: 'text',
},
{
name: 'title,'
name: 'title',
type: 'text',
},
],
@@ -55,7 +55,7 @@ const config = buildConfig({
],
plugins: [
contentSourceMaps({
collections: ["pages"],
collections: ['pages'],
}),
],
})

View File

@@ -51,7 +51,7 @@ export default buildConfig({
// add as many cron jobs as you want
],
shouldAutoRun: async (payload) => {
// Tell Payload if it should run jobs or not.
// Tell Payload if it should run jobs or not. This function is optional and will return true by default.
// This function will be invoked each time Payload goes to pick up and run jobs.
// If this function ever returns false, the cron schedule will be stopped.
return true

View File

@@ -0,0 +1,155 @@
---
title: Job Schedules
label: Schedules
order: 60
desc: Payload allows you to schedule jobs to run periodically
keywords: jobs queue, application framework, typescript, node, react, nextjs, scheduling, cron, schedule
---
Payload's `schedule` property lets you enqueue Jobs regularly according to a cron schedule - daily, weekly, hourly, or any custom interval. This is ideal for tasks or workflows that must repeat automatically and without manual intervention.
Scheduling Jobs differs significantly from running them:
- **Queueing**: Scheduling only creates (enqueues) the Job according to your cron expression. It does not immediately execute any business logic.
- **Running**: Execution happens separately through your Jobs runner - such as autorun, or manual invocation using `payload.jobs.run()` or the `payload-jobs/run` endpoint.
Use the `schedule` property specifically when you have recurring tasks or workflows. To enqueue a single Job to run once in the future, use the `waitUntil` property instead.
## Example use cases
**Regular emails or notifications**
Send nightly digests, weekly newsletters, or hourly updates.
**Batch processing during off-hours**
Process analytics data or rebuild static sites during low-traffic times.
**Periodic data synchronization**
Regularly push or pull updates to or from external APIs.
## Handling schedules
Something needs to actually trigger the scheduling of jobs (execute the scheduling lifecycle seen below). By default, the `jobs.autorun` configuration, as well as the `/api/payload-jobs/run` will also handle scheduling for the queue specified in the `autorun` configuration.
You can disable this behavior by setting `disableScheduling: true` in your `autorun` configuration, or by passing `disableScheduling=true` to the `/api/payload-jobs/run` endpoint. This is useful if you want to handle scheduling manually, for example, by using a cron job or a serverless function that calls the `/api/payload-jobs/handle-schedules` endpoint or the `payload.jobs.handleSchedules()` local API method.
## Defining schedules on Tasks or Workflows
Schedules are defined using the `schedule` property:
```ts
export type ScheduleConfig = {
cron: string // required, supports seconds precision
queue: string // required, the queue to push Jobs onto
hooks?: {
// Optional hooks to customize scheduling behavior
beforeSchedule?: BeforeScheduleFn
afterSchedule?: AfterScheduleFn
}
}
```
### Example schedule
The following example demonstrates scheduling a Job to enqueue every day at midnight:
```ts
import type { TaskConfig } from 'payload'
export const SendDigestEmail: TaskConfig<'SendDigestEmail'> = {
slug: 'SendDigestEmail',
schedule: [
{
cron: '0 0 * * *', // Every day at midnight
queue: 'nightly',
},
],
handler: async () => {
await sendDigestToAllUsers()
},
}
```
This configuration only queues the Job - it does not execute it immediately. To actually run the queued Job, you configure autorun in your Payload config (note that autorun should **not** be used on serverless platforms):
```ts
export default buildConfig({
jobs: {
autoRun: [
{
cron: '* * * * *', // Runs every minute
queue: 'nightly',
},
],
tasks: [SendDigestEmail],
},
})
```
That way, Payload's scheduler will automatically enqueue the job into the `nightly` queue every day at midnight. The autorun configuration will check the `nightly` queue every minute and execute any Jobs that are due to run.
## Scheduling lifecycle
Here's how the scheduling process operates in detail:
1. **Cron evaluation**: Payload (or your external trigger in `manual` mode) identifies which schedules are due to run. To do that, it will
read the `payload-jobs-stats` global which contains information about the last time each scheduled task or workflow was run.
2. **BeforeSchedule hook**:
- The default beforeSchedule hook checks how many active or runnable jobs of the same type that have been queued by the scheduling system currently exist.
If such a job exists, it will skip scheduling a new one.
- You can provide your own `beforeSchedule` hook to customize this behavior. For example, you might want to allow multiple overlapping Jobs or dynamically set the Job input data.
3. **Enqueue Job**: Payload queues up a new job. This job will have `waitUntil` set to the next scheduled time based on the cron expression.
4. **AfterSchedule hook**:
- The default afterSchedule hook updates the `payload-jobs-stats` global metadata with the last scheduled time for the Job.
- You can provide your own afterSchedule hook to it for custom logging, metrics, or other post-scheduling actions.
## Customizing concurrency and input (Advanced)
You may want more control over concurrency or dynamically set Job inputs at scheduling time. For instance, allowing multiple overlapping Jobs to be scheduled, even if a previously scheduled job has not completed yet, or preparing dynamic data to pass to your Job handler:
```ts
import { countRunnableOrActiveJobsForQueue } from 'payload'
schedule: [
{
cron: '* * * * *', // every minute
queue: 'reports',
hooks: {
beforeSchedule: async ({ queueable, req }) => {
const runnableOrActiveJobsForQueue =
await countRunnableOrActiveJobsForQueue({
queue: queueable.scheduleConfig.queue,
req,
taskSlug: queueable.taskConfig?.slug,
workflowSlug: queueable.workflowConfig?.slug,
onlyScheduled: true,
})
// Allow up to 3 simultaneous scheduled jobs and set dynamic input
return {
shouldSchedule: runnableOrActiveJobsForQueue < 3,
input: { text: 'Hi there' },
}
},
},
},
]
```
This allows fine-grained control over how many Jobs can run simultaneously and provides dynamically computed input values each time a Job is scheduled.
## Scheduling in serverless environments
On serverless platforms, scheduling must be triggered externally since Payload does not automatically run cron schedules in ephemeral environments. You have two main ways to trigger scheduling manually:
- **Invoke via Payload's API:** `payload.jobs.handleSchedules()`
- **Use the REST API endpoint:** `/api/payload-jobs/handle-schedules`
- **Use the run endpoint, which also handles scheduling by default:** `GET /api/payload-jobs/run`
For example, on Vercel, you can set up a Vercel Cron to regularly trigger scheduling:
- **Vercel Cron Job:** Configure Vercel Cron to periodically call `GET /api/payload-jobs/handle-schedules`. If you would like to auto-run your scheduled jobs as well, you can use the `GET /api/payload-jobs/run` endpoint.
Once Jobs are queued, their execution depends entirely on your configured runner setup (e.g., autorun, or manual invocation).

View File

@@ -45,13 +45,11 @@ The following options are available:
| Path | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`url`** \* | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`url`** | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). |
| **`collections`** | Array of collection slugs to enable Live Preview on. |
| **`globals`** | Array of global slugs to enable Live Preview on. |
_\* An asterisk denotes that a property is required._
### URL
The `url` property resolves to a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events.
@@ -88,17 +86,16 @@ const config = buildConfig({
// ...
livePreview: {
// highlight-start
url: ({
data,
collectionConfig,
locale
}) => `${data.tenant.url}${ // Multi-tenant top-level domain
collectionConfig.slug === 'posts' ? `/posts/${data.slug}` : `${data.slug !== 'home' : `/${data.slug}` : ''}`
}${locale ? `?locale=${locale?.code}` : ''}`, // Localization query param
url: ({ data, collectionConfig, locale }) =>
`${data.tenant.url}${
collectionConfig.slug === 'posts'
? `/posts/${data.slug}`
: `${data.slug !== 'home' ? `/${data.slug}` : ''}`
}${locale ? `?locale=${locale?.code}` : ''}`, // Localization query param
collections: ['pages'],
},
// highlight-end
}
},
})
```

View File

@@ -51,6 +51,7 @@ export default async function Page() {
collection: 'pages',
id: '123',
draft: true,
trash: true, // add this if trash is enabled in your collection and want to preview trashed documents
})
return (

View File

@@ -194,6 +194,27 @@ const result = await payload.count({
})
```
### FindDistinct#collection-find-distinct
```js
// Result will be an object with:
// {
// values: ['value-1', 'value-2'], // array of distinct values,
// field: 'title', // the field
// totalDocs: 10, // count of the distinct values satisfies query,
// perPage: 10, // count of distinct values per page (based on provided limit)
// }
const result = await payload.findDistinct({
collection: 'posts', // required
locale: 'en',
where: {}, // pass a `where` query here
user: dummyUser,
overrideAccess: false,
field: 'title',
sort: 'title',
})
```
### Update by ID#collection-update-by-id
```js

View File

@@ -0,0 +1,244 @@
---
title: Performance
label: Overview
order: 10
desc: Ensure your Payload app runs as quickly and efficiently as possible.
keywords: performance, optimization, indexes, depth, select, block references, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload is designed with performance in mind, but its customizability means that there are many ways to configure your app that can impact performance.
With this in mind, Payload provides several options and best practices to help you optimize your app's specific performance needs. This includes the database, APIs, and Admin Panel.
Whether you're building an app or troubleshooting an existing one, follow these guidelines to ensure that it runs as quickly and efficiently as possible.
## Building your application
### Database proximity
The proximity of your database to your server can significantly impact performance. Ensure that your database is hosted in the same region as your server to minimize latency and improve response times.
### Indexing your fields
If a particular field is queried often, build an [Index](../database/indexes) for that field to produce faster queries.
When your query runs, the database will not search the entire document to find that one field, but will instead use the index to quickly locate the data.
To learn more, see the [Indexes](../database/indexes) docs.
### Querying your data
There are several ways to optimize your [Queries](../queries/overview). Many of these options directly impact overall database overhead, response sizes, and/or computational load and can significantly improve performance.
When building queries, combine as many of these options together as possible. This will ensure your queries are as efficient as they can be.
To learn more, see the [Query Performance](../queries/overview#performance) docs.
### Optimizing your APIs
When querying data through Payload APIs, the request lifecycle includes running hooks, access control, validations, and other operations that can add significant overhead to the request.
To optimize your APIs, any custom logic should be as efficient as possible. This includes writing lightweight hooks, preventing memory leaks, offloading long-running tasks, and optimizing custom validations.
To learn more, see the [Hooks Performance](../hooks/overview#performance) docs.
### Writing efficient validations
If your validation functions are asynchronous or computationally heavy, ensure they only run when necessary.
To learn more, see the [Validation Performance](../fields/overview#validation-performance) docs.
### Optimizing custom components
When building custom components in the Admin Panel, ensure that they are as efficient as possible. This includes using React best practices such as memoization, lazy loading, and avoiding unnecessary re-renders.
To learn more, see the [Custom Components Performance](../admin/custom-components#performance) docs.
## Other Best Practices
### Block references
Use [Block References](../fields/blocks#block-references) to share the same block across multiple fields without bloating the config. This will reduce the number of fields to traverse when processing permissions, etc. and can significantly reduce the amount of data sent from the server to the client in the Admin Panel.
For example, if you have a block that is used in multiple fields, you can define it once and reference it in each field.
To do this, use the `blockReferences` option in your blocks field:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
blocks: [
{
slug: 'TextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
collections: [
{
slug: 'posts',
fields: [
{
name: 'content',
type: 'blocks',
// highlight-start
blockReferences: ['TextBlock'],
blocks: [], // Required to be empty, for compatibility reasons
// highlight-end
},
],
},
{
slug: 'pages',
fields: [
{
name: 'content',
type: 'blocks',
// highlight-start
blockReferences: ['TextBlock'],
blocks: [], // Required to be empty, for compatibility reasons
// highlight-end
},
],
},
],
})
```
### Using the cached Payload instance
Ensure that you do not instantiate Payload unnecessarily. Instead, Payload provides a caching mechanism to reuse the same instance across your app.
To do this, use the `getPayload` function to get the cached instance of Payload:
```ts
import { getPayload } from 'payload'
import config from '@payload-config'
const myFunction = async () => {
const payload = await getPayload({ config })
// use payload here
}
```
### When to make direct-to-db calls
<Banner type="warning">
**Warning:** Direct database calls bypass all hooks and validations. Only use
this method when you are certain that the operation is safe and does not
require any of these features.
</Banner>
Making direct database calls can significantly improve performance by bypassing much of the request lifecycle such as hooks, validations, and other overhead associated with Payload APIs.
For example, this can be especially useful for the `update` operation, where Payload would otherwise need to make multiple API calls to fetch, update, and fetch again. Making a direct database call can reduce this to a single operation.
To do this, use the `payload.db` methods:
```ts
await payload.db.updateOne({
collection: 'posts',
id: post.id,
data: {
title: 'New Title',
},
})
```
<Banner type="warning">
**Note:** Direct database methods do not start a
[transaction](../database/transactions). You have to start that yourself.
</Banner>
#### Returning
To prevent unnecessary database computation and reduce the size of the response, you can also set `returning: false` in your direct database calls if you don't need the updated document returned to you.
```ts
await payload.db.updateOne({
collection: 'posts',
id: post.id,
data: { title: 'New Title' }, // See note above ^ about Postgres
// highlight-start
returning: false,
// highlight-end
})
```
<Banner type="warning">
**Note:** The `returning` option is only available on direct-to-db methods.
E.g. those on the `payload.db` object. It is not exposed to the Local API.
</Banner>
### Avoid bundling the entire UI library in your front-end
If your front-end imports from `@payloadcms/ui`, ensure that you do not bundle the entire package as this can significantly increase your bundle size.
To do this, import using the full path to the specific component you need:
```ts
import { Button } from '@payloadcms/ui/elements/Button'
```
Custom components within the Admin Panel, however, do not have this same restriction and can import directly from `@payloadcms/ui`:
```ts
import { Button } from '@payloadcms/ui'
```
<Banner type="success">
**Tip:** Use
[`@next/bundle-analyzer`](https://nextjs.org/docs/app/guides/package-bundling)
to analyze your component tree and identify unnecessary re-renders or large
components that could be optimized.
</Banner>
## Optimizing local development
Everything mentioned above applies to local development as well, but there are a few additional steps you can take to optimize your local development experience.
### Enable Turbopack
<Banner type="warning">
**Note:** In the future this will be the default. Use as your own risk.
</Banner>
Add `--turbo` to your dev script to significantly speed up your local development server start time.
```json
{
"scripts": {
"dev": "next dev --turbo"
}
}
```
### Only bundle server packages in production
<Banner type="warning">
**Note:** This is enabled by default in `create-payload-app` since v3.28.0. If
you created your app after this version, you don't need to do anything.
</Banner>
By default, Next.js bundles both server and client code. However, during development, bundling certain server packages isn't necessary.
Payload has thousands of modules, slowing down compilation.
Setting this option skips bundling Payload server modules during development. Fewer files to compile means faster compilation speeds.
To do this, add the `devBundleServerPackages` option to `withPayload` in your `next.config.js` file:
```ts
const nextConfig = {
// your existing next config
}
export default withPayload(nextConfig, { devBundleServerPackages: false })
```

View File

@@ -1,7 +1,7 @@
---
title: Form Builder Plugin
label: Form Builder
order: 40
order: 30
desc: Easily build and manage forms from the Admin Panel. Send dynamic, personalized emails and even accept and process payments.
keywords: plugins, plugin, form, forms, form builder
---

View File

@@ -0,0 +1,155 @@
---
title: Import Export Plugin
label: Import Export
order: 40
desc: Add Import and export functionality to create CSV and JSON data exports
keywords: plugins, plugin, import, export, csv, JSON, data, ETL, download
---
![https://www.npmjs.com/package/@payloadcms/plugin-import-export](https://img.shields.io/npm/v/@payloadcms/plugin-import-export)
<Banner type="warning">
**Note**: This plugin is in **beta** as some aspects of it may change on any
minor releases. It is under development and currently only supports exporting
of collection data.
</Banner>
This plugin adds features that give admin users the ability to download or create export data as an upload collection and import it back into a project.
## Core Features
- Export data as CSV or JSON format via the admin UI
- Download the export directly through the browser
- Create a file upload of the export data
- Use the jobs queue for large exports
- (Coming soon) Import collection data
## Installation
Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com):
```bash
pnpm add @payloadcms/plugin-import-export
```
## Basic Usage
In the `plugins` array of your [Payload Config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options):
```ts
import { buildConfig } from 'payload'
import { importExportPlugin } from '@payloadcms/plugin-import-export'
const config = buildConfig({
collections: [Pages, Media],
plugins: [
importExportPlugin({
collections: ['users', 'pages'],
// see below for a list of available options
}),
],
})
export default config
```
## Options
| Property | Type | Description |
| -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `collections` | string[] | Collections to include Import/Export controls in. Defaults to all collections. |
| `debug` | boolean | If true, enables debug logging. |
| `disableDownload` | boolean | If true, disables the download button in the export preview UI. |
| `disableJobsQueue` | boolean | If true, forces the export to run synchronously. |
| `disableSave` | boolean | If true, disables the save button in the export preview UI. |
| `format` | string | Forces a specific export format (`csv` or `json`), hides the format dropdown, and prevents the user from choosing the export format. |
| `overrideExportCollection` | function | Function to override the default export collection; takes the default export collection and allows you to modify and return it. |
## Field Options
In addition to the above plugin configuration options, you can granularly set the following field level options using the `custom['plugin-import-export']` properties in any of your collections.
| Property | Type | Description |
| ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | boolean | When `true` the field is completely excluded from the import-export plugin. |
| `toCSV` | function | Custom function used to modify the outgoing csv data by manipulating the data, siblingData or by returning the desired value. |
### Customizing the output of CSV data
To manipulate the data that a field exports you can add `toCSV` custom functions. This allows you to modify the outgoing csv data by manipulating the data, siblingData or by returning the desired value.
The toCSV function argument is an object with the following properties:
| Property | Type | Description |
| ------------ | ------- | ----------------------------------------------------------------- |
| `columnName` | string | The CSV column name given to the field. |
| `doc` | object | The top level document |
| `row` | object | The object data that can be manipulated to assign data to the CSV |
| `siblingDoc` | object | The document data at the level where it belongs |
| `value` | unknown | The data for the field. |
Example function:
```ts
const pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'author',
type: 'relationship',
relationTo: 'users',
custom: {
'plugin-import-export': {
toCSV: ({ value, columnName, row }) => {
// add both `author_id` and the `author_email` to the csv export
if (
value &&
typeof value === 'object' &&
'id' in value &&
'email' in value
) {
row[`${columnName}_id`] = (value as { id: number | string }).id
row[`${columnName}_email`] = (value as { email: string }).email
}
},
},
},
},
],
}
```
## Exporting Data
There are four possible ways that the plugin allows for exporting documents, the first two are available in the admin UI from the list view of a collection:
1. Direct download - Using a `POST` to `/api/exports/download` and streams the response as a file download
2. File storage - Goes to the `exports` collection as an uploads enabled collection
3. Local API - A create call to the uploads collection: `payload.create({ slug: 'uploads', ...parameters })`
4. Jobs Queue - `payload.jobs.queue({ task: 'createCollectionExport', input: parameters })`
By default, a user can use the Export drawer to create a file download by choosing `Save` or stream a downloadable file directly without persisting it by using the `Download` button. Either option can be disabled to provide the export experience you desire for your use-case.
The UI for creating exports provides options so that users can be selective about which documents to include and also which columns or fields to include.
It is necessary to add access control to the uploads collection configuration using the `overrideExportCollection` function if you have enabled this plugin on collections with data that some authenticated users should not have access to.
<Banner type="warning">
**Note**: Users who have read access to the upload collection may be able to
download data that is normally not readable due to [access
control](../access-control/overview).
</Banner>
The following parameters are used by the export function to handle requests:
| Property | Type | Description |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `format` | text | Either `csv` or `json` to determine the shape of data exported |
| `limit` | number | The max number of documents to return |
| `sort` | select | The field to use for ordering documents |
| `locale` | string | The locale code to query documents or `all` |
| `draft` | string | Either `yes` or `no` to return documents with their newest drafts for drafts enabled collections |
| `fields` | string[] | Which collection fields are used to create the export, defaults to all |
| `collectionSlug` | string | The slug to query against |
| `where` | object | The WhereObject used to query documents to export. This is set by making selections or filters from the list view |
| `filename` | text | What to call the export being created |

View File

@@ -1,7 +1,7 @@
---
title: Multi-Tenant Plugin
label: Multi-Tenant
order: 40
order: 50
desc: Scaffolds multi-tenancy for your Payload application
keywords: plugins, multi-tenant, multi-tenancy, plugin, payload, cms, seo, indexing, search, search engine
---
@@ -54,7 +54,8 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* After a tenant is deleted, the plugin will attempt
* to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
@@ -67,19 +68,22 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to behave as a global
* Set to `true` if you want the collection to
* behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Set to `false` if you want to manually apply the baseListFilter
* Set to `false` if you want to manually apply
* the baseListFilter
*
* @default true
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
* Set to `false` if you want to handle collection access
* manually without the multi-tenant constraints applied
*
* @default true
*/
@@ -88,7 +92,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
}
/**
* Enables debug mode
* - Makes the tenant field visible in the admin UI within applicable collections
* - Makes the tenant field visible in the
* admin UI within applicable collections
*
* @default false
*/
@@ -100,22 +105,27 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
enabled?: boolean
/**
* Field configuration for the field added to all tenant enabled collections
* Field configuration for the field added
* to all tenant enabled collections
*/
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant enabled collections
* The name of the field added to all tenant
* enabled collections
*
* @default 'tenant'
*/
name?: string
}
/**
* Field configuration for the field added to the users collection
* Field configuration for the field added
* to the users collection
*
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
* If `includeDefaultField` is `false`, you must
* include the field on your users collection manually
* This is useful if you want to customize the field
* or place the field in a specific location
*/
tenantsArrayField?:
| {
@@ -136,7 +146,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
* When `includeDefaultField` is `true`, the field will
* be added to the users collection automatically
*/
includeDefaultField?: true
/**
@@ -153,7 +164,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
arrayFieldName?: string
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
* When `includeDefaultField` is `false`, you must
* include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
@@ -162,7 +174,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* Customize tenant selector label
*
* Either a string or an object where the keys are i18n codes and the values are the string labels
* Either a string or an object where the keys are i18n
* codes and the values are the string labels
*/
tenantSelectorLabel?:
| Partial<{
@@ -176,7 +189,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
tenantsSlug?: string
/**
* Function that determines if a user has access to _all_ tenants
* Function that determines if a user has access
* to _all_ tenants
*
* Useful for super-admin type users
*/
@@ -184,15 +198,18 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
) => boolean
/**
* Opt out of adding access constraints to the tenants collection
* Opt out of adding access constraints to
* the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter tenants by selected tenant
* Opt out including the baseListFilter to filter
* tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter users by selected tenant
* Opt out including the baseListFilter to filter
* users by selected tenant
*/
useUsersTenantFilter?: boolean
}
@@ -212,15 +229,15 @@ const config = buildConfig({
{
slug: 'tenants',
admin: {
useAsTitle: 'name'
}
useAsTitle: 'name',
},
fields: [
// remember, you own these fields
// these are merely suggestions/examples
{
name: 'name',
type: 'text',
required: true,
name: 'name',
type: 'text',
required: true,
},
{
name: 'slug',
@@ -231,7 +248,7 @@ const config = buildConfig({
name: 'domain',
type: 'text',
required: true,
}
},
],
},
],
@@ -241,7 +258,7 @@ const config = buildConfig({
pages: {},
navigation: {
isGlobal: true,
}
},
},
}),
],
@@ -327,14 +344,16 @@ type ContextType = {
/**
* Prevents a refresh when the tenant is changed
*
* If not switching tenants while viewing a "global", set to true
* If not switching tenants while viewing a "global",
* set to true
*/
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
/**
* Sets the selected tenant ID
*
* @param args.id - The ID of the tenant to select
* @param args.refresh - Whether to refresh the page after changing the tenant
* @param args.refresh - Whether to refresh the page
* after changing the tenant
*/
setTenant: (args: {
id: number | string | undefined

View File

@@ -1,7 +1,7 @@
---
title: Nested Docs Plugin
label: Nested Docs
order: 40
order: 60
desc: Nested documents in a parent, child, and sibling relationship.
keywords: plugins, nested, documents, parent, child, sibling, relationship
---

View File

@@ -55,6 +55,7 @@ Payload maintains a set of Official Plugins that solve for some of the common us
- [Sentry](./sentry)
- [SEO](./seo)
- [Stripe](./stripe)
- [Import/Export](./import-export)
You can also [build your own plugin](./build-your-own) to easily extend Payload's functionality in some other way. Once your plugin is ready, consider [sharing it with the community](#community-plugins).

View File

@@ -1,7 +1,7 @@
---
title: Redirects Plugin
label: Redirects
order: 40
order: 70
desc: Automatically create redirects for your Payload application
keywords: plugins, redirects, redirect, plugin, payload, cms, seo, indexing, search, search engine
---

View File

@@ -1,7 +1,7 @@
---
title: Search Plugin
label: Search
order: 40
order: 80
desc: Generates records of your documents that are extremely fast to search on.
keywords: plugins, search, search plugin, search engine, search index, search results, search bar, search box, search field, search form, search input
---

View File

@@ -1,7 +1,7 @@
---
title: Sentry Plugin
label: Sentry
order: 40
order: 90
desc: Integrate Sentry error tracking into your Payload application
keywords: plugins, sentry, error, tracking, monitoring, logging, bug, reporting, performance
---

View File

@@ -2,7 +2,7 @@
description: Manage SEO metadata from your Payload admin
keywords: plugins, seo, meta, search, engine, ranking, google
label: SEO
order: 30
order: 100
title: SEO Plugin
---

View File

@@ -1,7 +1,7 @@
---
title: Stripe Plugin
label: Stripe
order: 40
order: 110
desc: Easily accept payments with Stripe
keywords: plugins, stripe, payments, ecommerce
---

View File

@@ -14,7 +14,9 @@ Solutions:
## Using the experimental-build-mode Next.js build flag
You can run Next.js build using the `pnpx next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpx next build --experimental-build-mode generate` command when you have a DB connection.
You can run Next.js build using the `pnpm next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpm next build --experimental-build-mode generate` command when you have a DB connection.
When running `pnpm next build --experimental-build-mode compile`, environment variables prefixed with `NEXT_PUBLIC` will not be inlined and will be `undefined` on the client. To make these variables available, either run `pnpm next build --experimental-build-mode generate` if a DB connection is available, or use `pnpm next build --experimental-build-mode generate-env` if you do not have a DB connection.
[Next.js documentation](https://nextjs.org/docs/pages/api-reference/cli/next#next-build-options)

View File

@@ -24,16 +24,6 @@ Payload can be deployed _anywhere that Next.js can run_ - including Vercel, Netl
But it's important to remember that most Payload projects will also need a database, file storage, an email provider, and a CDN. Make sure you have all of the requirements that your project needs, no matter what deployment platform you choose.
Often, the easiest and fastest way to deploy Payload is to use [Payload Cloud](https://payloadcms.com/new) — where you get everything you need out of the box, including:
1. A MongoDB Atlas database
1. S3 file storage
1. Resend email service
1. Cloudflare CDN
1. Blue / green deployments
1. Logs
1. And more
## Basics
Payload runs fully in Next.js, so the [Next.js build process](https://nextjs.org/docs/app/building-your-application/deploying) is used for building Payload. If you've used `create-payload-app` to create your project, executing the `build`

View File

@@ -8,7 +8,7 @@ keywords: query, documents, pagination, documentation, Content Management System
Documents in Payload can have relationships to other Documents. This is true for both [Collections](../configuration/collections) as well as [Globals](../configuration/globals). When you query a Document, you can specify the depth at which to populate any of its related Documents either as full objects, or only their IDs.
Depth will optimize the performance of your application by limiting the amount of processing made in the database and significantly reducing the amount of data returned. Since Documents can be infinitely nested or recursively related, it's important to be able to control how deep your API populates.
Since Documents can be infinitely nested or recursively related, it's important to be able to control how deep your API populates. Depth can impact the performance of your queries by affecting the load on the database and the size of the response.
For example, when you specify a `depth` of `0`, the API response might look like this:
@@ -48,7 +48,9 @@ import type { Payload } from 'payload'
const getPosts = async (payload: Payload) => {
const posts = await payload.find({
collection: 'posts',
depth: 2, // highlight-line
// highlight-start
depth: 2,
// highlight-end
})
return posts
@@ -65,7 +67,9 @@ const getPosts = async (payload: Payload) => {
To specify depth in the [REST API](../rest-api/overview), you can use the `depth` parameter in your query:
```ts
fetch('https://localhost:3000/api/posts?depth=2') // highlight-line
// highlight-start
fetch('https://localhost:3000/api/posts?depth=2')
// highlight-end
.then((res) => res.json())
.then((data) => console.log(data))
```
@@ -75,6 +79,24 @@ fetch('https://localhost:3000/api/posts?depth=2') // highlight-line
the `/api/globals` endpoint.
</Banner>
## Default Depth
If no depth is specified in the request, Payload will use its default depth for all requests. By default, this is set to `2`.
To change the default depth on the application level, you can use the `defaultDepth` option in your root Payload config:
```ts
import { buildConfig } from 'payload/config'
export default buildConfig({
// ...
// highlight-start
defaultDepth: 1,
// highlight-end
// ...
})
```
## Max Depth
Fields like the [Relationship Field](../fields/relationship) or the [Upload Field](../fields/upload) can also set a maximum depth. If exceeded, this will limit the population depth regardless of what the depth might be on the request.
@@ -89,7 +111,9 @@ To set a max depth for a field, use the `maxDepth` property in your field config
name: 'author',
type: 'relationship',
relationTo: 'users',
maxDepth: 2, // highlight-line
// highlight-start
maxDepth: 2,
// highlight-end
}
]
}

View File

@@ -60,7 +60,7 @@ The following operators are available for use in queries:
<Banner type="success">
**Tip:** If you know your users will be querying on certain fields a lot, add
`index: true` to the Field Config. This will speed up searches using that
field immensely.
field immensely. [More details](../database/indexes).
</Banner>
### And / Or Logic
@@ -192,3 +192,130 @@ const getPosts = async () => {
// Continue to handle the response below...
}
```
## Performance
There are several ways to optimize your queries. Many of these options directly impact overall database overhead, response sizes, and/or computational load and can significantly improve performance.
When building queries, combine as many of these strategies together as possible to ensure your queries are as performant as they can be.
<Banner type="success">
For more performance tips, see the [Performance
documentation](../performance/overview).
</Banner>
### Indexes
Build [Indexes](../database/indexes) for fields that are often queried or sorted by.
When your query runs, the database will not search the entire document to find that one field, but will instead use the index to quickly locate the data.
This is done by adding `index: true` to the Field Config for that field:
```ts
// In your collection configuration
{
name: 'posts',
fields: [
{
name: 'title',
type: 'text',
// highlight-start
index: true, // Add an index to the title field
// highlight-end
},
// Other fields...
],
}
```
To learn more, see the [Indexes documentation](../database/indexes).
### Depth
Set the [Depth](./depth) to only the level that you need to avoid populating unnecessary related documents.
Relationships will only populate down to the specified depth, and any relationships beyond that depth will only return the ID of the related document.
```ts
const posts = await payload.find({
collection: 'posts',
where: { ... },
// highlight-start
depth: 0, // Only return the IDs of related documents
// highlight-end
})
```
To learn more, see the [Depth documentation](./depth).
### Limit
Set the [Limit](./pagination#limit) if you can reliably predict the number of matched documents, such as when querying on a unique field.
```ts
const posts = await payload.find({
collection: 'posts',
where: {
slug: {
equals: 'unique-post-slug',
},
},
// highlight-start
limit: 1, // Only expect one document to be returned
// highlight-end
})
```
<Banner type="success">
**Tip:** Use in combination with `pagination: false` for best performance when
querying by unique fields.
</Banner>
To learn more, see the [Limit documentation](./pagination#limit).
### Select
Use the [Select API](./select) to only process and return the fields you need.
This will reduce the amount of data returned from the request, and also skip processing of any fields that are not selected, such as running their field hooks.
```ts
const posts = await payload.find({
collection: 'posts',
where: { ... },
// highlight-start
select: [{
title: true,
}],
// highlight-end
```
This is a basic example, but there are many ways to use the Select API, including selecting specific fields, excluding fields, etc.
To learn more, see the [Select documentation](./select).
### Pagination
[Disable Pagination](./pagination#disabling-pagination) if you can reliably predict the number of matched documents, such as when querying on a unique field.
```ts
const posts = await payload.find({
collection: 'posts',
where: {
slug: {
equals: 'unique-post-slug',
},
},
// highlight-start
pagination: false, // Return all matched documents without pagination
// highlight-end
})
```
<Banner type="success">
**Tip:** Use in combination with `limit: 1` for best performance when querying
by unique fields.
</Banner>
To learn more, see the [Pagination documentation](./pagination).

View File

@@ -6,9 +6,61 @@ desc: Payload queries are equipped with automatic pagination so you create pagin
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
All collection `find` queries are paginated automatically. Responses are returned with top-level meta data related to pagination, and returned documents are nested within a `docs` array.
With Pagination you can limit the number of documents returned per page, and get a specific page of results. This is useful for creating paginated lists of documents within your application.
**`Find` response properties:**
All paginated responses include documents nested within a `docs` array, and return top-level meta data related to pagination such as `totalDocs`, `limit`, `totalPages`, `page`, and more.
<Banner type="success">
**Note:** Collection `find` queries are paginated automatically.
</Banner>
## Options
All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application:
| Control | Default | Description |
| ------------ | ------- | ------------------------------------------------------------------------- |
| `limit` | `10` | Limits the number of documents returned per page. [More details](#limit). |
| `pagination` | `true` | Set to `false` to disable pagination and return all documents. |
| `page` | `1` | Get a specific page number. |
## Local API
To specify pagination controls in the [Local API](../local-api/overview), you can use the `limit`, `page`, and `pagination` options in your query:
```ts
import type { Payload } from 'payload'
const getPosts = async (payload: Payload) => {
const posts = await payload.find({
collection: 'posts',
// highlight-start
limit: 10,
page: 2,
// highlight-end
})
return posts
}
```
## REST API
With the [REST API](../rest-api/overview), you can use the pagination controls below as query strings:
```ts
// highlight-start
fetch('https://localhost:3000/api/posts?limit=10&page=2')
// highlight-end
.then((res) => res.json())
.then((data) => console.log(data))
```
## Response
All paginated responses include documents nested within a `docs` array, and return top-level meta data related to pagination.
The `find` operation includes the following properties in its response:
| Property | Description |
| --------------- | --------------------------------------------------------- |
@@ -51,16 +103,59 @@ All collection `find` queries are paginated automatically. Responses are returne
}
```
## Pagination controls
## Limit
All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application:
You can specify a `limit` to restrict the number of documents returned per page.
| Control | Default | Description |
| ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit` | `10` | Limits the number of documents returned per page - set to `0` to show all documents, we automatically disabled pagination for you when `limit` is `0` for optimisation |
| `pagination` | `true` | Set to `false` to disable pagination and return all documents |
| `page` | `1` | Get a specific page number |
<Banner type="warning">
**Reminder:** By default, any query with `limit: 0` will automatically
[disable pagination](#disabling-pagination).
</Banner>
### Disabling pagination within Local API
#### Performance benefits
If you are querying for a specific document and can reliably expect only one document to match, you can set a limit of `1` (or another low number) to reduce the number of database lookups and improve performance.
For example, when querying a document by a unique field such as `slug`, you can set the limit to `1` since you know there will only be one document with that slug.
To do this, set the `limit` option in your query:
```ts
await payload.find({
collection: 'posts',
where: {
slug: {
equals: 'post-1',
},
},
// highlight-start
limit: 1,
// highlight-end
})
```
## Disabling pagination
Disabling pagination can improve performance by reducing the overhead of pagination calculations and improve query speed.
For `find` operations within the Local API, you can disable pagination to retrieve all documents from a collection by passing `pagination: false` to the `find` local operation.
To do this, set `pagination: false` in your query:
```ts
import type { Payload } from 'payload'
const getPost = async (payload: Payload) => {
const posts = await payload.find({
collection: 'posts',
where: {
title: { equals: 'My Post' },
},
// highlight-start
pagination: false,
// highlight-end
})
return posts
}
```

View File

@@ -6,9 +6,9 @@ desc: Payload select determines which fields are selected to the result.
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
By default, Payload's APIs will return _all fields_ for a given collection or global. But, you may not need all of that data for all of your queries. Sometimes, you might want just a few fields from the response, which can speed up the Payload API and reduce the amount of JSON that is sent to you from the API.
By default, Payload's APIs will return _all fields_ for a given collection or global. But, you may not need all of that data for all of your queries. Sometimes, you might want just a few fields from the response.
This is where Payload's `select` feature comes in. Here, you can define exactly which fields you'd like to retrieve from the API.
With the Select API, you can define exactly which fields you'd like to retrieve. This can impact the performance of your queries by affecting the load on the database and the size of the response.
## Local API
@@ -21,6 +21,7 @@ import type { Payload } from 'payload'
const getPosts = async (payload: Payload) => {
const posts = await payload.find({
collection: 'posts',
// highlight-start
select: {
text: true,
// select a specific field from group
@@ -29,7 +30,8 @@ const getPosts = async (payload: Payload) => {
},
// select all fields from array
array: true,
}, // highlight-line
},
// highlight-end
})
return posts
@@ -40,12 +42,14 @@ const getPosts = async (payload: Payload) => {
const posts = await payload.find({
collection: 'posts',
// Select everything except for array and group.number
// highlight-start
select: {
array: false,
group: {
number: false,
},
}, // highlight-line
},
// highlight-end
})
return posts
@@ -67,8 +71,10 @@ To specify select in the [REST API](../rest-api/overview), you can use the `sele
```ts
fetch(
// highlight-start
'https://localhost:3000/api/posts?select[color]=true&select[group][number]=true',
) // highlight-line
// highlight-end
)
.then((res) => res.json())
.then((data) => console.log(data))
```
@@ -149,7 +155,7 @@ export const Pages: CollectionConfig<'pages'> = {
not be able to construct the correct file URL, instead returning `url: null`.
</Banner>
## populate
## Populate
Setting `defaultPopulate` will enforce that each time Payload performs a "population" of a related document, only the fields specified will be queried and returned. However, you can override `defaultPopulate` with the `populate` property in the Local and REST API:

View File

@@ -6,13 +6,15 @@ desc: Payload sort allows you to order your documents by a field in ascending or
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order. If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specified by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields.
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order.
If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specified by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields.
Because sorting is handled by the database, the field cannot be a [Virtual Field](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) unless it's [linked with a relationship field](/docs/fields/relationship#linking-virtual-fields-with-relationships). It must be stored in the database to be searchable.
<Banner type="success">
**Tip:** For performance reasons, it is recommended to enable `index: true`
for the fields that will be sorted upon. [More details](../fields/overview).
for the fields that will be sorted upon. [More details](../database/indexes).
</Banner>
## Local API

View File

@@ -474,11 +474,15 @@ const MyNodeComponent = React.lazy(() =>
)
/**
* This node is a DecoratorNode. DecoratorNodes allow you to render React components in the editor.
* This node is a DecoratorNode. DecoratorNodes allow
* you to render React components in the editor.
*
* They need both createDom and decorate functions. createDom => outside of the html. decorate => React Component inside of the html.
* They need both createDom and decorate functions.
* createDom => outside of the html.
* decorate => React Component inside of the html.
*
* If we used DecoratorBlockNode instead, we would only need a decorate method
* If we used DecoratorBlockNode instead,
* we would only need a decorate method
*/
export class MyNode extends DecoratorNode<React.ReactElement> {
static clone(node: MyNode): MyNode {
@@ -490,9 +494,11 @@ export class MyNode extends DecoratorNode<React.ReactElement> {
}
/**
* Defines what happens if you copy a div element from another page and paste it into the lexical editor
* Defines what happens if you copy a div element
* from another page and paste it into the lexical editor
*
* This also determines the behavior of lexical's internal HTML -> Lexical converter
* This also determines the behavior of lexical's
* internal HTML -> Lexical converter
*/
static importDOM(): DOMConversionMap | null {
return {
@@ -504,14 +510,18 @@ export class MyNode extends DecoratorNode<React.ReactElement> {
}
/**
* The data for this node is stored serialized as JSON. This is the "load function" of that node: it takes the saved data and converts it into a node.
* The data for this node is stored serialized as JSON.
* This is the "load function" of that node: it takes
* the saved data and converts it into a node.
*/
static importJSON(serializedNode: SerializedMyNode): MyNode {
return $createMyNode()
}
/**
* Determines how the hr element is rendered in the lexical editor. This is only the "initial" / "outer" HTML element.
* Determines how the hr element is rendered in the
* lexical editor. This is only the "initial" / "outer"
* HTML element.
*/
createDOM(config: EditorConfig): HTMLElement {
const element = document.createElement('div')
@@ -519,22 +529,28 @@ export class MyNode extends DecoratorNode<React.ReactElement> {
}
/**
* Allows you to render a React component within whatever createDOM returns.
* Allows you to render a React component within
* whatever createDOM returns.
*/
decorate(): React.ReactElement {
return <MyNodeComponent nodeKey={this.__key} />
}
/**
* Opposite of importDOM, this function defines what happens when you copy a div element from the lexical editor and paste it into another page.
* Opposite of importDOM, this function defines what
* happens when you copy a div element from the lexical
* editor and paste it into another page.
*
* This also determines the behavior of lexical's internal Lexical -> HTML converter
* This also determines the behavior of lexical's
* internal Lexical -> HTML converter
*/
exportDOM(): DOMExportOutput {
return { element: document.createElement('div') }
}
/**
* Opposite of importJSON. This determines what data is saved in the database / in the lexical editor state.
* Opposite of importJSON. This determines what
* data is saved in the database / in the lexical
* editor state.
*/
exportJSON(): SerializedLexicalNode {
return {
@@ -556,18 +572,23 @@ export class MyNode extends DecoratorNode<React.ReactElement> {
}
}
// This is used in the importDOM method. Totally optional if you do not want your node to be created automatically when copy & pasting certain dom elements
// into your editor.
// This is used in the importDOM method. Totally optional
// if you do not want your node to be created automatically
// when copy & pasting certain dom elements into your editor.
function $yourConversionMethod(): DOMConversionOutput {
return { node: $createMyNode() }
}
// This is a utility method to create a new MyNode. Utility methods prefixed with $ make it explicit that this should only be used within lexical
// This is a utility method to create a new MyNode.
// Utility methods prefixed with $ make it explicit
// that this should only be used within lexical
export function $createMyNode(): MyNode {
return $applyNodeReplacement(new MyNode())
}
// This is just a utility method you can use to check if a node is a MyNode. This also ensures correct typing.
// This is just a utility method you can use
// to check if a node is a MyNode. This also
// ensures correct typing.
export function $isMyNode(
node: LexicalNode | null | undefined,
): node is MyNode {
@@ -626,10 +647,12 @@ export const INSERT_MYNODE_COMMAND: LexicalCommand<void> = createCommand(
)
/**
* Plugin which registers a lexical command to insert a new MyNode into the editor
* Plugin which registers a lexical command to
* insert a new MyNode into the editor
*/
export const MyNodePlugin: PluginComponent = () => {
// The useLexicalComposerContext hook can be used to access the lexical editor instance
// The useLexicalComposerContext hook can be used
// to access the lexical editor instance
const [editor] = useLexicalComposerContext()
useEffect(() => {

View File

@@ -0,0 +1,494 @@
---
description: Features officially maintained by Payload.
keywords: lexical, rich text, editor, headless cms, official, features
label: Official Features
order: 35
title: Official Features
---
Below are all the Rich Text Features Payload offers. Everything is customizable; you can [create your own features](/docs/rich-text/custom-features), modify ours and share them with the community.
## Features Overview
| Feature Name | Included by default | Description |
| ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`BoldFeature`** | Yes | Adds support for bold text formatting. |
| **`ItalicFeature`** | Yes | Adds support for italic text formatting. |
| **`UnderlineFeature`** | Yes | Adds support for underlined text formatting. |
| **`StrikethroughFeature`** | Yes | Adds support for strikethrough text formatting. |
| **`SubscriptFeature`** | Yes | Adds support for subscript text formatting. |
| **`SuperscriptFeature`** | Yes | Adds support for superscript text formatting. |
| **`InlineCodeFeature`** | Yes | Adds support for inline code formatting. |
| **`ParagraphFeature`** | Yes | Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion. |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Adds support for text alignment (left, center, right, justify) |
| **`IndentFeature`** | Yes | Adds support for text indentation with toolbar buttons |
| **`UnorderedListFeature`** | Yes | Adds support for unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds support for ordered lists (ol) |
| **`ChecklistFeature`** | Yes | Adds support for interactive checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Adds support for horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | Provides a floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | Provides a persistent toolbar pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Provides a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
| **`TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
## In depth
### BoldFeature
- Description: Adds support for bold text formatting, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
- Markdown Support: `**bold**` or `__bold__`
- Keyboard Shortcut: Ctrl/Cmd + B
### ItalicFeature
- Description: Adds support for italic text formatting, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
- Markdown Support: `*italic*` or `_italic_`
- Keyboard Shortcut: Ctrl/Cmd + I
### UnderlineFeature
- Description: Adds support for underlined text formatting, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
- Keyboard Shortcut: Ctrl/Cmd + U
### StrikethroughFeature
- Description: Adds support for strikethrough text formatting, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
- Markdown Support: `~~strikethrough~~`
### SubscriptFeature
- Description: Adds support for subscript text formatting, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
### SuperscriptFeature
- Description: Adds support for superscript text formatting, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
### InlineCodeFeature
- Description: Adds support for inline code formatting with distinct styling, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
- Markdown Support: \`code\`
### ParagraphFeature
- Description: Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion.
- Included by default: Yes
### HeadingFeature
- Description: Adds support for heading nodes (H1-H6) with toolbar dropdown and slash menu entries for each enabled heading size.
- Included by default: Yes
- Markdown Support: `#`, `##`, `###`, ..., at start of line.
- Types:
```ts
type HeadingFeatureProps = {
enabledHeadingSizes?: HeadingTagType[] // ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
}
```
- Usage example:
```ts
HeadingFeature({
enabledHeadingSizes: ['h1', 'h2', 'h3'], // Default: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
})
```
### AlignFeature
- Description: Allows text alignment (left, center, right, justify), along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
- Keyboard Shortcut: Ctrl/Cmd + Shift + L/E/R/J (left/center/right/justify)
### IndentFeature
- Description: Adds support for text indentation, along with buttons to apply it in both fixed and inline toolbars.
- Included by default: Yes
- Keyboard Shortcut: Tab (increase), Shift + Tab (decrease)
- Types:
```ts
type IndentFeatureProps = {
/**
* The nodes that should not be indented. "type"
* property of the nodes you don't want to be indented.
* These can be: "paragraph", "heading", "listitem",
* "quote" or other indentable nodes if they exist.
*/
disabledNodes?: string[]
/**
* If true, pressing Tab in the middle of a block such
* as a paragraph or heading will not insert a tabNode.
* Instead, Tab will only be used for block-level indentation.
* @default false
*/
disableTabNode?: boolean
}
```
- Usage example:
```ts
// Allow block-level indentation only
IndentFeature({
disableTabNode: true,
})
```
### UnorderedListFeature
- Description: Adds support for unordered lists (bullet points) with toolbar dropdown and slash menu entries.
- Included by default: Yes
- Markdown Support: `-`, `*`, or `+` at start of line
### OrderedListFeature
- Description: Adds support for ordered lists (numbered lists) with toolbar dropdown and slash menu entries.
- Included by default: Yes
- Markdown Support: `1.` at start of line
### ChecklistFeature
- Description: Adds support for interactive checklists with toolbar dropdown and slash menu entries.
- Included by default: Yes
- Markdown Support: `- [ ]` (unchecked) or `- [x]` (checked)
### LinkFeature
- Description: Allows creation of internal and external links with toolbar buttons and automatic URL conversion.
- Included by default: Yes
- Markdown Support: `[anchor](url)`
- Types:
```ts
type LinkFeatureServerProps = {
/**
* Disables the automatic creation of links
* from URLs typed or pasted into the editor,
* @default false
*/
disableAutoLinks?: 'creationOnly' | true
/**
* A function or array defining additional
* fields for the link feature.
* These will be displayed in the link editor drawer.
*/
fields?:
| ((args: {
config: SanitizedConfig
defaultFields: FieldAffectingData[]
}) => (Field | FieldAffectingData)[])
| Field[]
/**
* Sets a maximum population depth for the internal
* doc default field of link, regardless of the
* remaining depth when the field is reached.
*/
maxDepth?: number
} & ExclusiveLinkCollectionsProps
type ExclusiveLinkCollectionsProps =
| {
disabledCollections?: CollectionSlug[]
enabledCollections?: never
}
| {
disabledCollections?: never
enabledCollections?: CollectionSlug[]
}
```
- Usage example:
```ts
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
type: 'select',
options: ['noopener', 'noreferrer', 'nofollow'],
},
],
enabledCollections: ['pages', 'posts'], // Collections for internal links
maxDepth: 2, // Population depth for internal links
disableAutoLinks: false, // Allow auto-conversion of URLs
})
```
### RelationshipFeature
- Description: Allows creation of block-level relationships to other documents with toolbar button and slash menu entry.
- Included by default: Yes
- Types:
```ts
type RelationshipFeatureProps = {
/**
* Sets a maximum population depth for this relationship,
* regardless of the remaining depth when the respective
* field is reached.
*/
maxDepth?: number
} & ExclusiveRelationshipFeatureProps
type ExclusiveRelationshipFeatureProps =
| {
disabledCollections?: CollectionSlug[]
enabledCollections?: never
}
| {
disabledCollections?: never
enabledCollections?: CollectionSlug[]
}
```
- Usage example:
```ts
RelationshipFeature({
disabledCollections: ['users'], // Collections to exclude
maxDepth: 2, // Population depth for relationships
})
```
### UploadFeature
- Description: Allows creation of upload/media nodes with toolbar button and slash menu entry, supports all file types.
- Included by default: Yes
- Types:
```ts
type UploadFeatureProps = {
collections?: {
[collection: CollectionSlug]: {
fields: Field[]
}
}
/**
* Sets a maximum population depth for this upload
* (not the fields for this upload), regardless of
* the remaining depth when the respective field is
* reached.
*/
maxDepth?: number
}
```
- Usage example:
```ts
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'text',
label: 'Caption',
},
{
name: 'alt',
type: 'text',
label: 'Alt Text',
},
],
},
},
maxDepth: 1, // Population depth for uploads
})
```
### BlockquoteFeature
- Description: Allows creation of blockquotes with toolbar button and slash menu entry.
- Included by default: Yes
- Markdown Support: `> quote text`
### HorizontalRuleFeature
- Description: Adds support for horizontal rules/separators with toolbar button and slash menu entry.
- Included by default: Yes
- Markdown Support: `---`
### InlineToolbarFeature
- Description: Provides a floating toolbar that appears when text is selected, containing formatting options relevant to selected text.
- Included by default: Yes
### FixedToolbarFeature
- Description: Provides a persistent toolbar pinned to the top of the editor that's always visible.
- Included by default: No
- Types:
```ts
type FixedToolbarFeatureProps = {
/**
* @default false
* If this is enabled, the toolbar will apply
* to the focused editor, not the editor with
* the FixedToolbarFeature.
*/
applyToFocusedEditor?: boolean
/**
* Custom configurations for toolbar groups
* Key is the group key (e.g. 'format', 'indent', 'align')
* Value is a partial ToolbarGroup object that will
* be merged with the default configuration
*/
customGroups?: CustomGroups
/**
* @default false
* If there is a parent editor with a fixed toolbar,
* this will disable the toolbar for this editor.
*/
disableIfParentHasFixedToolbar?: boolean
}
```
- Usage example:
```ts
FixedToolbarFeature({
applyToFocusedEditor: false, // Apply to focused editor
customGroups: {
format: {
// Custom configuration for format group
},
},
})
```
### BlocksFeature
- Description: Allows use of Payload's Blocks Field directly in the editor with toolbar buttons and slash menu entries for each block type.
- Included by default: No
- Types:
```ts
type BlocksFeatureProps = {
blocks?: (Block | BlockSlug)[] | Block[]
inlineBlocks?: (Block | BlockSlug)[] | Block[]
}
```
- Usage example:
```ts
BlocksFeature({
blocks: [
{
slug: 'callout',
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
},
],
inlineBlocks: [
{
slug: 'mention',
fields: [
{
name: 'name',
type: 'text',
required: true,
},
],
},
],
})
```
### TreeViewFeature
- Description: Provides a debug panel below the editor showing the editor's internal state, DOM tree, and time travel debugging.
- Included by default: No
### EXPERIMENTAL_TableFeature
- Description: Adds support for tables with toolbar button and slash menu entry for creation and editing.
- Included by default: No
### TextStateFeature
- Description: Allows storing key-value attributes in text nodes with inline styles and toolbar dropdown for style selection.
- Included by default: No
- Types:
```ts
type TextStateFeatureProps = {
/**
* The keys of the top-level object (stateKeys) represent the attributes that the textNode can have (e.g., color).
* The values of the top-level object (stateValues) represent the values that the attribute can have (e.g., red, blue, etc.).
* Within the stateValue, you can define inline styles and labels.
*/
state: { [stateKey: string]: StateValues }
}
type StateValues = {
[stateValue: string]: {
css: StyleObject
label: string
}
}
type StyleObject = {
[K in keyof PropertiesHyphenFallback]?:
| Extract<PropertiesHyphenFallback[K], string>
| undefined
}
```
- Usage example:
```ts
// We offer default colors that have good contrast and look good in dark and light mode.
import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical'
TextStateFeature({
// prettier-ignore
state: {
color: {
...defaultColors,
// fancy gradients!
galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } },
sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } },
},
// You can have both colored and underlined text at the same time.
// If you don't want that, you should group them within the same key.
// (just like I did with defaultColors and my fancy gradients)
underline: {
'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } },
// You'll probably want to use the CSS light-dark() utility.
'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } },
},
},
}),
```
This is what the example above will look like:
<LightDarkImage
srcDark="https://payloadcms.com/images/docs/text-state-feature.png"
srcLight="https://payloadcms.com/images/docs/text-state-feature.png"
alt="Example usage in light and dark mode for TextStateFeature with defaultColors and some custom styles"
/>

View File

@@ -138,39 +138,9 @@ import { CallToAction } from '../blocks/CallToAction'
| **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. |
| **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. |
## Features overview
## Official Features
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`BoldFeature`** | Yes | Handles the bold text format |
| **`ItalicFeature`** | Yes | Handles the italic text format |
| **`UnderlineFeature`** | Yes | Handles the underline text format |
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
| **`ChecklistFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
| **`EXPERIMENTAL_TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
You can find more information about the official features in our [official features docs](../rich-text/official-features).
## Creating your own, custom Feature

200
docs/trash/overview.mdx Normal file
View File

@@ -0,0 +1,200 @@
---
title: Trash
label: Overview
order: 10
desc: Enable soft deletes for your collections to mark documents as deleted without permanently removing them.
keywords: trash, soft delete, deletedAt, recovery, restore
---
Trash (also known as soft delete) allows documents to be marked as deleted without being permanently removed. When enabled on a collection, deleted documents will receive a `deletedAt` timestamp, making it possible to restore them later, view them in a dedicated Trash view, or permanently delete them.
Soft delete is a safer way to manage content lifecycle, giving editors a chance to review and recover documents that may have been deleted by mistake.
<Banner type="warning">
**Note:** The Trash feature is currently in beta and may be subject to change
in minor version updates.
</Banner>
## Collection Configuration
To enable soft deleting for a collection, set the `trash` property to `true`:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
trash: true,
fields: [
{
name: 'title',
type: 'text',
},
// other fields...
],
}
```
When enabled, Payload automatically injects a deletedAt field into the collection's schema. This timestamp is set when a document is soft-deleted, and cleared when the document is restored.
## Admin Panel behavior
Once `trash` is enabled, the Admin Panel provides a dedicated Trash view for each collection:
- A new route is added at `/collections/:collectionSlug/trash`
- The `Trash` view shows all documents that have a `deletedAt` timestamp
From the Trash view, you can:
- Use bulk actions to manage trashed documents:
- **Restore** to clear the `deletedAt` timestamp and return documents to their original state
- **Delete** to permanently remove selected documents
- **Empty Trash** to select and permanently delete all trashed documents at once
- Enter each document's **edit view**, just like in the main list view. While in the edit view of a trashed document:
- All fields are in a **read-only** state
- Standard document actions (e.g., Save, Publish, Restore Version) are hidden and disabled.
- The available actions are **Restore** and **Permanently Delete**.
- Access to the **API**, **Versions**, and **Preview** views is preserved.
When deleting a document from the main collection List View, Payload will soft-delete the document by default. A checkbox in the delete confirmation modal allows users to skip the trash and permanently delete instead.
## API Support
Soft deletes are fully supported across all Payload APIs: **Local**, **REST**, and **GraphQL**.
The following operations respect and support the `trash` functionality:
- `find`
- `findByID`
- `update`
- `updateByID`
- `delete`
- `deleteByID`
- `findVersions`
- `findVersionByID`
### Understanding `trash` Behavior
Passing `trash: true` to these operations will **include soft-deleted documents** in the query results.
To return _only_ soft-deleted documents, you must combine `trash: true` with a `where` clause that checks if `deletedAt` exists.
### Examples
#### Local API
Return all documents including trashed:
```ts
const result = await payload.find({
collection: 'posts',
trash: true,
})
```
Return only trashed documents:
```ts
const result = await payload.find({
collection: 'posts',
trash: true,
where: {
deletedAt: {
exists: true,
},
},
})
```
Return only non-trashed documents:
```ts
const result = await payload.find({
collection: 'posts',
trash: false,
})
```
#### REST
Return **all** documents including trashed:
```http
GET /api/posts?trash=true
```
Return **only trashed** documents:
```http
GET /api/posts?trash=true&where[deletedAt][exists]=true
```
Return only non-trashed documents:
```http
GET /api/posts?trash=false
```
#### GraphQL
Return all documents including trashed:
```ts
query {
Posts(trash: true) {
docs {
id
deletedAt
}
}
}
```
Return only trashed documents:
```ts
query {
Posts(
trash: true
where: { deletedAt: { exists: true } }
) {
docs {
id
deletedAt
}
}
}
```
Return only non-trashed documents:
```ts
query {
Posts(trash: false) {
docs {
id
deletedAt
}
}
}
```
## Access Control
All trash-related actions (delete, permanent delete) respect the `delete` access control defined in your collection config.
This means:
- If a user is denied delete access, they cannot soft delete or permanently delete documents
## Versions and Trash
When a document is soft-deleted:
- It can no longer have a version **restored** until it is first restored from trash
- Attempting to restore a version while the document is in trash will result in an error
- This ensures consistency between the current document state and its version history
However, versions are still fully **visible and accessible** from the **edit view** of a trashed document. You can view the full version history, but must restore the document itself before restoring any individual version.

View File

@@ -90,31 +90,33 @@ export const Media: CollectionConfig = {
_An asterisk denotes that an option is required._
| Option | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
| **`constructorOptions`** | An object passed to the the Sharp image library that accepts any Constructor options and applies them to the upload file. [More](https://sharp.pixelplumbing.com/api-constructor/) |
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`pasteURL`** | Controls whether files can be uploaded from remote URLs by pasting them into the Upload field. **Enabled by default.** Accepts `false` to disable or an object with an `allowList` of valid remote URLs. [More](#uploading-files-from-remote-urls) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`skipSafeFetch`** | Set to an `allowList` to skip the safe fetch check when fetching external files. Set to `true` to skip the safe fetch for all documents in this collection. Defaults to `false`. |
| **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug |
| **`trimOptions`** | An object passed to the the Sharp image library to trim the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize#trim) |
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. |
| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. |
| Option | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
| **`constructorOptions`** | An object passed to the the Sharp image library that accepts any Constructor options and applies them to the upload file. [More](https://sharp.pixelplumbing.com/api-constructor/) |
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. If using this option, you should handle the removal of any sensitive cookies (like payload-prefixed cookies) to prevent leaking session information to external services. By default, Payload automatically filters out payload-prefixed cookies when this option is not defined. |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`pasteURL`** | Controls whether files can be uploaded from remote URLs by pasting them into the Upload field. **Enabled by default.** Accepts `false` to disable or an object with an `allowList` of valid remote URLs. [More](#uploading-files-from-remote-urls) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`skipSafeFetch`** | Set to an `allowList` to skip the safe fetch check when fetching external files. Set to `true` to skip the safe fetch for all documents in this collection. Defaults to `false`. |
| **`allowRestrictedFileTypes`** | Set to `true` to allow restricted file types. If your Collection has defined [mimeTypes](#mimetypes), restricted file verification will be skipped. Defaults to `false`. [More](#restricted-file-types) |
| **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug |
| **`trimOptions`** | An object passed to the the Sharp image library to trim the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize#trim) |
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. |
| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. |
| **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) |
### Payload-wide Upload Options
@@ -303,6 +305,47 @@ export const Media: CollectionConfig = {
}
```
## Restricted File Types
Possibly problematic file types are automatically restricted from being uploaded to your application.
If your Collection has defined [mimeTypes](#mimetypes) or has set `allowRestrictedFileTypes` to `true`, restricted file verification will be skipped.
Restricted file types and extensions:
| File Extensions | MIME Type |
| ------------------------------------ | ----------------------------------------------- |
| `exe`, `dll` | `application/x-msdownload` |
| `exe`, `com`, `app`, `action` | `application/x-executable` |
| `bat`, `cmd` | `application/x-msdos-program` |
| `exe`, `com` | `application/x-ms-dos-executable` |
| `dmg` | `application/x-apple-diskimage` |
| `deb` | `application/x-debian-package` |
| `rpm` | `application/x-redhat-package-manager` |
| `exe`, `dll` | `application/vnd.microsoft.portable-executable` |
| `msi` | `application/x-msi` |
| `jar`, `ear`, `war` | `application/java-archive` |
| `desktop` | `application/x-desktop` |
| `cpl` | `application/x-cpl` |
| `lnk` | `application/x-ms-shortcut` |
| `pkg` | `application/x-apple-installer` |
| `htm`, `html`, `shtml`, `xhtml` | `text/html` |
| `php`, `phtml` | `application/x-httpd-php` |
| `js`, `jse` | `text/javascript` |
| `jsp` | `application/x-jsp` |
| `py` | `text/x-python` |
| `rb` | `text/x-ruby` |
| `pl` | `text/x-perl` |
| `ps1`, `psc1`, `psd1`, `psh`, `psm1` | `application/x-powershell` |
| `vbe`, `vbs` | `application/x-vbscript` |
| `ws`, `wsc`, `wsf`, `wsh` | `application/x-ms-wsh` |
| `scr` | `application/x-msdownload` |
| `asp`, `aspx` | `application/x-asp` |
| `hta` | `application/x-hta` |
| `reg` | `application/x-registry` |
| `url` | `application/x-url` |
| `workflow` | `application/x-workflow` |
| `command` | `application/x-command` |
## MimeTypes
Specifying the `mimeTypes` property can restrict what files are allowed from the user's file picker. This accepts an array of strings, which can be any valid mimetype or mimetype wildcards
@@ -411,7 +454,7 @@ To fetch files from **restricted URLs** that would otherwise be blocked by CORS,
Heres how to configure the pasteURL option to control remote URL fetching:
```
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
@@ -424,7 +467,7 @@ export const Media: CollectionConfig = {
pathname: '',
port: '',
protocol: 'https',
search: ''
search: '',
},
{
hostname: 'example.com',
@@ -477,3 +520,44 @@ _An asterisk denotes that an option is required._
## Access Control
All files that are uploaded to each Collection automatically support the `read` [Access Control](/docs/access-control/overview) function from the Collection itself. You can use this to control who should be allowed to see your uploads, and who should not.
## Modifying response headers
You can modify the response headers for files by specifying the `modifyResponseHeaders` option in your upload config. This option accepts an object with existing headers and allows you to manipulate the response headers for media files.
### Modifying existing headers
With this method you can directly interface with the `Headers` object and modify the existing headers to append or remove headers.
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
modifyResponseHeaders: ({ headers }) => {
headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning
},
},
}
```
### Return new headers
You can also return a new `Headers` object with the modified headers. This is useful if you want to set new headers or remove existing ones.
```ts
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
modifyResponseHeaders: ({ headers }) => {
const newHeaders = new Headers(headers) // Copy existing headers
newHeaders.set('X-Frame-Options', 'DENY') // Set new header
return newHeaders
},
},
}
```

View File

@@ -292,7 +292,8 @@ Reference any of the existing storage adapters for guidance on how this should b
```ts
export interface GeneratedAdapter {
/**
* Additional fields to be injected into the base collection and image sizes
* Additional fields to be injected into the base
* collection and image sizes
*/
fields?: Field[]
/**

View File

@@ -6,6 +6,8 @@ import { anyone } from './access/anyone'
import { checkRole } from './access/checkRole'
import { loginAfterCreate } from './hooks/loginAfterCreate'
import { protectRoles } from './hooks/protectRoles'
import { access } from 'fs'
import { create } from 'domain'
export const Users: CollectionConfig = {
slug: 'users',
@@ -32,6 +34,34 @@ export const Users: CollectionConfig = {
afterChange: [loginAfterCreate],
},
fields: [
{
name: 'email',
type: 'email',
required: true,
unique: true,
access: {
read: adminsAndUser,
update: adminsAndUser,
},
},
{
name: 'password',
type: 'password',
required: true,
admin: {
description: 'Leave blank to keep the current password.',
},
},
{
name: 'resetPasswordToken',
type: 'text',
hidden: true,
},
{
name: 'resetPasswordExpiration',
type: 'date',
hidden: true,
},
{
name: 'firstName',
type: 'text',
@@ -45,6 +75,11 @@ export const Users: CollectionConfig = {
type: 'select',
hasMany: true,
saveToJWT: true,
access: {
read: admins,
update: admins,
create: admins,
},
hooks: {
beforeChange: [protectRoles],
},

View File

@@ -1,4 +1,5 @@
import type { CollectionConfig } from 'payload'
import { sanitizeUserDataForEmail } from 'payload/shared'
import { generateEmailHTML } from '../email/generateEmailHTML'
@@ -26,7 +27,7 @@ export const Newsletter: CollectionConfig = {
.sendEmail({
from: 'sender@example.com',
html: await generateEmailHTML({
content: `<p>${doc.name ? `Hi ${doc.name}!` : 'Hi!'} We'll be in touch soon...</p>`,
content: `<p>${doc.name ? `Hi ${sanitizeUserDataForEmail(doc.name)}!` : 'Hi!'} We'll be in touch soon...</p>`,
headline: 'Welcome to the newsletter!',
}),
subject: 'Thanks for signing up!',

View File

@@ -1,5 +1,7 @@
import { generateEmailHTML } from './generateEmailHTML'
import { sanitizeUserDataForEmail } from 'payload/shared'
type User = {
email: string
name?: string
@@ -16,7 +18,7 @@ export const generateVerificationEmail = async (
const { token, user } = args
return generateEmailHTML({
content: `<p>Hi${user.name ? ' ' + user.name : ''}! Validate your account by clicking the button below.</p>`,
content: `<p>Hi${user.name ? ' ' + sanitizeUserDataForEmail(user.name) : ''}! Validate your account by clicking the button below.</p>`,
cta: {
buttonLabel: 'Verify',
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/verify?token=${token}&email=${user.email}`,

View File

@@ -53,7 +53,7 @@ export default buildConfig({
admin: {
components: {
// The `BeforeLogin` component renders a message that you see while logging into your admin panel.
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
// Feel free to delete this at any time. Simply remove the line below.
beforeLogin: ['@/components/BeforeLogin'],
afterDashboard: ['@/components/AfterDashboard'],
},

View File

@@ -14,9 +14,12 @@ export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
return true
}
return {
tenant: {
in: getUserTenantIDs(req.user, 'tenant-admin'),
},
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
const requestedTenant = req?.data?.tenant
if (requestedTenant && adminTenantAccessIDs.includes(requestedTenant)) {
return true
}
return false
}

View File

@@ -1,6 +1,6 @@
import type { Access } from 'payload'
import type { User } from '../../../payload-types'
import type { Tenant, User } from '../../../payload-types'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
@@ -14,9 +14,20 @@ export const createAccess: Access<User> = ({ req }) => {
return true
}
if (!isSuperAdmin(req.user) && req.data?.roles?.includes('super-admin')) {
return false
}
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
if (adminTenantAccessIDs.length) {
const requestedTenants: Tenant['id'][] =
req.data?.tenants?.map((t: { tenant: Tenant['id'] }) => t.tenant) ?? []
const hasAccessToAllRequestedTenants = requestedTenants.every((tenantID) =>
adminTenantAccessIDs.includes(tenantID),
)
if (hasAccessToAllRequestedTenants) {
return true
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.44.0",
"version": "3.49.1",
"private": true,
"type": "module",
"workspaces": [
@@ -14,6 +14,7 @@
"build:all": "turbo build --filter \"!blank\" --filter \"!website\"",
"build:app": "next build",
"build:app:analyze": "cross-env ANALYZE=true next build",
"build:bundle-for-analysis": "turbo run build:bundle-for-analysis",
"build:clean": "pnpm clean:build",
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\" --filter \"!@payloadcms/storage-*\" --filter \"!blank\" --filter \"!website\"",
"build:core:force": "pnpm clean:build && pnpm build:core --no-cache --force",
@@ -90,6 +91,10 @@
"reinstall": "pnpm clean:all && pnpm install",
"release": "pnpm --filter releaser release --tag latest",
"runts": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" node --no-deprecation --no-experimental-strip-types --import @swc-node/register/esm-register",
"script:audit": "pnpm audit -P",
"script:audit:critical": "pnpm audit -P --audit-level critical",
"script:audit:high": "pnpm audit -P --audit-level high",
"script:audit:moderate": "pnpm audit -P --audit-level moderate",
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
"script:gen-templates": "pnpm --filter scripts gen-templates",
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
@@ -107,6 +112,7 @@
"test:e2e:prod:ci": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod",
"test:e2e:prod:ci:noturbo": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod --no-turbo",
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:firestore": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=firestore DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:types": "tstyche",
@@ -120,19 +126,18 @@
"prettier --write",
"eslint --cache --fix"
],
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --no-frozen-lockfile --ignore-workspace; pnpm run lint --fix\"",
"templates/**/pnpm-lock.yaml": "pnpm runts scripts/remove-template-lock-files.ts",
"tsconfig.base.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.3.2",
"@next/bundle-analyzer": "15.4.4",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
"@playwright/test": "1.50.0",
"@playwright/test": "1.54.1",
"@sentry/nextjs": "^8.33.1",
"@sentry/node": "^8.33.1",
"@swc-node/register": "1.10.10",
@@ -142,8 +147,8 @@
"@types/jest": "29.5.12",
"@types/minimist": "1.2.5",
"@types/node": "22.15.30",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/shelljs": "0.8.15",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -151,8 +156,8 @@
"create-payload-app": "workspace:*",
"cross-env": "7.0.3",
"dotenv": "16.4.7",
"drizzle-kit": "0.31.0",
"drizzle-orm": "0.43.1",
"drizzle-kit": "0.31.4",
"drizzle-orm": "0.44.2",
"escape-html": "^1.0.3",
"execa": "5.1.1",
"form-data": "3.0.1",
@@ -163,12 +168,12 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "10.1.4",
"next": "15.3.2",
"next": "15.4.4",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"pg": "8.11.3",
"playwright": "1.50.0",
"playwright-core": "1.50.0",
"pg": "8.16.3",
"playwright": "1.54.1",
"playwright-core": "1.54.1",
"prettier": "3.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.44.0",
"version": "3.49.1",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -42,8 +42,8 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.44.0",
"version": "3.49.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.44.0",
"version": "3.49.1",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -36,6 +36,25 @@ export const connect: Connect = async function connect(
try {
this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection
if (this.useAlternativeDropDatabase) {
if (this.connection.db) {
// Firestore doesn't support dropDatabase, so we monkey patch
// dropDatabase to delete all documents from all collections instead
this.connection.db.dropDatabase = async function (): Promise<boolean> {
const existingCollections = await this.listCollections().toArray()
await Promise.all(
existingCollections.map(async (collectionInfo) => {
const collection = this.collection(collectionInfo.name)
await collection.deleteMany({})
}),
)
return true
}
this.connection.dropDatabase = async function () {
await this.db?.dropDatabase()
}
}
}
// If we are running a replica set with MongoDB Memory Server,
// wait until the replica set elects a primary before proceeding

View File

@@ -12,6 +12,7 @@ import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { resolveJoins } from './utilities/resolveJoins.js'
import { transform } from './utilities/transform.js'
export const find: Find = async function find(
@@ -155,6 +156,16 @@ export const find: Find = async function find(
result = await Model.paginate(query, paginationOptions)
}
if (!this.useJoinAggregations) {
await resolveJoins({
adapter: this,
collectionSlug,
docs: result.docs as Record<string, unknown>[],
joins,
locale,
})
}
transform({
adapter: this,
data: result.docs,

View File

@@ -0,0 +1,141 @@
import type { PipelineStage } from 'mongoose'
import { type FindDistinct, getFieldByPath } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
export const findDistinct: FindDistinct = async function (this: MongooseAdapter, args) {
const { collectionConfig, Model } = getCollection({
adapter: this,
collectionSlug: args.collection,
})
const session = await getSession(this, args.req)
const { where = {} } = args
const sortAggregation: PipelineStage[] = []
const sort = buildSortParam({
adapter: this,
config: this.payload.config,
fields: collectionConfig.flattenedFields,
locale: args.locale,
sort: args.sort ?? args.field,
sortAggregation,
timestamps: true,
})
const query = await buildQuery({
adapter: this,
collectionSlug: args.collection,
fields: collectionConfig.flattenedFields,
locale: args.locale,
where,
})
const fieldPathResult = getFieldByPath({
fields: collectionConfig.flattenedFields,
path: args.field,
})
let fieldPath = args.field
if (fieldPathResult?.pathHasLocalized && args.locale) {
fieldPath = fieldPathResult.localizedPath.replace('<locale>', args.locale)
}
const page = args.page || 1
const sortProperty = Object.keys(sort)[0]! // assert because buildSortParam always returns at least 1 key.
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
const pipeline: PipelineStage[] = [
{
$match: query,
},
...(sortAggregation.length > 0 ? sortAggregation : []),
{
$group: {
_id: {
_field: `$${fieldPath}`,
...(sortProperty === fieldPath
? {}
: {
_sort: `$${sortProperty}`,
}),
},
},
},
{
$sort: {
[sortProperty === fieldPath ? '_id._field' : '_id._sort']: sortDirection,
},
},
]
const getValues = async () => {
return Model.aggregate(pipeline, { session }).then((res) =>
res.map((each) => ({
[args.field]: JSON.parse(JSON.stringify(each._id._field)),
})),
)
}
if (args.limit) {
pipeline.push({
$skip: (page - 1) * args.limit,
})
pipeline.push({ $limit: args.limit })
const totalDocs = await Model.aggregate(
[
{
$match: query,
},
{
$group: {
_id: `$${fieldPath}`,
},
},
{ $count: 'count' },
],
{
session,
},
).then((res) => res[0]?.count ?? 0)
const totalPages = Math.ceil(totalDocs / args.limit)
const hasPrevPage = page > 1
const hasNextPage = totalPages > page
const pagingCounter = (page - 1) * args.limit + 1
return {
hasNextPage,
hasPrevPage,
limit: args.limit,
nextPage: hasNextPage ? page + 1 : null,
page,
pagingCounter,
prevPage: hasPrevPage ? page - 1 : null,
totalDocs,
totalPages,
values: await getValues(),
}
}
const values = await getValues()
return {
hasNextPage: false,
hasPrevPage: false,
limit: 0,
page: 1,
pagingCounter: 1,
totalDocs: values.length,
totalPages: 1,
values,
}
}

View File

@@ -10,6 +10,7 @@ import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { resolveJoins } from './utilities/resolveJoins.js'
import { transform } from './utilities/transform.js'
export const findOne: FindOne = async function findOne(
@@ -67,6 +68,16 @@ export const findOne: FindOne = async function findOne(
doc = await Model.findOne(query, {}, options)
}
if (doc && !this.useJoinAggregations) {
await resolveJoins({
adapter: this,
collectionSlug,
docs: [doc] as Record<string, unknown>[],
joins,
locale,
})
}
if (!doc) {
return null
}

View File

@@ -42,6 +42,7 @@ import { deleteOne } from './deleteOne.js'
import { deleteVersions } from './deleteVersions.js'
import { destroy } from './destroy.js'
import { find } from './find.js'
import { findDistinct } from './findDistinct.js'
import { findGlobal } from './findGlobal.js'
import { findGlobalVersions } from './findGlobalVersions.js'
import { findOne } from './findOne.js'
@@ -143,6 +144,29 @@ export interface Args {
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
/**
* Set to `true` to use an alternative `dropDatabase` implementation that calls `collection.deleteMany({})` on every collection instead of sending a raw `dropDatabase` command.
* Payload only uses `dropDatabase` for testing purposes.
* @default false
*/
useAlternativeDropDatabase?: boolean
/**
* Set to `true` to use `BigInt` for custom ID fields of type `'number'`.
* Useful for databases that don't support `double` or `int32` IDs.
* @default false
*/
useBigIntForNumberIDs?: boolean
/**
* Set to `false` to disable join aggregations (which use correlated subqueries) and instead populate join fields via multiple `find` queries.
* @default true
*/
useJoinAggregations?: boolean
/**
* Set to `false` to disable the use of `pipeline` in the `$lookup` aggregation in sorting.
* @default true
*/
usePipelineInSortLookup?: boolean
}
export type MongooseAdapter = {
@@ -159,6 +183,10 @@ export type MongooseAdapter = {
up: (args: MigrateUpArgs) => Promise<void>
}[]
sessions: Record<number | string, ClientSession>
useAlternativeDropDatabase: boolean
useBigIntForNumberIDs: boolean
useJoinAggregations: boolean
usePipelineInSortLookup: boolean
versions: {
[slug: string]: CollectionModel
}
@@ -194,6 +222,10 @@ declare module 'payload' {
updateVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
) => Promise<TypeWithVersion<T>>
useAlternativeDropDatabase: boolean
useBigIntForNumberIDs: boolean
useJoinAggregations: boolean
usePipelineInSortLookup: boolean
versions: {
[slug: string]: CollectionModel
}
@@ -214,6 +246,10 @@ export function mongooseAdapter({
prodMigrations,
transactionOptions = {},
url,
useAlternativeDropDatabase = false,
useBigIntForNumberIDs = false,
useJoinAggregations = true,
usePipelineInSortLookup = true,
}: Args): DatabaseAdapterObj {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(migrationDirArg)
@@ -262,6 +298,7 @@ export function mongooseAdapter({
destroy,
disableFallbackSort,
find,
findDistinct,
findGlobal,
findGlobalVersions,
findOne,
@@ -279,6 +316,10 @@ export function mongooseAdapter({
updateOne,
updateVersion,
upsert,
useAlternativeDropDatabase,
useBigIntForNumberIDs,
useJoinAggregations,
usePipelineInSortLookup,
})
}
@@ -290,6 +331,8 @@ export function mongooseAdapter({
}
}
export { compatabilityOptions } from './utilities/compatabilityOptions.js'
/**
* Attempt to find migrations directory.
*

View File

@@ -143,7 +143,12 @@ export const buildSchema = (args: {
const idField = schemaFields.find((field) => fieldAffectsData(field) && field.name === 'id')
if (idField) {
fields = {
_id: idField.type === 'number' ? Number : String,
_id:
idField.type === 'number'
? payload.db.useBigIntForNumberIDs
? mongoose.Schema.Types.BigInt
: Number
: String,
}
schemaFields = schemaFields.filter(
(field) => !(fieldAffectsData(field) && field.name === 'id'),
@@ -900,7 +905,11 @@ const getRelationshipValueType = (field: RelationshipField | UploadField, payloa
}
if (customIDType === 'number') {
return mongoose.Schema.Types.Number
if (payload.db.useBigIntForNumberIDs) {
return mongoose.Schema.Types.BigInt
} else {
return mongoose.Schema.Types.Number
}
}
return mongoose.Schema.Types.String

View File

@@ -77,6 +77,9 @@ const relationshipSort = ({
) {
const relationshipPath = segments.slice(0, i + 1).join('.')
let sortFieldPath = segments.slice(i + 1, segments.length).join('.')
if (sortFieldPath.endsWith('.id')) {
sortFieldPath = sortFieldPath.split('.').slice(0, -1).join('.')
}
if (Array.isArray(field.relationTo)) {
throw new APIError('Not supported')
}
@@ -96,31 +99,57 @@ const relationshipSort = ({
sortFieldPath = foreignFieldPath.localizedPath.replace('<locale>', locale)
}
if (
!sortAggregation.some((each) => {
return '$lookup' in each && each.$lookup.as === `__${path}`
})
) {
const as = `__${relationshipPath.replace(/\./g, '__')}`
// If we have not already sorted on this relationship yet, we need to add a lookup stage
if (!sortAggregation.some((each) => '$lookup' in each && each.$lookup.as === as)) {
let localField = versions ? `version.${relationshipPath}` : relationshipPath
if (adapter.usePipelineInSortLookup) {
const flattenedField = `__${localField.replace(/\./g, '__')}_lookup`
sortAggregation.push({
$addFields: {
[flattenedField]: `$${localField}`,
},
})
localField = flattenedField
}
sortAggregation.push({
$lookup: {
as: `__${path}`,
as,
foreignField: '_id',
from: foreignCollection.Model.collection.name,
localField: versions ? `version.${relationshipPath}` : relationshipPath,
pipeline: [
{
$project: {
[sortFieldPath]: true,
localField,
...(!adapter.usePipelineInSortLookup && {
pipeline: [
{
$project: {
[sortFieldPath]: true,
},
},
},
],
],
}),
},
})
sort[`__${path}.${sortFieldPath}`] = sortDirection
return true
if (adapter.usePipelineInSortLookup) {
sortAggregation.push({
$unset: localField,
})
}
}
if (!adapter.usePipelineInSortLookup) {
const lookup = sortAggregation.find(
(each) => '$lookup' in each && each.$lookup.as === as,
) as PipelineStage.Lookup
const pipeline = lookup.$lookup.pipeline![0] as PipelineStage.Project
pipeline.$project[sortFieldPath] = true
}
sort[`${as}.${sortFieldPath}`] = sortDirection
return true
}
}

View File

@@ -12,6 +12,7 @@ import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { resolveJoins } from './utilities/resolveJoins.js'
import { transform } from './utilities/transform.js'
export const queryDrafts: QueryDrafts = async function queryDrafts(
@@ -158,6 +159,17 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
result = await Model.paginate(versionQuery, paginationOptions)
}
if (!this.useJoinAggregations) {
await resolveJoins({
adapter: this,
collectionSlug,
docs: result.docs as Record<string, unknown>[],
joins,
locale,
versions: true,
})
}
transform({
adapter: this,
data: result.docs,

View File

@@ -1,4 +1,4 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { MongooseUpdateQueryOptions, UpdateQuery } from 'mongoose'
import type { UpdateOne } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -50,14 +50,20 @@ export const updateOne: UpdateOne = async function updateOne(
let result
transform({ adapter: this, data, fields, operation: 'write' })
const $inc: Record<string, number> = {}
let updateData: UpdateQuery<any> = data
transform({ $inc, adapter: this, data, fields, operation: 'write' })
if (Object.keys($inc).length) {
updateData = { $inc, $set: updateData }
}
try {
if (returning === false) {
await Model.updateOne(query, data, options)
await Model.updateOne(query, updateData, options)
transform({ adapter: this, data, fields, operation: 'read' })
return null
} else {
result = await Model.findOneAndUpdate(query, data, options)
result = await Model.findOneAndUpdate(query, updateData, options)
}
} catch (error) {
handleError({ collection: collectionSlug, error, req })

View File

@@ -76,7 +76,11 @@ export const aggregatePaginate = async ({
countPromise = Model.estimatedDocumentCount(query)
} else {
const hint = adapter.disableIndexHints !== true ? { _id: 1 } : undefined
countPromise = Model.countDocuments(query, { collation, hint, session })
countPromise = Model.countDocuments(query, {
collation,
session,
...(hint ? { hint } : {}),
})
}
}

View File

@@ -44,6 +44,9 @@ export const buildJoinAggregation = async ({
projection,
versions,
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
if (!adapter.useJoinAggregations) {
return
}
if (
(Object.keys(collectionConfig.joins).length === 0 &&
collectionConfig.polymorphicJoins.length == 0) ||

View File

@@ -0,0 +1,25 @@
import type { Args } from '../index.js'
/**
* Each key is a mongo-compatible database and the value
* is the recommended `mongooseAdapter` settings for compatability.
*/
export const compatabilityOptions = {
cosmosdb: {
transactionOptions: false,
useJoinAggregations: false,
usePipelineInSortLookup: false,
},
documentdb: {
disableIndexHints: true,
},
firestore: {
disableIndexHints: true,
ensureIndexes: false,
transactionOptions: false,
useAlternativeDropDatabase: true,
useBigIntForNumberIDs: true,
useJoinAggregations: false,
usePipelineInSortLookup: false,
},
} satisfies Record<string, Partial<Args>>

View File

@@ -2,6 +2,15 @@ import type { PayloadRequest } from 'payload'
import { ValidationError } from 'payload'
function extractFieldFromMessage(message: string) {
// eslint-disable-next-line regexp/no-super-linear-backtracking
const match = message.match(/index:\s*(.*?)_/)
if (match && match[1]) {
return match[1] // e.g., returns "email" from "index: email_1"
}
return null
}
export const handleError = ({
collection,
error,
@@ -18,20 +27,22 @@ export const handleError = ({
}
// Handle uniqueness error from MongoDB
if (
'code' in error &&
error.code === 11000 &&
'keyValue' in error &&
error.keyValue &&
typeof error.keyValue === 'object'
) {
if ('code' in error && error.code === 11000) {
let path: null | string = null
if ('keyValue' in error && error.keyValue && typeof error.keyValue === 'object') {
path = Object.keys(error.keyValue)[0] ?? ''
} else if ('message' in error && typeof error.message === 'string') {
path = extractFieldFromMessage(error.message)
}
throw new ValidationError(
{
collection,
errors: [
{
message: req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique',
path: Object.keys(error.keyValue)[0] ?? '',
path: path ?? '',
},
],
global,

View File

@@ -0,0 +1,647 @@
import type { JoinQuery, SanitizedJoins, Where } from 'payload'
import {
appendVersionToQueryKey,
buildVersionCollectionFields,
combineQueries,
getQueryDraftsSort,
} from 'payload'
import { fieldShouldBeLocalized } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
import { buildQuery } from '../queries/buildQuery.js'
import { buildSortParam } from '../queries/buildSortParam.js'
import { transform } from './transform.js'
export type ResolveJoinsArgs = {
/** The MongoDB adapter instance */
adapter: MongooseAdapter
/** The slug of the collection being queried */
collectionSlug: string
/** Array of documents to resolve joins for */
docs: Record<string, unknown>[]
/** Join query specifications (which joins to resolve and how) */
joins?: JoinQuery
/** Optional locale for localized queries */
locale?: string
/** Optional projection for the join query */
projection?: Record<string, true>
/** Whether to resolve versions instead of published documents */
versions?: boolean
}
/**
* Resolves join relationships for a collection of documents.
* This function fetches related documents based on join configurations and
* attaches them to the original documents with pagination support.
*/
export async function resolveJoins({
adapter,
collectionSlug,
docs,
joins,
locale,
projection,
versions = false,
}: ResolveJoinsArgs): Promise<void> {
// Early return if no joins are specified or no documents to process
if (!joins || docs.length === 0) {
return
}
// Get the collection configuration from the adapter
const collectionConfig = adapter.payload.collections[collectionSlug]?.config
if (!collectionConfig) {
return
}
// Build a map of join paths to their configurations for quick lookup
// This flattens the nested join structure into a single map keyed by join path
const joinMap: Record<string, { targetCollection: string } & SanitizedJoin> = {}
// Add regular joins
for (const [target, joinList] of Object.entries(collectionConfig.joins)) {
for (const join of joinList) {
joinMap[join.joinPath] = { ...join, targetCollection: target }
}
}
// Add polymorphic joins
for (const join of collectionConfig.polymorphicJoins || []) {
// For polymorphic joins, we use the collections array as the target
joinMap[join.joinPath] = { ...join, targetCollection: join.field.collection as string }
}
// Process each requested join concurrently
const joinPromises = Object.entries(joins).map(async ([joinPath, joinQuery]) => {
if (!joinQuery) {
return null
}
// If a projection is provided, and the join path is not in the projection, skip it
if (projection && !projection[joinPath]) {
return null
}
// Get the join definition from our map
const joinDef = joinMap[joinPath]
if (!joinDef) {
return null
}
// Normalize collections to always be an array for unified processing
const allCollections = Array.isArray(joinDef.field.collection)
? joinDef.field.collection
: [joinDef.field.collection]
// Use the provided locale or fall back to the default locale for localized fields
const localizationConfig = adapter.payload.config.localization
const effectiveLocale =
locale ||
(typeof localizationConfig === 'object' &&
localizationConfig &&
localizationConfig.defaultLocale)
// Extract relationTo filter from the where clause to determine which collections to query
const relationToFilter = extractRelationToFilter(joinQuery.where || {})
// Determine which collections to query based on relationTo filter
const collections = relationToFilter
? allCollections.filter((col) => relationToFilter.includes(col))
: allCollections
// Check if this is a polymorphic collection join (where field.collection is an array)
const isPolymorphicJoin = Array.isArray(joinDef.field.collection)
// Apply pagination settings
const limit = joinQuery.limit ?? joinDef.field.defaultLimit ?? 10
const page = joinQuery.page ?? 1
const skip = (page - 1) * limit
// Process collections concurrently
const collectionPromises = collections.map(async (joinCollectionSlug) => {
const targetConfig = adapter.payload.collections[joinCollectionSlug]?.config
if (!targetConfig) {
return null
}
const useDrafts = versions && Boolean(targetConfig.versions?.drafts)
let JoinModel
if (useDrafts) {
JoinModel = adapter.versions[targetConfig.slug]
} else {
JoinModel = adapter.collections[targetConfig.slug]
}
if (!JoinModel) {
return null
}
// Extract all parent document IDs to use in the join query
const parentIDs = docs.map((d) => (versions ? (d.parent ?? d._id ?? d.id) : (d._id ?? d.id)))
// Build the base query
let whereQuery: null | Record<string, unknown> = null
whereQuery = isPolymorphicJoin
? filterWhereForCollection(
joinQuery.where || {},
targetConfig.flattenedFields,
true, // exclude relationTo for individual collections
)
: joinQuery.where || {}
// Skip this collection if the WHERE clause cannot be satisfied for polymorphic collection joins
if (whereQuery === null) {
return null
}
whereQuery = useDrafts
? await JoinModel.buildQuery({
locale,
payload: adapter.payload,
where: combineQueries(appendVersionToQueryKey(whereQuery as Where), {
latest: {
equals: true,
},
}),
})
: await buildQuery({
adapter,
collectionSlug: joinCollectionSlug,
fields: targetConfig.flattenedFields,
locale,
where: whereQuery as Where,
})
// Handle localized paths and version prefixes
let dbFieldName = joinDef.field.on
if (effectiveLocale && typeof localizationConfig === 'object' && localizationConfig) {
const pathSegments = joinDef.field.on.split('.')
const transformedSegments: string[] = []
const fields = useDrafts
? buildVersionCollectionFields(adapter.payload.config, targetConfig, true)
: targetConfig.flattenedFields
for (let i = 0; i < pathSegments.length; i++) {
const segment = pathSegments[i]!
transformedSegments.push(segment)
// Check if this segment corresponds to a localized field
const fieldAtSegment = fields.find((f) => f.name === segment)
if (fieldAtSegment && fieldAtSegment.localized) {
transformedSegments.push(effectiveLocale)
}
}
dbFieldName = transformedSegments.join('.')
}
// Add version prefix for draft queries
if (useDrafts) {
dbFieldName = `version.${dbFieldName}`
}
// Check if the target field is a polymorphic relationship
const isPolymorphic = joinDef.targetField
? Array.isArray(joinDef.targetField.relationTo)
: false
if (isPolymorphic) {
// For polymorphic relationships, we need to match both relationTo and value
whereQuery[`${dbFieldName}.relationTo`] = collectionSlug
whereQuery[`${dbFieldName}.value`] = { $in: parentIDs }
} else {
// For regular relationships and polymorphic collection joins
whereQuery[dbFieldName] = { $in: parentIDs }
}
// Build the sort parameters for the query
const fields = useDrafts
? buildVersionCollectionFields(adapter.payload.config, targetConfig, true)
: targetConfig.flattenedFields
const sort = buildSortParam({
adapter,
config: adapter.payload.config,
fields,
locale,
sort: useDrafts
? getQueryDraftsSort({
collectionConfig: targetConfig,
sort: joinQuery.sort || joinDef.field.defaultSort || targetConfig.defaultSort,
})
: joinQuery.sort || joinDef.field.defaultSort || targetConfig.defaultSort,
timestamps: true,
})
const projection = buildJoinProjection(dbFieldName, useDrafts, sort)
const [results, dbCount] = await Promise.all([
JoinModel.find(whereQuery, projection, {
sort,
...(isPolymorphicJoin ? {} : { limit, skip }),
}).lean(),
isPolymorphicJoin ? Promise.resolve(0) : JoinModel.countDocuments(whereQuery),
])
const count = isPolymorphicJoin ? results.length : dbCount
transform({
adapter,
data: results,
fields: useDrafts
? buildVersionCollectionFields(adapter.payload.config, targetConfig, false)
: targetConfig.fields,
operation: 'read',
})
// Return results with collection info for grouping
return {
collectionSlug: joinCollectionSlug,
count,
dbFieldName,
results,
sort,
useDrafts,
}
})
const collectionResults = await Promise.all(collectionPromises)
// Group the results by parent ID
const grouped: Record<
string,
{
docs: Record<string, unknown>[]
sort: Record<string, string>
}
> = {}
let totalCount = 0
for (const collectionResult of collectionResults) {
if (!collectionResult) {
continue
}
const { collectionSlug, count, dbFieldName, results, sort, useDrafts } = collectionResult
totalCount += count
for (const result of results) {
if (useDrafts) {
result.id = result.parent
}
const parentValues = getByPathWithArrays(result, dbFieldName) as (
| { relationTo: string; value: number | string }
| number
| string
)[]
if (parentValues.length === 0) {
continue
}
for (let parentValue of parentValues) {
if (!parentValue) {
continue
}
if (typeof parentValue === 'object') {
parentValue = parentValue.value
}
const joinData = {
relationTo: collectionSlug,
value: result.id,
}
const parentKey = parentValue as string
if (!grouped[parentKey]) {
grouped[parentKey] = {
docs: [],
sort,
}
}
// Always store the ObjectID reference in polymorphic format
grouped[parentKey].docs.push({
...result,
__joinData: joinData,
})
}
}
}
for (const results of Object.values(grouped)) {
results.docs.sort((a, b) => {
for (const [fieldName, sortOrder] of Object.entries(results.sort)) {
const sort = sortOrder === 'asc' ? 1 : -1
const aValue = a[fieldName] as Date | number | string
const bValue = b[fieldName] as Date | number | string
if (aValue < bValue) {
return -1 * sort
}
if (aValue > bValue) {
return 1 * sort
}
}
return 0
})
results.docs = results.docs.map(
(doc) => (isPolymorphicJoin ? doc.__joinData : doc.id) as Record<string, unknown>,
)
}
// Determine if the join field should be localized
const localeSuffix =
fieldShouldBeLocalized({
field: joinDef.field,
parentIsLocalized: joinDef.parentIsLocalized,
}) &&
adapter.payload.config.localization &&
effectiveLocale
? `.${effectiveLocale}`
: ''
// Adjust the join path with locale suffix if needed
const localizedJoinPath = `${joinPath}${localeSuffix}`
return {
grouped,
isPolymorphicJoin,
joinQuery,
limit,
localizedJoinPath,
page,
skip,
totalCount,
}
})
// Wait for all join operations to complete
const joinResults = await Promise.all(joinPromises)
// Process the results and attach them to documents
for (const joinResult of joinResults) {
if (!joinResult) {
continue
}
const { grouped, isPolymorphicJoin, joinQuery, limit, localizedJoinPath, skip, totalCount } =
joinResult
// Attach the joined data to each parent document
for (const doc of docs) {
const id = (versions ? (doc.parent ?? doc._id ?? doc.id) : (doc._id ?? doc.id)) as string
const all = grouped[id]?.docs || []
// Calculate the slice for pagination
// When limit is 0, it means unlimited - return all results
const slice = isPolymorphicJoin
? limit === 0
? all
: all.slice(skip, skip + limit)
: // For non-polymorphic joins, we assume that page and limit were applied at the database level
all
// Create the join result object with pagination metadata
const value: Record<string, unknown> = {
docs: slice,
hasNextPage: limit === 0 ? false : totalCount > skip + slice.length,
}
// Include total count if requested
if (joinQuery.count) {
value.totalDocs = totalCount
}
// Navigate to the correct nested location in the document and set the join data
// This handles nested join paths like "user.posts" by creating intermediate objects
const segments = localizedJoinPath.split('.')
let ref: Record<string, unknown>
if (versions) {
if (!doc.version) {
doc.version = {}
}
ref = doc.version as Record<string, unknown>
} else {
ref = doc
}
for (let i = 0; i < segments.length - 1; i++) {
const seg = segments[i]!
if (!ref[seg]) {
ref[seg] = {}
}
ref = ref[seg] as Record<string, unknown>
}
// Set the final join data at the target path
ref[segments[segments.length - 1]!] = value
}
}
}
/**
* Extracts relationTo filter values from a WHERE clause
* @param where - The WHERE clause to search
* @returns Array of collection slugs if relationTo filter found, null otherwise
*/
function extractRelationToFilter(where: Record<string, unknown>): null | string[] {
if (!where || typeof where !== 'object') {
return null
}
// Check for direct relationTo conditions
if (where.relationTo && typeof where.relationTo === 'object') {
const relationTo = where.relationTo as Record<string, unknown>
if (relationTo.in && Array.isArray(relationTo.in)) {
return relationTo.in as string[]
}
if (relationTo.equals) {
return [relationTo.equals as string]
}
}
// Check for relationTo in logical operators
if (where.and && Array.isArray(where.and)) {
for (const condition of where.and) {
const result = extractRelationToFilter(condition)
if (result) {
return result
}
}
}
if (where.or && Array.isArray(where.or)) {
for (const condition of where.or) {
const result = extractRelationToFilter(condition)
if (result) {
return result
}
}
}
return null
}
/**
* Filters a WHERE clause to only include fields that exist in the target collection
* This is needed for polymorphic joins where different collections have different fields
* @param where - The original WHERE clause
* @param availableFields - The fields available in the target collection
* @param excludeRelationTo - Whether to exclude relationTo field (for individual collections)
* @returns A filtered WHERE clause, or null if the query cannot match this collection
*/
function filterWhereForCollection(
where: Record<string, unknown>,
availableFields: Array<{ name: string }>,
excludeRelationTo: boolean = false,
): null | Record<string, unknown> {
if (!where || typeof where !== 'object') {
return where
}
const fieldNames = new Set(availableFields.map((f) => f.name))
// Add special fields that are available in polymorphic relationships
if (!excludeRelationTo) {
fieldNames.add('relationTo')
}
const filtered: Record<string, unknown> = {}
for (const [key, value] of Object.entries(where)) {
if (key === 'and') {
// Handle AND operator - all conditions must be satisfiable
if (Array.isArray(value)) {
const filteredConditions: Record<string, unknown>[] = []
for (const condition of value) {
const filteredCondition = filterWhereForCollection(
condition,
availableFields,
excludeRelationTo,
)
// If any condition in AND cannot be satisfied, the whole AND fails
if (filteredCondition === null) {
return null
}
if (Object.keys(filteredCondition).length > 0) {
filteredConditions.push(filteredCondition)
}
}
if (filteredConditions.length > 0) {
filtered[key] = filteredConditions
}
}
} else if (key === 'or') {
// Handle OR operator - at least one condition must be satisfiable
if (Array.isArray(value)) {
const filteredConditions = value
.map((condition) =>
filterWhereForCollection(condition, availableFields, excludeRelationTo),
)
.filter((condition) => condition !== null && Object.keys(condition).length > 0)
if (filteredConditions.length > 0) {
filtered[key] = filteredConditions
}
// If no OR conditions can be satisfied, we still continue (OR is more permissive)
}
} else if (key === 'relationTo' && excludeRelationTo) {
// Skip relationTo field for non-polymorphic collections
continue
} else if (fieldNames.has(key)) {
// Include the condition if the field exists in this collection
filtered[key] = value
} else {
// Field doesn't exist in this collection - this makes the query unsatisfiable
return null
}
}
return filtered
}
type SanitizedJoin = SanitizedJoins[string][number]
/**
* Builds projection for join queries
*/
function buildJoinProjection(
baseFieldName: string,
useDrafts: boolean,
sort: Record<string, string>,
): Record<string, 1> {
const projection: Record<string, 1> = {
_id: 1,
[baseFieldName]: 1,
}
if (useDrafts) {
projection.parent = 1
}
for (const fieldName of Object.keys(sort)) {
projection[fieldName] = 1
}
return projection
}
/**
* Enhanced utility function to safely traverse nested object properties using dot notation
* Handles arrays by searching through array elements for matching values
* @param doc - The document to traverse
* @param path - Dot-separated path (e.g., "array.category")
* @returns Array of values found at the specified path (for arrays) or single value
*/
function getByPathWithArrays(doc: unknown, path: string): unknown[] {
const segments = path.split('.')
let current = doc
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]!
if (current === undefined || current === null) {
return []
}
// Get the value at the current segment
const value = (current as Record<string, unknown>)[segment]
if (value === undefined || value === null) {
return []
}
// If this is the last segment, return the value(s)
if (i === segments.length - 1) {
return Array.isArray(value) ? value : [value]
}
// If the value is an array and we have more segments to traverse
if (Array.isArray(value)) {
const remainingPath = segments.slice(i + 1).join('.')
const results: unknown[] = []
// Search through each array element
for (const item of value) {
if (item && typeof item === 'object') {
const subResults = getByPathWithArrays(item, remainingPath)
results.push(...subResults)
}
}
return results
}
// Continue traversing
current = value
}
return []
}

Some files were not shown because too many files have changed in this diff Show More