Commit Graph

1997 Commits

Author SHA1 Message Date
Alessio Gravili 070ded796f refactor: deprecate returning failed state from job queue handlers (#15102)
This PR deprecates returning `{ state: 'failed' }`, which unnecessarily
inflates the API.

**Reasons:**
- It duplicates existing behavior, making the API and docs more
confusing.
- It creates ambiguity about which approach developers should use.
- It increases maintenance cost.
- Throwing errors is more flexible: for example, cancelling a job from a
task handler currently requires throwing `JobCancelledError`, which
cannot be done via a return value.


# Migration Example

## Before

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

export const ReturnCustomErrorTask: TaskConfig<'ReturnCustomError'> = {
  retries: 0,
  slug: 'ReturnCustomError',
  inputSchema: [],
  outputSchema: [],
  handler: ({ input }) => {
    return {
      state: 'failed',
      errorMessage: 'oh no! this failed'
    }
  },
}
```

## After

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

export const ReturnCustomErrorTask: TaskConfig<'ReturnCustomError'> = {
  retries: 0,
  slug: 'ReturnCustomError',
  inputSchema: [],
  outputSchema: [],
  handler: ({ input }) => {
    throw new Error('oh no! this failed')
  },
}
```
2026-01-06 15:43:30 -08:00
Thomas Coldwell 58faafd718 feat: add typescript.strictDraftTypes flag for opt-in draft query type safety (#14388)
### What?

This PR adds an opt-in `typescript.strictDraftTypes` config flag that
enables strict type safety for draft mode queries. When enabled, `find`
operations with `draft: true` will correctly type required fields as
optional.

```typescript
export default buildConfig({
  typescript: {
    strictDraftTypes: true,  // <-- defaults to false
  },
  // ...
})
```

### Why?

Support for correct types when creating a draft document was added in
#14271, however, the types returned when querying a document (e.g. via
Local API `find`) with `draft: true` still return incorrect types—many
required fields are possibly null but typed as non-null.

Making this change without a flag would be a breaking change for
existing user code, as any queries using a dynamic `draft` parameter
would start showing required fields as optional, requiring null checks
throughout their codebase. By introducing this behind a flag, users can
opt-in when ready, and it will become the default behavior in v4.0.

### How?

- Added `strictDraftTypes` config flag to `typescript` config object
(defaults to `false`)
- Added a generic `TDraft extends boolean` parameter to `find()` and
`findLocal()`
- Added conditional return types that check both `TDraft` and
`GeneratedTypes extends { strictDraftTypes: true }`
- Created `QueryDraftDataFromCollection` type for draft query results
where:
  - `id` is required (always present in query results)
- User-defined required fields are optional (validation skipped for
drafts)
  - System fields like `createdAt`, `updatedAt` are optional
- Updated `DraftTransformCollectionWithSelect` to use the new type for
draft queries
- Modified type generation to add `strictDraftTypes: true` to
`GeneratedTypes` when enabled

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
2026-01-06 10:13:36 -08:00
Anders Semb Hermansen 2ccf898d64 feat: support qs-esm sort arrays in REST API (#15065)
### What?

Support qs-esm array in sort parameter to sort by multiple columns in
REST API

### Why?

To align input of Local API and REST API so that REST API also accepts
array like Local API do.

### How?

Extract parsing/sanitizing of sort input to it's own method which also
accept array.
Added unit and integration test. Updated docs with example.

Fixes #15052
2025-12-30 21:46:47 +00:00
Riley Langbein 4468197305 fix: add beforeDocumentControls to globals generate importmap (#15036)
### What?

When adding `beforeDocumentControls` as a custom component to a global,
it does not get picked up by the import map, even when running
`generate:importmap`.

### Why?

`beforeDocumentControls` is missing from `addToImportMap` in
`iterateGlobals`.

### How?

Adds `beforeDocumentControls` to `addToImportMap` in `iterateGlobals`.

While doing this I've used the opportunity to add `Description` to the
globals custom component documentation as it was missing.

Fixes #15035

Co-authored-by: Riley Langbein <rilrom2@hotmail.com>
2025-12-29 23:35:15 +00:00
Alessio Gravili cd87ab4517 fix: warning during Next.js build "the request of a dependency is an expression" (#15007)
Fixes https://github.com/payloadcms/payload/issues/15006

https://github.com/payloadcms/payload/pull/14337 introduced a next build
warning, because the `eval` around `await import` statements has been
removed.

Unlike jest, vitest is fine with it. But during Next.js build, the
bundler will throw a `"Critical dependency: the request of a dependency
is an expression"` warning.

This PR fixes this warning by adding back the eval statement around the
import. It does not add back the conditional require statement - that
part was only necessary to appease jest.

For vitest, we have to directly call `await import()`. Using eval in
vitest will throw an `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING` error.

## Preventing this regression in the future

This PR adds a comment above the eval explaining why it's needed.
Previously this was missing, and we only had a comment explaining why
the `require` was needed.

Additionally, the reason this was not caught in CI was because warnings
during next build did not fail the build script. This PR adds an
additional check in `build-template-with-local-pkgs.ts` that throws an
error if a warning was detected during next build. You can find the CI
failure here:
https://github.com/payloadcms/payload/actions/runs/20402288354/job/58626594247?pr=15007
(CI run from commit that added this check, before the commit that fixed
it)

Now, if this ever happens again, our CI will fail and prevent a
regression.

## More Tests

This PR adds additional database and job queue tests that verify that
the job queue handler and migration file imports work correctly.
2025-12-23 11:52:54 -05:00
Sasha 4e45432592 test: migrate to vitest (#14337)
Continuation of https://github.com/payloadcms/payload/pull/10200

* For VSCode - you'll need to install the official plugin
https://marketplace.visualstudio.com/items?itemName=vitest.explorer
* Every test suite now explicitly imports the test runner functions like
`describe`, `expect`, instead of using it from `global`. While `vitest`
supports globals with https://vitest.dev/config/#globals, the main
benefit of not doing this is that we don't conflict with test runners
that have the same functions, e.g `playwright` and `tstyche`.
* Now we have only 1 test config file that uses
[projects](https://vitest.dev/guide/projects) for unit and integration
tests

---------

Co-authored-by: Alessio Gravili <github@gravili.net>
2025-12-19 16:07:14 -05:00
Jarrod Flesch fa6b503896 fix: basePath not working properly with admin routes (#14967)
This PR adds a base-path test suite, updates the formatAdminURL helper
function.

The helper function allows you to create routes inside the admin panel
that take `basePath` into account when set.

There are a lot of touched files here, most have the same treatment.

## API URL construction changes
Previously we were appending the basePath in the default api
configuration. That is no longer the case and is more predictable to use
the helper function that will generate a correct path/url.

## Admin URL construction changes
The default admin route differed from the default api route, the default
admin route did not prepend the basePath. The reason it did not/should
not, is because `usePathname` from NextJS excludes basePath.

So if we were to prepend it to `routes.admin` and then for example use
`pathname.startsWith(config.routes.admin)` it would always return false
when using a basePath, since `/route` will never start with
`/basePath/route`.

Also, when you do something like `router.push(/adminRoute)` NextJS will
prepend the basePath and push you to `/basePath/adminRoute`. If we
prepended it in the config it would push you to
`/basePath/basePath/adminRoute`.


## Helper function usage

The helper is especially useful for plugin work, this way your plugin
will generate correct urls for users using basePath.

Before we were doing:
```ts
// worked because we prepended basePath to apiRoute
const apiURL = `${serverURL}${apiRoute}/collections/${slug}/${id}`

// never worked since basePath was never prepended to adminRoute
const adminURL = `${serverURL}${adminRoute}/collections/${slug}/${id}`
```

Now we can do:
```ts
import { formatAdminURL } from 'payload/shared'

// Admin url example
const adminURL = formatAdminURL({
  adminRoute: config.routes.admin,
  path: `/collections/${slug}/${id}`,
})

// API url example
const apiURL = formatAdminURL({
  apiRoute: config.routes.api,
  path: `/collections/${slug}/${id}`,
  // serverURL, when supplied returns a full url
})
```

If you pass `serverURL` the result will be a full URL. If excluded the
result will be a relative path.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 11:13:21 -05:00
German Jablonski f1116247ed feat: modular dashboards - widgets (#13683)
[RFC Here](https://github.com/payloadcms/payload/discussions/11862).

You can test the feature by running `pnpm dev dashboard` on this branch.

<details>
<summary>Old (obsolete) example</summary>

See the [comment
below](https://github.com/payloadcms/payload/pull/13683#issuecomment-3581457901)
explaining the change in approach we took


https://github.com/user-attachments/assets/96157f83-c5d7-4350-9f31-c014daedb2a8

</details>

### New demo


https://github.com/user-attachments/assets/6c08d8d6-c989-4845-b56f-6d3fbd30b1af

## Future Work 

The following improvements are planned but will be added in the future: 

- fields: You'll be able to define `fields` that a widget receives,
which will serve as props in the component. Why might this be useful?
Imagine a chart widget that can have a weekly, daily, or yearly view. Or
a "count" widget that shows how many documents there are in a collection
(the collection could be a field).
- A11y (EDITED): Okay, I finally went the extra mile here, and you can
reorder and resize it with the keyboard. The screen reader works,
although there's room for improvement.
- Dashboard presets: we're planning to add the ability to create and
share dashboard presets, similar to how [query
presets](https://payloadcms.com/docs/query-presets/overview) work today.
For example, you could build dashboards that adjust based on a variable,
such as a "daily, weekly, or monthly" interval. You could also create
dashboards tailored to different focus areas, like "marketing, sales, or
product."
2025-12-19 15:22:03 +00:00
Jake 519a3c6015 fix: get field by path for blocks (#14984)
It's currently not possible to use `getFieldByPath` to find a field
nested within a block.

For example:

```ts
// This return undefined
const fieldInBlock = getFieldByPath({
  path: 'blocks.block2.text2' ,
  fields: [
    {
      type: 'blocks',
      name: 'blocks',
      blocks: [
        {
          slug: 'block1',
          fields: [
            {
              name: 'text1',
              type: 'text',
            },
          ],
        },
        {
          slug: 'block2',
          fields: [
            {
              name: 'text2',
              type: 'text',
            },
          ],
        },
      ],
    }
  ]
})
```

This is because the function is ignoring block slugs and looping over
each block's fields until one is found. Except they are never found
because the path is not trimmed for the next iteration. If the same
field exists across different blocks, I would also imagine the wrong
schema could get matched.
2025-12-19 09:26:40 -05:00
Alessio Gravili 446cbb14ab refactor: replace deprecated url.parse api usage (#14980)
Replaces `URL.parse()` usage which is now deprecated with `new URL()`.
Fixes https://github.com/payloadcms/payload/issues/14978
2025-12-18 21:15:15 +00:00
Jessica Rynkar 61298c6b59 fix: improves upload security for PDFs and SVGs (#14929)
#### What?
Improves upload security by closing validation gaps for `PDF` and `SVG`
files, preventing malicious files from bypassing MIME type restrictions.

#### Why?

- **PDFs**: Corrupted PDFs with a manipulated content type could bypass
detection when no `fileTypeFromBuffer` was undefined, allowing
unauthorized file types pass.

- **SVGs**: SVGs can contain malicious scripts that execute when
rendered, currently we do not check the SVG for anything potentially
harmful

#### How?
- Added extension validation: If the PDF returns no buffer, we have an
additional check on the `ext` which closes the validation gap
- Added SVG sanitization: New `validateSvg` utility checks for attack
elements including scripts, event handlers, foreign objects, iframes etc
2025-12-18 12:58:23 -05:00
Jarrod Flesch ec7c1920a8 fix: missing range headers (#14887)
## Summary

Adds HTTP Range request support (RFC 7233) to Payload's core file
serving, enabling video streaming with scrubbing/seeking capabilities in
browsers.

## Changes

- **Added `Accept-Ranges: bytes` header** to all file responses
- **Created `parseRangeHeader` utility** for parsing Range headers using
`range-parser` package
- **Enhanced `streamFile` function** to support byte range streaming via
`fs.createReadStream` options
- **Added integration tests** covering various range request scenarios

## Technical Details

- Uses `range-parser` library for RFC 7233 compliant parsing
  - Supports single byte ranges (e.g., `bytes=0-1023`)
  - Handles open-ended ranges (e.g., `bytes=1024-`)
  - Handles suffix ranges (e.g., `bytes=-512`)
- Multi-range requests return first range only (standard simplification)

## Testing

Added comprehensive integration test suite covering:
- Full file requests with Accept-Ranges header
- Partial content requests (206 responses)
- Invalid range handling (416 responses)
- Response body size verification
- Edge cases (out-of-bounds, malformed headers)


### Before


https://github.com/user-attachments/assets/065060ae-35db-4c3d-bc72-cfb976b57349

### After


https://github.com/user-attachments/assets/b70caa49-e055-47b2-87f8-31f02a42c86a
2025-12-16 08:27:17 -05:00
Jake 4f12bee5ef refactor: add relative flag to formatAdminURL util (#14925)
Adds a `relative` flag to the `formatAdminURL` util.

There are cases where this function can produce a fully qualified URL,
like for client-side routing, and other times when it must remain
relative, like when matching routes.

This flag differentiates these two behaviors declaratively so we don't
have to rely on the omission of `serverURL`.

```ts
const result = formatAdminURL({
  adminRoute: '/admin',
  basePath: '/v1',
  path: '/collections/posts',
  serverURL: 'http://payloadcms.com',
  relative: true,
})

// returns '/v1/admin/collections/posts'
```

Related: https://github.com/payloadcms/payload/pull/14919 and
https://github.com/payloadcms/payload/pull/14907
2025-12-15 15:02:29 -05:00
Jessica Rynkar bb1501ea14 fix: previousValue from hooks should be populated within lexical blocks (#14856)
### What?
Updates the `previousValue` data sent to the `afterChange` hook to
accomodate **lexical nested** paths.

### Why?
Currently the `afterChange` hook uses the previousDoc:
```ts
previousValue: getNestedValue(previousDoc, pathSegments) ?? previousDoc?.[field.name],
```

However, with fields from within Lexical, the data on the full
`previousDoc` has a complex nesting structure. In this case, we should
be using the `previousSiblingDoc` to only return the sibling data and
easily de-structure the data.

### How?
Checks if sibling data exists, and uses it if it does:
```ts
 const previousValData =
    previousSiblingDoc && Object.keys(previousSiblingDoc).length > 0
      ? previousSiblingDoc
      : previousDoc
```

**Reported by client.**

Related PR: https://github.com/payloadcms/payload/pull/14582

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
2025-12-12 10:58:54 +00:00
Jarrod Flesch b82356b9bf fix: passes serverURL through to all formatAdminURL calls (#14869)
Fixes issues with basePath and NextJS. Many of the links created in the
admin panel were not passing a `serverURL` through to the
`formatAdminURL` helper.

This PR adds all the missing `serverURL` instances and adds a new helper
`formatApiURL` that allows for similar route creation but for api
routes.
2025-12-10 14:42:07 -05:00
Alessio Gravili c9a8aa0344 refactor: replace generic Error('Unauthorized') with UnauthorizedError (#14879)
Refactors all auth checks to throw `UnauthorizedError` instead of the
generic `Error('Unauthorized')`. This ensures consistent unauthorized
errors being thrown
2025-12-10 11:07:42 -06:00
Jarrod Flesch 0da0da8693 fix: adds read and readDistinct to hook mapping (#14853)
Fixes https://github.com/payloadcms/payload/issues/14847
2025-12-08 16:46:12 +00:00
Patrik 3a29cec318 fix: undefined drafts error in collections with join fields (#14855)
Fixes #14846
2025-12-08 08:22:21 -08:00
Rot4tion 9c9c7c525b fix: use hasDraftsEnabled utility instead of direct check (#14843)
Replaces direct checks of `collectionConfig.versions?.drafts` with the
`hasDraftsEnabled` utility function for consistency across document
permissions, update operations, and duplicate document functionality.
2025-12-08 10:33:19 -05:00
Elliot DeNolf ccfaee0b1b fix(storage-*): upload filename respects prefix (#14566)
## Overview

Fixes filename collision detection to respect storage adapter
prefix/suffix patterns. When using cloud storage with prefixes (e.g.,
multi-tenant scenarios), files with identical names under different
prefixes were incorrectly
  triggering deduplication.

Resolves #14561 

  ## Changes

  ### Core Upload Logic
- **`docWithFilenameExists`**: Now accepts and checks `prefix` parameter
in WHERE clause, scoping collision detection to prefix namespace
- **`getSafeFileName`**: Accepts `prefix` parameter and passes to
collision check
- **`generateFileData`**: Extracts `prefix` from data (set by storage
plugins) and threads through to collision detection

  ### Test Coverage
- **New collection `MediaWithDynamicPrefix`**: Tests multi-tenant
scenario with hook-based prefix assignment
- **Updated `MediaWithPrefix`**: Added `prefix` field and
`filenameCompoundIndex: true`
- **Comprehensive test suite**: Validates collision within prefix, no
collision across prefixes, backward compatibility for collections
without prefix, and dynamic prefix from hooks
2025-12-08 09:57:27 -05:00
Patrik d86c17455d fix: file objects being serialized to plain objects in upload form state (#14818)
### What?

Fixes a bug where saving an upload document would fail with `"[object
Object]" is not valid JSON` error after changing the file and then
modifying another field.

### Why?

When a file was changed in an upload document and saved, the File object
would persist in the client-side form state.

On the next save (when modifying a different field), the form state
would be deep copied and serialized before submission. This
serialization converted the File object into a plain object `{name,
size, type, ...}`, which was then stringified to `"[object Object]"` and
sent to the server, causing a JSON parsing error.

### How?

1. **Preserve File objects during serialization**
- Added instanceof File check in
`deepCopyObjectSimpleWithoutReactComponents` to return File objects
as-is instead of serializing them to plain objects
   - Similar to how Date objects are already handled

2. **Clear file field after successful save**
- Delete the `file` field from form state after save in upload
collections
   - Prevents stale File objects from persisting between saves

I could of just added an instanceof File check in
[createFormData](https://github.com/payloadcms/payload/blob/1340818097c90904ff9e0facb5b30329bbeb430e/packages/ui/src/forms/Form/index.tsx#L528)
but it would be just a bandaid fix. If a non-File object reaches that
code, it indicates a bug elsewhere that should fail loudly. The root
causes were:
- File objects being incorrectly serialized during form submission
- File objects persisting in form state after they should be cleared

Fixes #14814
2025-12-08 12:24:47 +00: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
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
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
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