Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/jobs-queue/jobs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ These functions receive the current `req` and return a boolean. By default, they
Omit `overrideAccess` or pass `overrideAccess: false` to run them from the Local API:

```ts
const req = await createLocalReq({ user }, payload)
const req = await createPayloadRequest({ payload, user })

await payload.jobs.queue({
workflow: 'createPost',
Expand Down
79 changes: 64 additions & 15 deletions docs/migration-guide/v4.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,55 @@ The codemod is idempotent and safe to run on a partially-migrated project.

## Breaking Changes

### Payload request creation is consolidated

Payload request creation now uses `createPayloadRequest` for local contexts and `createPayloadRequestFromWebRequest` to convert a Web `Request`. Admin context setup is exposed as `initAdminContext`.

| Old name | New name |
| --------------------------------------- | ------------------------------------------- |
| `createLocalReq` | `createPayloadRequest` |
| `CreateLocalReqOptions` (internal type) | `Omit<CreatePayloadRequestArgs, 'payload'>` |
| `createPayloadRequest` | `createPayloadRequestFromWebRequest` |
| `initReq` | `initAdminContext` |
| `InitReqArgs` | `InitAdminContextArgs` |
| `InitReqResult` | `AdminContext` |
| `InitReqCache` | `AdminContextCache` |
| `InitReqPartialResult` | `PartialAdminContext` |

`initAdminContext` and its cache types are primarily contracts for framework adapters. Custom Root layout adapters must rename their injected `initReq` callback to `initAdminContext`.

Local request creation now takes a single object argument containing `payload`. For a request without options:

```diff
-import { createLocalReq } from 'payload'
+import { createPayloadRequest } from 'payload'

-const req = await createLocalReq({}, payload)
+const req = await createPayloadRequest({ payload })
```

`CreatePayloadRequestArgs` is exported from `payload` and requires `payload`. Options-only values should use `Omit<CreatePayloadRequestArgs, 'payload'>`. The old `CreateLocalReqOptions` type was declared in the internal request utility, not exported from the package root; relative/private imports of that type require manual migration. The codemod handles an existing named import of that type only when its source is `payload`.

Web request conversion uses its new role-specific name:

```diff
-import { createPayloadRequest } from 'payload'
+import { createPayloadRequestFromWebRequest } from 'payload'

-const req = await createPayloadRequest({ config, request })
+const req = await createPayloadRequestFromWebRequest({ config, request })
```

#### Codemod

To migrate automatically, there's a codemod for this change available by running:

```bash
npx @payloadcms/codemod --transform migrate-payload-request-creation
```

Automatic call migration requires a plain identifier payload argument and either `{}` or an initialized local `const` options identifier. The options type must expose all seven old option properties, or be `any`. For that identifier form, the generated argument captures payload first and uses forwarding getters for `context`, `depth`, `fallbackLocale`, `locale`, `req`, `urlSuffix`, and `user`. This preserves inherited getters, their read order, and rejected promises from throwing getters without reading unrelated properties. Complex expressions, mutable/unresolved options bindings, narrow option types, collisions, and unsupported type usages are left unchanged with notes for manual migration.

### JWT authentication is versioned without moving token data

Payload 4 keeps the decoded JWT payload flat, so integrations can continue reading `id`, `collection`, `sid`, `email`, and fields selected by `saveToJWT` from the same paths as Payload 3.
Expand Down Expand Up @@ -396,20 +445,20 @@ Several types and utilities that were re-exported from `@payloadcms/ui` and `@pa

**Pass-through re-exports** — same name, new source:

