Commit Graph

2419 Commits

Author SHA1 Message Date
Elliot DeNolf e818a01dc0 chore(release): v3.67.0 [skip ci] 2025-12-05 15:24:38 -05:00
Patrik 2b7aa7aff4 feat: add groupBy support to query presets (#14808)
### What?

Adds `groupBy` field support to query presets. Presets now save, apply,
and clear groupBy state when switching between presets.

### Why?

Previously, `groupBy` settings would persist when switching between
presets. GroupBy was not being saved as part of the preset
configuration, so users couldn't save their preferred grouping with a
preset.

### How?

Added `groupBy?: string` to the `QueryPreset` type and updated the
preset collection config to include the field.

Modified `QueryPresetBar` to handle groupBy when selecting, deselecting,
resetting, saving, or creating presets.

Created custom Field and Cell components to display groupBy values in
the admin UI.

#### Preset creation with group-by added
<img width="711" height="684" alt="Screenshot 2025-12-03 at 3 15 03 PM"
src="https://github.com/user-attachments/assets/5fe6ee88-6ed7-40f6-b190-df6b2e89460c"
/>

#### Preset collection list view with group-by preset
<img width="2233" height="537" alt="Screenshot 2025-12-03 at 3 15 26 PM"
src="https://github.com/user-attachments/assets/a56645fc-9106-4885-8c54-0e000c429499"
/>

#### Preset creation without group-by
<img width="1250" height="704" alt="Screenshot 2025-12-03 at 3 15 48 PM"
src="https://github.com/user-attachments/assets/3438585b-7aa9-4336-9101-2fab59733480"
/>
2025-12-04 12:57:56 -08:00
Jens Becker 22a02557d6 fix: add missing beforeInput and afterInput properties to UploadAdmin type (#14775)
### What?

This PR adds the missing `beforeInput` and `afterInput` properties to
the `UploadAdmin` type so that the type definition reflects the actual
supported admin props for upload fields.

### Why?

Although upload fields [already support `beforeInput` and
`afterInput`](https://github.com/payloadcms/payload/blob/fb93cd13bcd66dca3777826fc420a941d3cb75ab/packages/ui/src/fields/Upload/index.tsx#L51)
in practice, these props are currently not included in the `UploadAdmin`
TypeScript type. This leads to type errors or the need for type
assertions when trying to use them. Aligning the type definition with
the existing runtime behavior improves DX and keeps the admin field
types consistent, similar to what was done for RichTextField in #12889.

### How?

Extended the UploadAdmin type to include the `beforeInput` and
`afterInput` properties.
2025-12-04 11:19:08 -06:00
Jarrod Flesch e9cd2a5333 fix: add missing afterOperation and beforeOperation hook calls (#14778)
Fixes https://github.com/payloadcms/payload/issues/14726

Refactors how we call beforeOperation hooks more similarly to how we
call afterOperation hooks.

Adds beforeOperations and afterOperations functions to operations. 

Both were missing in `unlock`, `findVersions` and `findVersionByID`
2025-12-04 10:14:18 -05:00
Jarrod Flesch 51c951fbcc fix: sanitized versions type was incorrect (#14810)
Updates the config.versions (on the sanitized config) to be proper.

#### Before
```ts
versions: SanitizedCollectionVersions // not accurate 
```

#### After
```ts
versions?: SanitizedCollectionVersions
```

Previously sanitization did not remove the versions key when it was set
to false so the actual type should have been `versions:
SanitizedCollectionVersions | false`.

I chose to remove the versions key if it is false in the sanitization
function so it is easier to work with, i.e. optional chaining.

Also adds importable helper functions:
```ts
import {
  getAutosaveInterval,
  getVersionsMax,
  hasAutosaveEnabled,
  hasDraftsEnabled,
  hasDraftValidationEnabled,
  hasScheduledPublishEnabled,
} from 'payload/shared'
```
2025-12-04 10:02:21 -05:00
Elliot DeNolf 7cc28c4327 chore(release): v3.66.0 [skip ci] 2025-12-03 16:07:25 -05:00
Jake Fletcher 59a1607a3f feat: support custom slugify functions (#14117)
Continuation of #14007.

Supports overriding the default slugify function of the slug field. This
is necessary if the slug requires special treatment, such as character
encoding, additional language support, etc.

For example, if you wanted to use the
[`slugify`](https://www.npmjs.com/package/slugify) package, you could do
something like this:

```ts
import type { CollectionConfig } from 'payload'
import { slugField } from 'payload'
import slugify from 'slugify';

export const MyCollection: CollectionConfig = {
  // ...
  fields: [
    // ...
    slugField({
      slugify: ({ valueToSlugify }) => slugify(valueToSlugify, {
        // ...additional `slugify` options here
      })
    })
  ]
}
```

This PR also deprecates the old `fieldToUse` arg in favor of `useAsSlug`
which is more semantically clear, following the same convention as
`useAsTitle`.

Example:

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

export const MyCollection: CollectionConfig = {
  // ...
  fields: [
    // ...
    slugField({
      useAsSlug: 'myCustomTitle'
    })
  ]
}
```

In follow-up PRs, we should also:
- Improve the default slugify function to better handle special
characters on its own, etc.
- [Support nested
slugs](https://github.com/payloadcms/payload/pull/14783)
2025-12-02 17:14:48 +00:00
Paul 8c8def9cfc fix: remove "all" from JSDoc comment on supported http methods in endpoints (#14777)
Removes the suggestion that "all" is a supported HTTP method in
endpoints since it's not supported and it's not a valid method either.

Closes https://github.com/payloadcms/payload/issues/14732
2025-12-01 21:25:26 +00:00
Jessica Rynkar 015b3633c7 fix: should exclude svg files from file buffer mime validation (#14751)
### What?
Fix bug of `svg` files throwing an error when `mimeTypes: ['image/*']`
is set.

### Why?
We recently added an additional mime type check on the buffer however
the `file-buffer` package that we use to detect file type does not
support svgs.

### How?
Updates `expectsDetectableType` logic to exclude svgs.

Fixes #14743
2025-11-27 17:58:59 +00:00
Tobias Odendahl cb3a078a15 feat: support external JSON schema file references in type generation (#14749)
## Description

Enables the use of external JSON schema file references in type
generation by adding the `cwd` parameter to
`json-schema-to-typescript`'s `compile()` function.

## Changes

- Added `cwd: process.cwd()` parameter to `generateTypes()` compile call
- Enables `$ref` pointers to external `.json` schema files in
`typescriptSchema` field config
- Added test case with external schema file reference
- Documented external schema reference usage in generating-types.mdx

## Test Coverage

✅ New test: `resolves external schema file references`
- Created `test/types/schemas/custom-type.json` as external schema
- Added field with `typescriptSchema` referencing the external file
- Verified generated types include the external type correctly

## Example Usage

```typescript
// payload.config.ts
{
  typescript: {
    schema: [
      ({ jsonSchema }) => {
        jsonSchema.definitions.MyType = {
          $ref: './schemas/my-type.json'
        }
        return jsonSchema
      },
    ]
  }
}
```

External references are resolved relative to `process.cwd()`.

## Related Issue

This was identified during investigation of JSON Schema parser
capabilities. The `cwd` parameter is a standard
`json-schema-to-typescript` option that was not being passed.
2025-11-27 14:48:49 +00:00
Elliot DeNolf fde824be13 chore(release): v3.65.0 [skip ci] 2025-11-25 13:37:17 -05:00
Patrik 850c25291b feat: add augmentable interfaces for collection and global custom properties (#14729)
### What?

Adds augmentable interfaces for the `custom` property on collections and
globals:
- `CollectionCustom` 
- `CollectionAdminCustom` 
- `GlobalCustom` 
- `GlobalAdminCustom`

Also updates `plugin-import-export` to use `CollectionAdminCustom` for
its internal storage.

### Why?

Fields already have `FieldCustom` which plugins can augment for
type-safe custom config. Collections and globals were still using
`Record<string, any>` directly, so there was no way to get proper types
when building plugins that need collection/global-level configuration.

This made it annoying to build well-typed plugins since you'd lose all
type safety and autocomplete at the collection/global level.

### How?

- Added the four interfaces (same pattern as `FieldCustom` - empty
interfaces extending `Record<string, any>`)
- Updated the type definitions in `collections/config/types.ts` and
`globals/config/types.ts` to use them
- Updated `plugin-import-export` to augment `CollectionAdminCustom` so
it gets proper types for the `disabledFields` array it stores

Fixes #14724
2025-11-25 09:38:36 -08:00
MrLijan 32560e9199 fix: trigger login hooks after reset password (#14711)
### What?
Added execution of `beforeLogin` and `afterLogin` hooks during the
password reset operation in `resetPasswordOperation()`. These hooks now
execute with the same parameters and flow as they do in the standard
login operation, ensuring consistent behavior across both authentication
paths.
- `packages/payload/src/auth/operations/resetPassword.ts` - Added hook
execution before and after JWT token generation.

### Why?
When a user resets their password, the operation performs two phases:
updating the password in the database, then automatically logging the
user in with a new session and JWT token. However, `beforeLogin` and
`afterLogin` hooks were not being triggered during this auto-login
phase, creating inconsistent behavior compared to standard login.
This caused issues for applications that uses `afterLogin` hooks to
attach additional cookie (for example).

### How?
Triggered `beforeLogin` and `afterLogin` in `resetPassword.ts`
2025-11-25 11:29:14 -05:00
Jessica Rynkar 3f3f5dba98 fix: ensure restoreAsDraft only updates the published doc when draft is false (#14658)
### What?
Updates the `restoreVersion` logic to ensure restoring as a draft does
not overwrite the currently published data.

### Why?
If you have a published doc, then save a draft, you expect to be able to
access the published data by `draft: false` and the draft by
`draft:true`. When you restore a version as a draft, the same behavior
is expected.

However, in the `restoreVersion` operation, we are updating the main doc
even if you are restoring as a draft. This overwrites the published
data, so now you will see draft data returned regardless of querying
with `draft: true/false`.

### How?
When restoring a version as a draft, we only need to save a new version.
The main doc should only be updated when restoring as published.

This change checks `(!draftArg)` before running the `updateOne` on the
main doc.

#### Reported by client.
2025-11-21 15:17:14 +00:00
Dan Ribbens c455f581ac fix: remove init of transaction for global doc access (#14693)
When getting the globals doc permissions we were creating a transaction.
This was causing transaction errors to occur where the session was
closed already due to a promise.All() during
[renderDocument](https://github.com/payloadcms/payload/blob/802a21afea066542a08bbd9fbd8f386f58a94e3d/packages/next/src/views/Document/index.tsx#L170)

This change makes it so we no longer create the transaction as is normal
for all other db read operations.
2025-11-20 21:23:46 +00:00
Jarrod Flesch e1168a0f72 fix: count versions should allow querying on localized fields (#14695)
`payload.countVersions` was not passing the locale through, so it was
not possible to query on localized fields without querying on the
localized path. This differed from how the find operation works.
2025-11-20 20:47:08 +00:00
Alessio Gravili 0d9ec910a9 perf: up to 5000% faster permissions calculation (#14631)
In large projects, calculating the `permissions` object is one of the
slowest parts of Payload. This PR completely rewrites the permission
calculation system to make it significantly faster and more reliable.

The permissions object is calculated multiple times throughout Payload's
lifecycle: for the entire config every time we load or navigate to any
admin panel page, on every API endpoint call, and as part of query path
validation when calling any Payload operation.
This is done by `getAccessResults` without any document data, and in
large configs can be one of the slowest operations, noticeably slowing
down admin panel navigation.

After this rewrite, **speed improvements in permissions calculation
without data range from 5.3% (access-control test suite) to 74% (fields
test suite - our largest config)**. The larger the config, the more
noticeable these improvements become. These performance gains apply to
both the Payload Admin Panel and API requests.

---

The `permissions` object can also be calculated WITH document data,
which triggers evaluation of `Where` conditions in
collection/global-level access control and may fetch the document to
pass to access control functions.

This has been heavily optimized with **speed improvements for
permissions calculation with data ranging between 75% and 5138%** (if I
craft a collection config that excessively uses aspects this PR
improves) in the benchmarks. This optimization affects:

- Loading any document (collection/global edit view or drawer)
- Saving a document
- Loading the list view with query presets applied
- Bulk upload initialization (runs twice)

These improvements are most noticeable when navigating _to_ documents in
the admin panel.

## What's Fixed

In addition to performance improvements, the new function is now
cleaner, easier to understand and works more reliably. I specifically
wanna call out two issues that were fixed:

**Field access control data bug**: Added a new e2e test that was
previously failing - when saving a document, the data object passed to
field-level update access control functions had the wrong shape (or in
some cases did not exist at all). This caused fields to show incorrect
`readOnly` states after save. The new implementation properly passes
document data through all access control checks.

**Where query validation**: The old implementation sometimes skipped
validating `Where` queries for collections when `req.data` existed,
potentially granting incorrect access. The new version always validates
where queries correctly when `fetchData: true`.

**Global Where queries:**: Where queries were _never_ executed for
globals. Instead, [we were just checking if the global existed in
general](https://github.com/payloadcms/payload/blob/main/packages/payload/src/utilities/getEntityPolicies.ts#L59).
This PR ensures that `Where` queries returned from global access control
are respected.


## What's Faster (and Why)

**Where query caching:** When multiple operations return identical where
queries (common pattern), we now cache the result. Instead of 3-4
separate DB calls, we make 1 call and reuse it.

**Parallel execution:** All `Where` query evaluations and async access
control functions (for both collections and fields) now execute
concurrently with maximum parallelism, rather than sequentially.

**Optimized DB operations**: When evaluating returned `Where` queries,
we now use direct database `db.count()` queries instead of
`payload.find()` operations. This is much faster (especially on
Postgres).

**Synchronous tree traversal**: The entire field permission tree is now
built synchronously with all async work collected and executed in
parallel at the end, rather than cascading await calls through each
nesting level. This eliminates sequential bottlenecks in deeply nested
field structures.

## Benchmarks

### Admin Panel

- Fields test suite with access control added
- 1000 blocks added to the blocks collection. This is not as excessive
as you would expect:
- most Payload apps do not run on an M3 Max. CPU/DB is often much, much
slower
  - this is all local, with a local DB
- a lot of projects accumulate a huge amount of blocks if you multiply
blocks x block fields. They can definitely reach 1000 total blocks
- each block is simple - only one text field per block. In real
projects, blocks are usually a lot more complex

**Before:**


https://github.com/user-attachments/assets/37b55f02-6dbc-4005-9da6-d7cd4bfdc925

**After:**


https://github.com/user-attachments/assets/8dd796dd-66ed-4d32-a688-27b4169577c6

Branch:
https://github.com/payloadcms/payload/tree/fix/update-field-access-control-after-save-benchmarks

### Modified Access-Control Test Suite

`cd test && pnpm payload run access-control/benchmark-permissions.ts`

```md
📊 Benchmark 1: getAccessResults (all collections + globals)
──────────────────────────────────────────────────────────────────────
┌─────────┬───────────────────┬─────────┬───────────────────┬──────────┐
│ (index) │ Task Name         │ ops/sec │ Average Time (ms) │ Margin   │
├─────────┼───────────────────┼─────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (optimized)' │ '16.91' │ '59203.986'       │ '±0.52%' │
│ 1       │ 'OLD (previous)'  │ '16.07' │ '62323.953'       │ '±0.57%' │
└─────────┴───────────────────┴─────────┴───────────────────┴──────────┘
  ⚡ Speedup: +5.3% 🚀
  📊 DB Calls per operation (across all collections + globals):
     NEW: 0.0 total (0.0 data, 0.0 where)
     OLD: 0.0 total (0.0 data, 0.0 where)

📊 Benchmark 2: docAccessOperation (with fetchData)
──────────────────────────────────────────────────────────────────────
  Collection: where-cache-same (same where queries)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (with cache)' │ '2272.81' │ '442.319'         │ '±0.11%' │
│ 1       │ 'OLD (no cache)'   │ '1019.72' │ '983.347'         │ '±0.12%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +122.9% 🚀
  📊 DB Calls per operation:
     NEW: 2.0 total (1.0 data, 1.0 where)
     OLD: 4.0 total (1.0 data, 3.0 where)

📊 Benchmark 3: docAccessOperation (with data passed)
──────────────────────────────────────────────────────────────────────
  Collection: where-cache-same (same where queries, no DB fetch)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (with cache)' │ '4945.43' │ '203.252'         │ '±0.11%' │
│ 1       │ 'OLD (no cache)'   │ '1023.75' │ '979.898'         │ '±0.14%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +383.1% 🚀
  📊 DB Calls per operation:
     NEW: 1.0 total (0.0 data, 1.0 where)
     OLD: 4.0 total (1.0 data, 3.0 where)

📊 Benchmark 4: docAccessOperation (unique where queries)
──────────────────────────────────────────────────────────────────────
  Collection: where-cache-unique (unique where queries per operation)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (parallel)'   │ '1693.67' │ '599.091'         │ '±0.29%' │
│ 1       │ 'OLD (sequential)' │ '970.37'  │ '1036.268'        │ '±0.20%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +74.5% 🚀
  📊 DB Calls per operation:
     NEW: 4.0 total (1.0 data, 3.0 where)
     OLD: 4.0 total (1.0 data, 3.0 where)

📊 Benchmark 5: Complex Collection (async access, nested blocks, field access)
──────────────────────────────────────────────────────────────────────
  Collection: complex-content (stress test)

┌─────────┬────────────────────┬──────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec  │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼──────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (optimized)'  │ '133.17' │ '7683.346'        │ '±0.83%' │
│ 1       │ 'OLD (sequential)' │ '72.86'  │ '13901.616'       │ '±0.82%' │
└─────────┴────────────────────┴──────────┴───────────────────┴──────────┘
  ⚡ Speedup: +82.8% 🚀
  📊 DB Calls per operation:
     NEW: 2.0 total (1.0 data, 1.0 where)
     OLD: 2.0 total (1.0 data, 1.0 where)

📊 Benchmark 6: Sync-Heavy Collection (same where, many sync field access)
──────────────────────────────────────────────────────────────────────
  Collection: sync-heavy (where cache + field access)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (with cache)' │ '3607.72' │ '279.559'         │ '±0.15%' │
│ 1       │ 'OLD (no cache)'   │ '68.87'   │ '15136.557'       │ '±1.44%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +5138.5% 🚀
  📊 DB Calls per operation:
     NEW: 1.0 total (0.0 data, 1.0 where)
     OLD: 5.0 total (1.0 data, 4.0 where)

══════════════════════════════════════════════════════════════════════
📈 Summary:
══════════════════════════════════════════════════════════════════════
  1. getAccessResults:                        (see above)
  2. docAccessOperation (with fetchData):     (see above)
  3. docAccessOperation (with data passed):   (see above)
  4. docAccessOperation (unique where):       (see above)
  5. Complex collection (async + nested):     (see above)
  6. Sync-heavy (where cache + fields):       (see above)
══════════════════════════════════════════════════════════════════════
```

### Fields Test Suite

`cd test && pnpm payload run fields/benchmark-getAccessResults.ts`

```md
📊 Benchmark: getAccessResults (all fields test collections + globals)
──────────────────────────────────────────────────────────────────────
┌─────────┬───────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name         │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼───────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (optimized)' │ '2104.06' │ '478.382'         │ '±0.18%' │
│ 1       │ 'OLD (previous)'  │ '1208.14' │ '834.974'         │ '±0.21%' │
└─────────┴───────────────────┴───────────┴───────────────────┴──────────┘

  ⚡ Speedup: +74.2% 🚀
  📊 DB Calls per operation (across all collections + globals):
     NEW: 0.0 total (0.0 data, 0.0 where)
     OLD: 0.0 total (0.0 data, 0.0 where)
```
2025-11-20 11:06:12 -08:00
Jessica Rynkar fe8a3e8489 fix: prevent upload mimeType error when useTempFiles is true (#14689)
### What?
Prevents unrelated MIME type error being thrown during upload when
`useTempFiles: true`.

### Why?
In [v3.64.0](https://github.com/payloadcms/payload/releases/tag/v3.64.0)
we released a change to check the mimetype on the file buffer. However,
when `useTempFiles` is true, no buffer is passed and this check fails.

### How?
Checks for `!useTempFiles` before returning an error for no buffer data.

Reported by client.
2025-11-20 17:33:42 +00:00
Sasha efa2fd2b9b feat: allow to specify payload instance cache key in handleEndpoints (#14675)
This PR extends https://github.com/payloadcms/payload/pull/13603 with
allowing you to specify `payloadInstanceCacheKey` inside
`handleEndpoints` and `createPayloadRequest`.
2025-11-20 09:48:01 -05:00
Zvonimir Bušić 8a3c6dc341 fix: autoRefresh not working due to stale closure and missing in client config (#14612)
### What?

Fixes `autoRefresh` functionality that was causing "Stay logged in"
modal to appear instead of automatically refreshing the token.

### Why?

Two issues were preventing `autoRefresh` from working:

1. `autoRefresh` was not exposed in the client config, causing it to be
`undefined` in the browser
2. The Auth provider's reminder timeout callback was capturing a stale
`autoRefresh` value due to closure timing during initial component
render

### How?

- Added `autoRefresh` to client config creation to expose it to the
client
- Used `useEffectEvent` in Auth provider to ensure the reminder timeout
handler always captures the latest `autoRefresh` value

### Fixes

#14613

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
2025-11-20 06:20:17 -08:00
Patrik 9b6e1a3468 fix: type TypeWithVersion missing latest property (#14676)
### What?

Adds the missing `latest?: boolean` property to the `TypeWithVersion<T>`
type definition.

### Why?

The `latest` property is set by database adapters when creating and
updating versions (both collection and global versions), and is included
in the version field schema when drafts are enabled. However, it was
missing from the TypeScript type definition, causing type errors when
consumers try to access this property on version documents.

### How?

Added `latest?: boolean` as an optional property to the
`TypeWithVersion<T>` type. The property is optional because it's only
present when drafts are enabled in the version configuration.

Fixes #14624
2025-11-19 09:13:30 -08:00
Jessica Rynkar fd7c94c027 chore: getLocalizedPaths should filter fields with locale-like names (#14661)
### What?
Ensures `getLocalizedPaths` doesn't falsely detect fields with
locale-like names as a localized field.

### Why?
In the function we are determining if the next segment is a locale based
on whether the segment of the field path matches a locale:
```ts
const nextSegmentIsLocale = localizationConfig && localizationConfig.localeCodes.includes(nextSegment)
```

If you have locale `en` and a nested field with `name: 'en'`, the
function will mistake it for a locale key regardless of whether the
field is localized.

### How?
Uses the existing `fieldShouldBeLocalized` function to check whether the
field is localized in addition to the other checks.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-11-18 15:44:27 -05:00
Jarrod Flesch 87137febd4 fix: corrects outgoing localized data from afterRead (#14603)
Additional incorrect data was being appended when querying with `locale:
"all"` for localized groups and tabs.

#### Before (incorrect)
```ts
g1: {
  en: {
    g2: {
      g2a1: [
        { text: 'EN Deep 1' },
      ]
    }
  },
  g2: { // <-- bad
    g2a1: []
  }
}
```

#### After (correct)
```ts
g1: {
  en: {
    g2: {
      g2a1: [
        { text: 'EN Deep 1' },
      ]
    }
  },
}
```
2025-11-18 11:02:39 -05:00
Alessio Gravili ab03163aac fix(db-*): do not exit process on error during connect (#14647)
This PR removes `process.exit(1)` from our exception handlers within db
connect methods. When a connection error occurred in vercel fluid
compute, it ungracefully shut down the entire compute process.

By re-throwing the error instead, [vercel is able to gracefully handle
the
error](https://vercel.com/changelog/improved-unhandled-node-js-errors-in-fluid-compute)
without shutting down concurrent requests running on the same compute
instance.

process.exit(1) [was initially added by this
commit](https://github.com/payloadcms/payload/commit/f3fee34d16a43a74732a5f5b58bdec63d07aa4a7#r169957812),
however noone was able to recall why it was added.
2025-11-18 10:46:21 +00:00
Jarrod Flesch 9f55254da3 fix: relationships should not fallback if fallbackLocale is false (#14641)
Relationship fields would fallback even if the parent of the request set
`fallbackLocale: false`. This was because of the logic in
sanitizeFallbackLocale which would transform `false` value into `null`,
and when a relationship populate function ran, it would transform the
`null` into the `localization.defaultLocale` value.

Added int test that failed before and passes after to cover.
2025-11-17 16:21:51 -05:00
Alessio Gravili 0cc06a1224 fix(ui): relationship field label not updated when document is updated from other drawer (#14609)
When updating a document from a document drawer that isn’t the one
associated with a relationship field, the relationship’s label doesn’t
update correctly.

## Reproduction

This issue is easily reproducible when two relationship fields link to
the same document. If you update the document from the drawer of one
field, the other field will not reflect the update. This happens because
each field only listens to the `onSave` event of its own document
drawer.

## Fix

Use the `useDocumentEvents` hook instead of the `onSave` hook.
`useDocumentEvents` listens for updates to all saved documents, ensuring
that all relationship fields stay in sync regardless of which drawer the
update originated from.

**Before:**


https://github.com/user-attachments/assets/eda152fe-3cda-4111-b0f3-9f0d5a48f37b


**After:**


https://github.com/user-attachments/assets/da252d3d-c0a9-43ee-91d8-508e302d323e
2025-11-17 13:21:24 +00:00
Jarrod Flesch fb05840a13 fix(ui): duplicate should duplicate as draft by default (#14619)
When duplicating documents it should duplicate as draft when drafts are
enabled. It is odd behavior to duplicate a published document and have
the copy immediately be published.
2025-11-14 14:31:48 -05:00
Alessio Gravili c74a40fb06 fix: tighten up error visibility handling (#14606)
Fixes https://github.com/payloadcms/payload/issues/14377

## Non-public errors were returned from delete and update endpoints

Previously, non-public errors (`500` errors, or errors without
`isPublic: true` and `status`) were returned from the delete and update
endpoint. For security reason, this PR excludes the error messages from
the response.

This aligns bulk endpoints with others (e.g., deleteByID) that already
gate error details through `routeError.ts`.

## Loosen up isPublic check when throwing API error

`APIError` now infers `isPublic` from the status code unless explicitly
provided:
- Non-500 (e.g., 4xx): `isPublic` defaults to `true`.
- 500 or missing status: `isPublic` defaults to `false`.

You can still override by passing a boolean `isPublic` explicitly.
Previously, the only way to mark the error as public was:

`throw new APIError('error message', 401, null ,false)`

which feels unnecessary explicit.

Now, you can do this:

`throw new APIError('error message', 401)`

which previously resulted in `isPublic: false` and now results in
`isPublic: true`.

## Stricter error check in routeError

`routeError` now uses `isErrorPublic(err, config)` to decide whether to
expose the error message. Compared to the old condition (`!config.debug
&& !err.isPublic && status === 500)`, this is a bit stricter and easier
to understand. Errors with `isPublic === false` are now hidden even for
non-500 statuses (previously they were shown).
2025-11-14 11:00:50 +00:00
Elliot DeNolf 2394fe6b6e chore(release): v3.64.0 [skip ci] 2025-11-13 14:22:51 -05:00
Kendell 860bdf2b44 feat(plugin-mcp): adds MCP context to req.payloadAPI (#14595)
- Adds `MCP` option to `req.payloadAPI`

Users can write a conditional to detect when the Payload API context is
'MCP' related.

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

export const Posts: CollectionConfig = {
  slug: 'posts',
  fields: [
    {
      name: 'title',
      type: 'text',
      admin: {
        description: 'The title of the post',
      },
      required: true,
    },

    // ... other fields
  ],
  hooks: {
    beforeRead: [
      ({ doc, req }) => {
        if (req.payloadAPI === 'MCP') {
          doc.title = `${doc.title} (MCP Hook Override)`
        }
        return doc
      },
    ],
  },
}
2025-11-13 14:19:05 -05:00
Paul 8b34e40105 feat: add support for UTC timezone in date fields (#14586)
Adds support for 'UTC' as a value for timezones. This was not previously
supported because we run a check on the values used against the runtime
of Intl API.

```ts
import { buildConfig } from 'payload'

const config = buildConfig({
  // ...
  admin: {
    timezones: {
      supportedTimezones: [
        {
          label: 'UTC',
          value: 'UTC',
        },
        // ...other timezones
      ],
      defaultTimezone: 'UTC',
    },
  },
})
```

Also fixes an issue when having only one timezone making it selected by
default.
2025-11-13 13:20:02 -05:00
Jessica Rynkar d60ea6e8f1 fix: previous value undefined for nested fields in afterChange (#14582)
### What?
Updates the `previousValue` data sent to the `afterChange` hook to
accomodate nested paths.

### Why?
Currently the `afterChange` hook returns this:
```ts
previousValue: previousDoc?.[field.name],
```

For any nested fields, this breaks because it does not match the
structure of the incoming data.

### How?
Uses the path segments to correctly extract the relevant data.

**Reported by client.**
2025-11-13 15:41:41 +00:00
Patrik 8f7ef35274 fix: default values inconsistent between Local and REST APIs (#14556)
### What?

Default values were incorrectly appearing (or not appearing) depending
on access control configuration. When access control denied access to a
global or collection document, default values would sometimes still
populate, and conversely, when access was allowed, defaults would
sometimes be blocked.

### Why?

The previous logic checked `args.data` before querying the database,
making it impossible to distinguish between:
1. A document that doesn't exist and access is allowed (should populate
defaults for globals)
2. A document that was filtered by access control (should NOT populate
defaults)

This caused default values like `_status: 'draft'` to appear when access
control should have returned an empty response, and also blocked
legitimate defaults on uncreated globals without access restrictions.

### How?

- **Query first**: Both globals and collections now query the database
before checking `args.data`
- **Detect access denial**: Only return early when `!docFromDB &&
!args.data && !overrideAccess && accessResult !== true`
- **Correct fallback**: Use `args.data ?? docFromDB ?? {}` (globals) or
`args.data ?? docFromDB` (collections)
- **Simplified**: Removed `shouldSkipDefaults` logic from `promise.ts`
since it's now handled at the operation level

Fixes #14493 
Fixes https://github.com/payloadcms/payload/issues/14476
2025-11-13 07:39:49 -08:00
German Jablonski ad8f0b2c1b feat(plugin-redirects): support translations (#14548)
Allows you to add translations to the plugin fields, replacing the need
for verbose field `overrides` configuration.

### Usage

```ts
export default buildConfig({
  i18n: {
    translations: {
      en: {
        'plugin-redirects': {
          fromUrl: 'Source URL (Custom)', // Override
        },
      },
      de: {
        'plugin-redirects': {
          fromUrl: 'Quell-URL',
          internalLink: 'Interner Link',
          // ... other keys
        },
      },
    },
  },
  plugins: [redirectsPlugin({ collections: ['pages'] })],
})
```
2025-11-13 15:22:51 +00:00
Jarrod Flesch 99ed0e526a fix: findGlobalVersionByID id type (#14534)
Adjusts the `id` type on `findGlobalVersionByID` to be `number |
string`, previously was `string`.
2025-11-13 10:00:18 -05:00
Jessica Rynkar ce479abddc fix: add secondary PDF mimeType validation and tests (#14529)
### What?

Adds a basic secondary PDF validation step that runs **after**
`fileTypeFromBuffer`.

### Why?

`fileTypeFromBuffer` is our final mime type check, and currently it will
incorrectly identify any file that starts with `%PDF-1.x` as a PDF. We
are providing some additional **basic** PDF validation steps.

We have opened an
[issue](https://github.com/sindresorhus/file-type/issues/780) on the
[file-type repo](https://github.com/sindresorhus/file-type) to see if
PDF detection can be improved. Update: they said the package is only for
a hint detection, they won't support full PDF validation.

### How?

Introduces a minimal PDF validation function that inspects the decoded
file buffer for required structural elements:

- Ensures the file begins with a valid `%PDF-1.` header  
- Confirms the presence of the required `EOF` marker and `xref`

### Considerations

These additional steps will help filter out invalid PDFs but can still
be intentionally manipulated.

Adding a separate PDF validation library might be necessary if this is
something we choose to support.

We can revisit it in the future if stronger PDF validation becomes
necessary.

Fixes [14434](https://github.com/payloadcms/payload/issues/14434)
2025-11-13 14:49:22 +00:00
Paul f63e34e686 fix: unlock access not being correctly denied when using a where query (#14585)
Fixes https://github.com/payloadcms/payload/issues/14459

We were previously not throwing an error from the endpoint if validation
failed for unlock and the UI was using permissions from the wrong hook,
I've added a jsdoc to explain the difference between permissions form
useAuth and useDocumentInfo
2025-11-13 09:15:11 -05:00
Kendell 73a18dc088 feat(plugin-mcp): adds custom auth config (#14538)
- Adds `overrideAuth` to the MCP Plugin.
- Adds `MCPAccessSettings` as an exported type from the mcpPlugin

```ts
import { type MCPAccessSettings, mcpPlugin } from '@payloadcms/plugin-mcp'

// ... other config

plugins: [
  mcpPlugin({

    // ... other plugin config

    overrideAuth: (req) => {
      const { payload } = req
    
      payload.logger.info('[Override MCP auth]:')
    
      return {
        posts: {
          find: true,
        },
        products: {
          find: true,
          update: true,
        },
      } as MCPAccessSettings
    },
  })
]
```

### Custom Auth Behaviors

The bypassed system uses an API Key that contains `MCPAccessSettings`
information. The `overrideAuth` function will bypass the API Key system
and use your function when authorizing requests.

This means that your function must return a valid `MCPAccessSettings`
object to use.
2025-11-12 14:21:43 -05:00
Alessio Gravili 072cb97ea1 fix: error when calling jobs.handleSchedules when there are no schedules defined (#14519)
Previously, if you called jobs.handleSchedules() with no schedules
defined, the function would throw an error, as the jobs stats global is
not registered when there are no schedules defined.

Now, instead of erroring, the handleSchedules function will return early
with empty arrays for `errored`, `queued` and `skipped`.
2025-11-12 12:45:27 -06:00
German Jablonski bb520d6e78 fix: changing the language of the admin panel does not work for "radio" and "select" fields. (#14569)
Plugin-defined label functions (e.g., `label: ({ t }) =>
t('plugin-redirects:internalLink')`) don't update when switching
languages.

## Root Cause

The global Payload config was being **mutated**. When
`createClientField` copied the `options` array by reference, it would
evaluate label functions and replace them with strings in the global
config. Subsequent requests would find strings instead of functions and
couldn't re-evaluate.

## Why PR #11725 Wasn't Enough

[PR #11725](https://github.com/payloadcms/payload/pull/11725) fixed the
client config cache to store separate configs per language, but didn't
prevent the source config from being mutated.

## Solution

**File**: `packages/payload/src/fields/config/client.ts`

- Skip copying `options` in the default case
- Create a fresh array in the radio/select handler instead of reusing
the reference
- This prevents mutating the global config while still evaluating labels
correctly per language

## Testing

The testing is located at
https://github.com/payloadcms/payload/pull/14569, which depends on this
PR. You can see that the new tests there pass because they already
contain this commit.

I originally created it into a different PR because CI was failing and I
wasn't sure if it was due to this change. Now I've left them separate
for traceability and clarity. The other pull request is a feature, this
one is a fix.
2025-11-12 16:42:50 +00:00
German Jablonski d7f1ea2ba9 fix: inject custom translations into supportedLanguages in sanitization (#14549)
## Description

Automatically injects custom translations from `i18n.translations` into
`supportedLanguages` during config sanitization. This allows translation
functions like `({ t }) => t('namespace:key')` to work with user-defined
custom translations, not just the default translations from
`@payloadcms/translations`.

## Problem

Currently, when users define custom translations in their config:

```typescript
i18n: {
  translations: {
    de: {
      'my-namespace': {
        customKey: 'Wert'
      }
    }
  }
}
```

These translations are **not** accessible to the `t` function used in
field labels and options. The `t` function only searches in
`config.i18n.supportedLanguages[lang].translations`, causing custom
translations to be ignored.

This issue was discovered while implementing translation support for
plugins in PR #14548, where radio button options using `label: ({ t })
=> t('plugin-redirects:key')` failed to translate for custom languages.

## Solution

During config sanitization (in
`packages/payload/src/config/sanitize.ts`), the code now:

1. Iterates through all languages in `i18n.translations`
2. For existing languages in `supportedLanguages`: merges custom
translations with existing ones
3. For new languages: creates them using English as a template, then
merges custom translations

This ensures that **all custom translations** defined by users or
plugins are automatically available to the `t` function throughout the
application.
2025-11-10 17:04:34 +00:00
Elliot DeNolf 5bf3bd4f8c chore(release): v3.63.0 [skip ci] 2025-11-07 10:06:24 -05:00
Slava Nossar 339a0c390d fix: use TSiblingData for previousSiblingDoc in FieldHook (#14503)
### What?
In field hooks, `previousSiblingDoc` is typed as `TData`.

### Why?
This value should actually be typed as `TSiblingData`, I've confirmed
the sibling data is actually what is returned when a field hook runs.

### How?
Updated `FieldHook` typing, as well as `RichText` field hook typings

Fixes #9735
2025-11-06 20:59:07 +00:00
Jacob Fletcher 3c9f56eb41 chore: standardize query param parsing (#14498)
The way we parse query params within endpoint handlers is currently
non-standard. This leads to subtle differences in how they are handled
across endpoints, e.g. using searchParams vs req.query, allowing values
to fallback to undefined, etc.

For background, each endpoint accepts arbitrary query params and must
transform them into what Payload expects.

For example:
- `draft` provided as a string of "true" is converted to a boolean
- `depth` provided as a string of "0" is converted to a number
- `sort` provided as a comma-separated string is converted to an array
of strings

It is also easy to overlook params that should fallback to `undefined`.
Not doing so will prevent the underlying operation from using its
defaults, if any:

```ts
await update({
  // ...
  trash: trash === 'true' // This is never `undefined` so the default arg will not used (if any)
})
```

Parsing these params is now shared via a single `parseParams` utility,
which takes in raw query parameters and returns them typed and properly
formatted.

```ts
const {
  depth, // number | undefined
  draft // number | undefined
} = parseParams(req.query)
```
2025-11-06 19:02:13 +00:00
Paul df40d0e6fe feat: add support for polymorphic uploads (#14363)
This PR adds support for polymorphic uploads similar to polymorphic
relationships. Closes
https://github.com/payloadcms/payload/discussions/13912

It works with bulk upload as well and in the bulk uploader you can
choose which collection the files are uploaded into.

You can enable this by adding an array of slugs into `relationTo`:

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

export const ExampleCollection: CollectionConfig = {
  slug: 'example-collection',
  fields: [
    {
      name: 'media',
      type: 'upload',
      relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
    },
  ],
}
```

It can also be combined with `hasMany`:

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

export const ExampleCollection: CollectionConfig = {
  slug: 'example-collection',
  fields: [
    {
      name: 'media',
      type: 'upload',
      relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
      hasMany: true, // allows multiple uploads
    },
  ],
}
```


**Known issue**: https://github.com/payloadcms/payload/issues/12014
Filtering by polymorphic relationships is currently broken and I'll work
on that in a separate PR.
2025-11-06 14:40:51 +00:00
Jacob Fletcher f6194c6c91 fix: support generic types for kv.get (#14494)
When getting values from the KV store, it is currently not possible to
type the return value without using assertions, etc. This is because
`payload.kv.get()` does not support generic types, forcing you to use a
workaround like this:

```ts
const value = await payload.kv.get('my-key') as MyValue
// or
const value: MyValue = await payload.kv.get('my-key')
// and so on...
```

Now, you can use generics and the return type will be applied
automatically:

```ts
const value = await payload.kv.get<MyValue>('my-key') // `value` is now typed as `MyValue`
```
2025-11-05 17:39:03 +00:00
Patrik 648021c13b fix: group-by sticky pagination not rendering for virtual relationship fields (#14470)
### What?

Fixes pagination when grouping by virtual relationship fields in
collection list views. The sticky pagination toolbar now correctly
appears when there are 10+ distinct values, and `totalPages` is
calculated accurately.

### Why?

When grouping by virtual relationship fields (e.g.,
`virtualTitleFromCategory` with `virtual: 'category.title'`), the
pagination was broken:
- `totalPages` always returned `1` even when there were 10+ distinct
values
- The sticky pagination toolbar at the bottom never appeared

This happened because:
1. The recursive `findDistinct` call for virtual fields wasn't passing
through `limit` and `page` parameters
2. MongoDB's count aggregation pipeline was missing the relationship
`$lookup` stages needed to traverse virtual field paths

### How?

**1. Fixed recursive findDistinct call**
- Added `limit` and `page` parameters to the recursive call when
handling virtual fields

**2. Fixed MongoDB count aggregation**
- Changed count pipeline to include the same stages as the main query
(`sortAggregation`, `relationLookup`, `$unwind`, `$group`)
- Ensures relationship lookups are performed before counting distinct
values
2025-11-05 08:18:02 -08:00
Tobias Odendahl 39bead7e4c refactor: use formatNames utility for interface name generation (#14481)
## Changes

This PR consolidates the inline interface name generation logic in
`configToJSONSchema.ts` to use the existing `formatNames()` utility
function.

## Motivation

The inline code `singular(toWords(slug, true))` was duplicating logic
already available through `formatNames(slug).singular`. Using the
existing utility function:
- Reduces code duplication
- Maintains consistency with the rest of the codebase
- Makes it clear where the interface naming logic lives

## Testing

Verified that `formatNames(slug).singular` produces identical results to
the previous inline implementation across various slug patterns (plural,
singular, edge cases like 'media', 'series', etc.).

**No behavioral changes** - this generates the exact same interface
names for all collection and global slugs.
2025-11-05 09:58:37 +00:00
Alessio Gravili 61a7095af3 fix: ensure importMap is reliably generated during HMR (fixes Next.js 16 issue) (#14474)
Fixes https://github.com/payloadcms/payload/issues/14419

## Problem

Next.js 16 changes the order of `getPayload()` calls during HMR. In
testing, the frontend now calls `getPayload()` before the admin panel
does. Since the frontend typically has `skipImportMapGeneration: true`
(due to no importMap passed to `getPayload`), this was skipping import
map generation entirely, breaking the admin panel.

## Solution

This PR ensures the import map is always regenerated during HMR by
passing `false` as the second parameter to `reload()`, regardless of the
`options.importMap` value passed to `getPayload()`.

Additionally, a new `ignoreResolveError` option is added to handle cases
where `getPayload()` is called exclusively from the frontend (where no
import map file exists). This prevents errors from interrupting the HMR
process in frontend-only scenarios.
2025-11-05 02:04:18 +00:00
Alessio Gravili 0231a8d1f9 fix: hmr not working in next.js 16 (#14462)
Next.js 16 broke HMR, as the shape of the data sent by the hmr websocket
has changed.

Next.js 15: `{ action: 'serverComponentChanges', hash: '11' }`
Next.js 16: `{ type: 'serverComponentChanges', hash: '16' }`

This PR adds back HMR support by checking for both `action` and `type`.
2025-11-04 03:34:23 +00:00