| Symbol | Old source | New source |
| ----------------------------- | ---------------------------- | ---------------- |
| `Column` | `@payloadcms/ui` | `payload` |
| `ListViewSlots` | `@payloadcms/ui` | `payload` |
| `ListViewClientProps` | `@payloadcms/ui` | `payload` |
| `EntityType` | `@payloadcms/ui/shared` | `payload/shared` |
| `formatAdminURL` | `@payloadcms/ui/shared` | `payload/shared` |
| `mergeListSearchAndWhere` | `@payloadcms/ui/shared` | `payload/shared` |
| `mergeHeaders` | `@payloadcms/next/utilities` | `payload` |
| `headersWithCors` | `@payloadcms/next/utilities` | `payload` |
| `createPayloadRequest` | `@payloadcms/next/utilities` | `payload` |
| `addDataAndFileToRequest` | `@payloadcms/next/utilities` | `payload` |
| `sanitizeLocales` | `@payloadcms/next/utilities` | `payload` |
| `addLocalesToRequestFromData` | `@payloadcms/next/utilities` | `payload` |
| Symbol | Old source | New source |
| ----------------------------- | ---------------------------- | ------------------------------------------------- |
| `Column` | `@payloadcms/ui` | `payload` |
| `ListViewSlots` | `@payloadcms/ui` | `payload` |
| `ListViewClientProps` | `@payloadcms/ui` | `payload` |
| `EntityType` | `@payloadcms/ui/shared` | `payload/shared` |
| `formatAdminURL` | `@payloadcms/ui/shared` | `payload/shared` |
| `mergeListSearchAndWhere` | `@payloadcms/ui/shared` | `payload/shared` |
| `mergeHeaders` | `@payloadcms/next/utilities` | `payload` |
| `headersWithCors` | `@payloadcms/next/utilities` | `payload` |
| `createPayloadRequest` | `@payloadcms/next/utilities` | `createPayloadRequestFromWebRequest` in `payload` |
| `addDataAndFileToRequest` | `@payloadcms/next/utilities` | `payload` |
| `sanitizeLocales` | `@payloadcms/next/utilities` | `payload` |
| `addLocalesToRequestFromData` | `@payloadcms/next/utilities` | `payload` |

**Renamed types** — use the new canonical name from `payload`:

Expand All @@ -428,7 +477,7 @@ Several types and utilities that were re-exported from `@payloadcms/ui` and `@pa
+ import { headersWithCors, mergeHeaders } from 'payload'
```

Run `npx @payloadcms/codemod --transform migrate-aliased-exports` to migrate automatically. Renamed types are imported using an `as` alias (e.g. `import type { CollectionPreferences as ListPreferences } from 'payload'`) so existing usages keep compiling — drop the alias and rename usages manually if you want to fully commit to the new name.
Running `npx @payloadcms/codemod --transform migrate-aliased-exports` preserves old local bindings by importing renamed symbols with an `as` alias (e.g. `import type { CollectionPreferences as ListPreferences } from 'payload'` or `import { createPayloadRequestFromWebRequest as createPayloadRequest } from 'payload'`). Run `migrate-payload-request-creation` to complete the request helper's identifier and call-site migration, or drop the other aliases and rename their usages manually if you want to fully commit to the new names.

### `DefaultValue` function variant is now typed to allow returning a `Promise` ([#17522](https://github.com/payloadcms/payload/pull/17522))

Expand Down
1 change: 1 addition & 0 deletions packages/codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ The tool loads your project via [ts-morph](https://ts-morph.com/), using your `t
- `migrate-block-references-to-blocks`: renames blocks field `blockReferences` configs to `blocks`, and removes a sibling `blocks: []` placeholder when present. If a non-empty `blocks` sibling already exists, the transform leaves the field untouched and surfaces a note for manual review.
- `migrate-hide-api-url`: migrates `admin.hideAPIURL: true` to `admin.components.views.edit.api.tab.condition: () => false` on collection and global configs.
- `migrate-aliased-exports`: rewrites imports of types and utilities that used to be re-exported from `@payloadcms/ui` and `@payloadcms/next/utilities` to their canonical sources in `payload` / `payload/shared`.
- `migrate-payload-request-creation`: renames named request-helper and admin-context imports from `payload` and `payload/internal`. Calls migrate only with a plain identifier payload and either `{}` or an initialized local `const` options identifier whose type exposes all seven old option properties (or is `any`). The generated argument captures payload first and forwards only `context`, `depth`, `fallbackLocale`, `locale`, `req`, `urlSuffix`, and `user` through getters, preserving inherited getters, read order, and async rejection behavior. Existing `CreateLocalReqOptions` named imports from `payload` become options-only usages of `Omit<CreatePayloadRequestArgs, 'payload'>`; relative/private imports of that internal type require manual migration. Unsupported shapes, type usages, and collisions leave the affected binding unchanged with notes.
- `migrate-document-title-context`: migrates `title` and `setDocumentTitle` destructured from `useDocumentInfo()` to `useDocumentTitle()`. They were removed from `DocumentInfoContext` in v4 and now live on `DocumentTitleContext`.
- `migrate-storage-adapters-to-config`: moves storage adapter factory calls (`s3Storage`, `gcsStorage`, `azureStorage`, `r2Storage`, `vercelBlobStorage`) from `plugins` to the new top-level `storage` array. Removes `plugins` if it becomes empty after the move. **Limitations:** aliased imports (e.g. `import { s3Storage as myS3 }`) are not detected; rename any aliases to the canonical factory name before running, or migrate those calls manually. The transform preserves AST structure but does not re-format output. Run `prettier --write` (or your project's formatter) after applying.
- `rename-storage-adapters-to-storage`: renames the top-level `storageAdapters` config property to `storage`. Skips any object that already has a `storage` property. Run this if you previously ran `migrate-storage-adapters-to-config` and need to update the property name.
Expand Down
2 changes: 2 additions & 0 deletions packages/codemod/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { migrateImportExportHooks } from './transforms/migrate-import-export-hoo
import { migrateListViewSelectAPI } from './transforms/migrate-list-view-select-api/index.js'
import { migrateNextGenerateViewportExport } from './transforms/migrate-next-generate-viewport-export/index.js'
import { migrateNextSubpathExports } from './transforms/migrate-next-subpath-exports/index.js'
import { migratePayloadRequestCreation } from './transforms/migrate-payload-request-creation/index.js'
import { migrateSlugField } from './transforms/migrate-slug-field/index.js'
import { migrateStorageAdaptersToConfig } from './transforms/migrate-storage-adapters-to-config/index.js'
import { migrateVersionsDefault } from './transforms/migrate-versions-default/index.js'
Expand All @@ -40,6 +41,7 @@ export const transforms: Transform[] = [
migrateDisabledFields,
migrateForceSelect,
migrateAliasedExports,
migratePayloadRequestCreation,
migrateBlockReferencesToBlocks,
migrateBuildScript,
migrateDocumentTitleContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ const here = dirname(fileURLToPath(import.meta.url))
const fixture = (name: string) => readFile(join(here, name), 'utf8')

describe('migrate-aliased-exports', () => {
it('should migrate legacy request helpers directly to the canonical export while preserving local bindings', async () => {
const source = await fixture('request.input.ts')
const output = await fixture('request.output.ts')

expect(await runTransform({ source, transform: migrateAliasedExports })).toBe(output)
expect(await runTransform({ source: output, transform: migrateAliasedExports })).toBe(output)
})

it('moves pass-through re-exports to canonical sources', async () => {
const input = await fixture('basic.input.ts')
const output = await fixture('basic.output.ts')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const SOURCE_MAP: Record<string, Record<string, Migration>> = {
'@payloadcms/next/utilities': {
addDataAndFileToRequest: { to: 'payload' },
addLocalesToRequestFromData: { to: 'payload' },
createPayloadRequest: { to: 'payload' },
createPayloadRequest: { rename: 'createPayloadRequestFromWebRequest', to: 'payload' },
headersWithCors: { to: 'payload' },
mergeHeaders: { to: 'payload' },
sanitizeLocales: { to: 'payload' },
Expand Down Expand Up @@ -85,6 +85,7 @@ export const migrateAliasedExports: Transform = {
const aliasNode = spec.getAliasNode()
const localName = aliasNode?.getText() ?? spec.getName()
const canonicalName = migration.rename ?? spec.getName()
const originalName = spec.getName()
const alias = localName !== canonicalName ? localName : undefined

spec.remove()
Expand All @@ -97,9 +98,7 @@ export const migrateAliasedExports: Transform = {
})

if (migration.rename) {
notes.push(
renameNote({ filePath: file.getFilePath(), migration, originalName: spec.getName() }),
)
notes.push(renameNote({ filePath: file.getFilePath(), migration, originalName }))
}
}

Expand All @@ -126,7 +125,7 @@ export const migrateAliasedExports: Transform = {
}
},
description:
'Move imports of aliased re-exports from @payloadcms/ui and @payloadcms/next/utilities to their canonical sources in `payload` / `payload/shared`. Renamed names (`ListPreferences` → `CollectionPreferences`, `ListComponentClientProps` → `ListViewClientProps`, `ListComponentServerProps` → `ListViewServerProps`) are imported using an `as` alias so existing usages keep compiling.',
'Move imports of aliased re-exports from @payloadcms/ui and @payloadcms/next/utilities to their canonical sources in `payload` / `payload/shared`. Renamed names (`createPayloadRequest` → `createPayloadRequestFromWebRequest`, `ListPreferences` → `CollectionPreferences`, `ListComponentClientProps` → `ListViewClientProps`, `ListComponentServerProps` → `ListViewServerProps`) are imported using an `as` alias so existing usages keep compiling.',
}

type ApplyRenameArgs = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createPayloadRequest } from '@payloadcms/next/utilities'
import { createPayloadRequest as webRequest, unrelated } from '@payloadcms/next/utilities'

const req = createPayloadRequest(args)
const aliased = webRequest(args)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createPayloadRequestFromWebRequest as createPayloadRequest, createPayloadRequestFromWebRequest as webRequest } from 'payload'
import { unrelated } from '@payloadcms/next/utilities'

const req = createPayloadRequest(args)
const aliased = webRequest(args)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CreateLocalReqOptions as Options, InitReqResult as Result } from 'payload'

import { createLocalReq as local, createPayloadRequest as web } from 'payload'

const options: Options = { user }
const req = local(options, payload)
const request = web(input, config)
type Context = Result
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CreatePayloadRequestArgs as Options, AdminContext as Result } from 'payload'

import { createPayloadRequest as local, createPayloadRequestFromWebRequest as web } from 'payload'

const options: Omit<Options, 'payload'> = { user }
const req = local({ payload: payload, get context() { return options.context }, get depth() { return options.depth }, get fallbackLocale() { return options.fallbackLocale }, get locale() { return options.locale }, get req() { return options.req }, get urlSuffix() { return options.urlSuffix }, get user() { return options.user } })
const request = web(input, config)
type Context = Result
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createLocalReq } from 'payload'
import { createPayloadRequest } from 'payload'
import { initReq } from 'payload/internal'

const createPayloadRequestFromWebRequest = other
const req = createLocalReq({}, payload)
function read(initAdminContext: Function) {
return initReq(args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createLocalReq } from 'payload'

const options: any = { user }
const req = createLocalReq(
/* options */ options /* after options */,
// payload instance
payload /* after payload */,
)
const empty = createLocalReq({ /* empty options */ }, payload)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createPayloadRequest } from 'payload'

const options: any = { user }
const req = createPayloadRequest({
/* options */ /* after options */
// payload instance
payload: payload /* after payload */, get context() { return options.context }, get depth() { return options.depth }, get fallbackLocale() { return options.fallbackLocale }, get locale() { return options.locale }, get req() { return options.req }, get urlSuffix() { return options.urlSuffix }, get user() { return options.user },
})
const empty = createPayloadRequest({ /* empty options */ payload: payload })
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createLocalReq, createPayloadRequest, type InitReqResult } from 'payload'

const req = await createLocalReq({}, payload)
const web = createPayloadRequest(request, config)
type Context = InitReqResult
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createPayloadRequest, createPayloadRequestFromWebRequest, type AdminContext } from 'payload'

const req = await createPayloadRequest({ payload: payload })
const web = createPayloadRequestFromWebRequest(request, config)
type Context = AdminContext
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createLocalReq } from 'payload'

const options = { user: 'old' }
const newUser = 'new'
const payload = {}
const safe = createLocalReq({}, payload)
globalThis.observed = createLocalReq(options, (options.user = newUser, payload)).user
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createLocalReq } from 'payload'

const req = await createLocalReq(getOptions(), payload)
const conditional = createLocalReq(enabled ? first : second, payload)
const nested = createLocalReq({ req: await createLocalReq({}, payload) }, payload)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createLocalReq } from 'payload'

const req = await createLocalReq(getOptions(), payload)
const conditional = createLocalReq(enabled ? first : second, payload)
const nested = createLocalReq({ req: await createLocalReq({}, payload) }, payload)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createLocalReq } from 'payload'

const events = []
let payload = { id: 'original' }
const inherited = {
get context() { events.push('context'); payload = { id: 'replacement' }; return 'context' },
get depth() { events.push('depth'); return 1 },
get fallbackLocale() { events.push('fallbackLocale'); return 'en' },
get locale() { events.push('locale'); return 'fr' },
get req() { events.push('req'); return {} },
get urlSuffix() { events.push('urlSuffix'); return '/path' },
get user() { events.push('user'); return 'inherited' },
}
const options = Object.create(inherited, {
unrelated: { enumerable: true, get() { throw new Error('unrelated getter must not run') } },
})
globalThis.observed = { request: createLocalReq(options, payload), events }
Loading
Loading