Compare commits

..

45 Commits

Author SHA1 Message Date
Germán Jabloñski
ac46c289a1 revert incorrect eslint autfixes 2025-02-28 15:30:46 -03:00
Germán Jabloñski
4c54ccc25a Merge remote-tracking branch 'origin/main' into revert-11133-typescript-strict-plugin 2025-02-28 15:23:39 -03:00
Jacob Fletcher
67c4a20237 fix(next): properly instantiates req.url on localhost (#11455)
The `req.url` property at the page level was not reflective of the
actual URL on localhost. This was because we were passing an
incompatible `url` override into `createLocalReq` (lacking protocol).
This would silently fail to construct the URL object, ultimately losing
the top-level domain on `req.url` as well as the port on `req.origin`
(see #11454).

Closes #11448.
2025-02-28 13:04:26 -05:00
Alessio Gravili
38131ed2c3 feat: ability to cancel jobs (#11409)
This adds new `payload.jobs.cancel` and `payload.jobs.cancelByID` methods that allow you to cancel already-running jobs, or prevent queued jobs from running.

While it's not possible to cancel a function mid-execution, this will stop job execution the next time the job makes a request to the db, which happens after every task.
2025-02-28 17:58:43 +00:00
Patrik
96d1d90e78 fix(ui): use full image url for upload previews instead of thumbnail url (#11435) 2025-02-28 12:47:02 -05:00
Jessica Chowdhury
9e97319c6f fix(ui): locale selector in versions view should remove filtered locales (#11447)
### What?
The `locale selector` in the version comparison view shows all locales
on first load. It does not accomodate the `filterAvailableLocales`
option and shows locales which should be filtered.

### How?
Pass the initial locales through the `filterAvailableLocales` function.

Closes #11408

#### Testing
Use test suite `localization` and the `localized-drafts` collection.
Test added to `test/localization/e2e`.
2025-02-28 17:37:07 +00:00
Jacob Fletcher
a65289c211 fix: ensures req.origin includes port on localhost (#11454)
The `req.origin` property on the `PayloadRequest` object does not
include the port when running on localhost, a requirement of the [HTML
Living Standard](https://html.spec.whatwg.org/#origin). This was because
we were initializing the url with a fallback of `http://localhost` (no
port). When constructed via `new URL()`, the port is unable to be
extracted. This is fixed by using the `host` property off the headers
object, if it exists, which includes the port.

Partial fix for #11448.
2025-02-28 12:26:38 -05:00
Alessio Gravili
4a1b74952f chore(richtext-lexical): improve UploadData jsdocs (#11292) 2025-02-28 12:18:09 -05:00
Martijn Luyckx
8b55e7b51a docs: replace HTML entity ' with literal apostrophe (#11321) 2025-02-28 17:15:00 +00:00
Roy Barber
48e613b61f fix(examples): replace depreciated 'mergeHeaders' import in the MultiTenant example (#11306) 2025-02-28 17:13:46 +00:00
Jessica Chowdhury
428c133033 fix(ui): copyToLocale should not pass id in data, throws error in postgres (#11402) 2025-02-28 12:06:32 -05:00
Alessio Gravili
c8c578f5ef perf(next): reduce initReq calls from 3 to 1 per page load (#11312)
This PR significantly improves performance when navigating through the admin panel by reducing the number of times `initReq` is called. Previously, `initReq`—which handles expensive tasks like initializing Payload and running access control—was called **three times** for a single page load (for the root layout, the root page, and the notFound page).

We initially tried to use React Cache to ensure `initReq` only ran once per request. However, because React Cache performs a shallow object reference check on function arguments, the configuration object we passed (`configPromise`) and the `overrides` object never maintained the same reference, causing the cache to miss.

### What’s Changed

*   **New `getInitReqContainer` Helper**  
    We introduced a helper that provides a stable object reference throughout the entire request. This allows React to properly cache the output, ensuring `initReq` doesn’t get triggered multiple times by mistake.
    
*   **Splitting `initReq` into Two Functions**  
    The `initReq` logic was split into:
    
    *   **`initPartialReq`:** Runs only **once** per request, handling tasks that do not depend on page-level data (e.g., calling `.auth`, which performs a DB request).
    *    **`initReq`:** Runs **twice** (once for Layout+NotFound page and once for main page), handling tasks, most notably access control, that rely on page-level data such as locale or query parameters. The NotFound page will share the same req as the layout page, as it's not localized, and its access control wouldn't need to access page query / url / locale, just like the layout.

* **Remove duplicative logic**
   * Previously, a lot of logic was run in **both** `initReq` **and** the respective page / layout. This was completely unnecessary, as `initReq` was already running that logic. This PR returns the calculated variables from `initReq`, so they don't have to be duplicatively calculated again.

### Performance Gains

*   Previously:
    *   `.auth` call ran **3 times**
    *   Access control ran **3 times**
*   Now:
    *   `.auth` call runs **1 time**
    *   Access control runs **2 times**

This change yields a noticeable performance improvement by cutting down on redundant work.
2025-02-28 09:25:03 -07:00
Alessio Gravili
d53f166476 fix: ensure errors returned from tasks are properly logged (#11443)
Fixes https://github.com/payloadcms/payload/issues/9767

We allow failing a job queue task by returning `{ state: 'failed' }` from the task, instead of throwing an error. However, previously, this threw an error when trying to update the task in the database. Additionally, it was not possible to customize the error message.

This PR fixes that by letting you return `errorMessage` alongside `{ state: 'failed' }`, and by ensuring the error is transformed into proper json before saving it to the `error` column.
2025-02-28 16:00:56 +00:00
Sasha
dfddee2125 fix(storage-*): ensure client handler is always added to the import map, even if the plugin is disabled (#11438)
Ensures that even if you pass `enabled: false` to the storage adapter
options, e.g:
```ts
s3Storage({
  enabled: false,
  collections: {
    [mediaSlug]: true,
  },
  bucket: process.env.S3_BUCKET,
  config: {
    credentials: {
      accessKeyId: process.env.S3_ACCESS_KEY_ID,
      secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
    },
  },
})
```
the client handler component is added to the import map. This prevents
errors when you use the adapter only on production, but you don't
regenerate the import map before running the build
2025-02-28 16:34:00 +02:00
Patrik
e055565ca8 ci: repro guide to use create-payload-app@latest instead of @beta (#11451)
This PR updates the reproduction guide to reference
`create-payload-app@latest -t blank` instead of `@beta`, ensuring users
follow the latest stable release when setting up a minimal reproduction.
2025-02-28 09:32:09 -05:00
Alessio Gravili
41c7413f59 feat(db-*): add updateMany method to database adapter (#11441)
This PR adds a new `payload.db.updateMany` method, which is a more performant way to update multiple documents compared to using `payload.update`.
2025-02-27 20:30:17 -07:00
Jacob Fletcher
3709950d50 feat: maintains column state in url (#11387)
Maintains column state in the URL. This makes it possible to share
direct links to the list view in a specific column order or active
column state, similar to the behavior of filters. This also makes it
possible to change both the filters and columns in the same rendering
cycle, a requirement of the "list presets" feature being worked on here:
#11330.

For example:

```
?columns=%5B"title"%2C"content"%2C"-updatedAt"%2C"createdAt"%2C"id"%5D
```

The `-` prefix denotes that the column is inactive.

This strategy performs a single round trip to the server, ultimately
simplifying the table columns provider as it no longer needs to request
a newly rendered table for itself. Without this change, column state
would need to be replaced first, followed by a change to the filters.
This would make an unnecessary number of requests to the server and
briefly render the UI in a stale state.

This all happens behind an optimistic update, where the state of the
columns is immediately reflected in the UI while the request takes place
in the background.

Technically speaking, an additional database query in performed compared
to the old strategy, whereas before we'd send the data through the
request to avoid this. But this is a necessary tradeoff and doesn't have
huge performance implications. One could argue that this is actually a
good thing, as the data might have changed in the background which would
not have been reflected in the result otherwise.
2025-02-27 20:00:40 -05:00
Alessio Gravili
6ce5e8b83b perf: disable returning of db operations that don't need the return value (#11437)
If the return value of a db operation is not used, we can pass `returning: false` which will result in the query being executed faster.

See https://github.com/payloadcms/payload/pull/11393
2025-02-28 00:53:23 +00:00
Philipp Meyer
f3844ee533 fix(next): email verification not working due to incorrect token url parsing (#11439)
### What?
This PR reverts a presumably accidental change made in
[b80010b1a1](b80010b1a1),
that broke the email verification feature in v3.24.0 and onwards.
### Why?
Through the missing verify in `const [collectionSlug, verify, token] =
params.segments`, the token value was always the string `verify`
2025-02-28 00:05:05 +00:00
Alessio Gravili
c21dac1b53 perf(db-*): add option to disable returning modified documents in db methods (#11393)
This PR adds a new `returning` option to various db adapter methods. Setting it to `false` where the return value is not used will lead to performance gains, as we don't have to do additional db calls to fetch the updated document and then sanitize it.
2025-02-27 17:40:22 -05:00
Jarrod Flesch
b3e7a9d194 fix: incorrect value inside beforeValidate field hooks (#11433)
### What?
`value` within the beforeValidate field hook was not correctly falling
back to the document value when no value was passed inside the request
for the field.

### Why?
The fallback logic was running after the beforeValidate field hooks are
called.

### How?
Run the fallback logic before running the beforeValidate field hooks.

Fixes https://github.com/payloadcms/payload/issues/10923
2025-02-27 16:00:27 -05:00
Jacob Fletcher
c4bc0ae48a fix(next): disables active nav item (#11434)
When visiting a collection's list view, the nav item corresponding to
that collection correctly appears in an active state, but is still
rendered as an anchor tag. This makes it possible to reload the current
page by simply clicking the link, which is a problem because this
performs an unnecessary server roundtrip. This is especially apparent
when search params exist in the current URL, as the href on the link
does not.

Unrelated: also cleans up leftover code that was missed in this PR:
#11155
2025-02-27 15:21:28 -05:00
Patrik
f7b1cd9d63 fix(ui): duplicate basePath in Logout Button Link (#11432)
This PR resolves an issue where the `href` for the Logout button in the
admin panel included duplicate `basePath` values when `basePath` was set
in `next.config.js`.

The Logout button was recently updated to use `NextLink` (`next/link`),
which automatically applies the `basePath` from the Next.js
configuration. As a result, manually adding the `basePath` to the `href`
is no longer necessary.

Relevant PRs that modified this behavior originally: 
- #9275
- #11155
2025-02-27 13:58:27 -05:00
Jarrod Flesch
9c25e7b68e fix(plugin-multi-tenant): scope access constraint to admin collection (#11430)
### What?
The idea of this plugin is to only add constraints when a user is
present on a request. This change makes it so access control only
applies to admin panel users as they are the ones assigned to tenants.

This change allows you to more freely write access functions on tenant
enabled collections. Say you have 2 auth enabled collections, the plugin
would incorrectly assume since there is a user on the req that it needs
to apply tenant constraints. When really, you should be able to just add
in your own access check for `req.user.collection` and return true/false
if you want to prevent/allow other auth enabled collections for certain
operations.

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

const readByTenant: Access = ({ req }) => {
  const { user } = req
  if (!user || user.collection === 'auth2') return false
  return true
}
```

When you have a function like this that returns `true` and the
collection is multi-tenant enabled - the plugin injects constraints
ensuring the user on the request is assigned to the tenant on the doc
being accessed.

Before this change, you would need to opt out of access control with
`useTenantAccess` and then wire up your own access function:

```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'

export const tenantAccess: Access = async ({ req: { user } }) => {
  if (user) {
    if (user.collection === 'auth2') {
      return true
    }

    // Before, you would need to re-implement
    // internal multi-tenant access constraints
    if (user.roles?.includes('super-admin')) return true

    return getTenantAccess({
      fieldName: 'tenant',
      user,
    })
  }

  return false
}
```

After this change you would not need to opt out of `useTenantAccess` and
can just write:

```ts
import type { Access } from 'payload'
import { getTenantAccess } from '@payloadcms/plugin-multi-tenant/utilities'

export const tenantAccess: Access = async ({ req: { user } }) => {
  return Boolean(user)
}
```

This is because internally the plugin will only add the tenant
constraint when the access function returns true/Where _AND_ the user
belongs to the admin panel users collection.
2025-02-27 13:53:45 -05:00
Elliot DeNolf
1d252cbacf templates: bump for v3.25.0 (#11431)
🤖 Automated bump of templates for v3.25.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-27 13:13:47 -05:00
Alessio Gravili
7118b6418f fix(ui): disable publish button if form is autosaving (#11343)
Fixes https://github.com/payloadcms/payload/issues/6648

This PR introduces a new `useFormBackgroundProcessing` hook and a corresponding `setBackgroundProcessing` function in the `useForm` hook.

Unlike `useFormProcessing` / `setProcessing`, which mark the entire form as read-only, this new approach only disables the Publish button during autosaving, keeping form fields editable for a better user experience.

I named it `backgroundProcessing` because it should run behind the scenes without disrupting the user. You could argue that it is a bit more generic than something like `isAutosaving`, but it signals intent: Background = do not disrupt the user.
2025-02-27 10:28:08 -07:00
Elliot DeNolf
bdf0113b2f chore(release): v3.25.0 [skip ci] 2025-02-27 12:06:03 -05:00
Sasha
3436fb16ea feat: allow to count related docs for join fields (#11395)
### What?
For the join field query adds ability to specify `count: true`, example:
```ts
const result = await payload.find({
  joins: {
    'group.relatedPosts': {
      sort: '-title',
      count: true,
    },
  },
  collection: "categories",
})

result.group?.relatedPosts?.totalDocs // available
```

### Why?
Can be useful to implement full pagination / show total related
documents count in the UI.

### How?
Implements the logic in database adapters. In MongoDB it's additional
`$lookup` that has `$count` in the pipeline. In SQL, it's additional
subquery with `COUNT(*)`. Preserves the current behavior by default,
since counting introduces overhead.


Additionally, fixes a typescript generation error for join fields.
Before, `docs` and `hasNextPage` were marked as nullable, which is not
true, these fields cannot be `null`.
Additionally, fixes threading of `joinQuery` in
`transform/read/traverseFields` for group / tab fields recursive calls.
2025-02-27 16:05:48 +00:00
Patrik
bcc68572bf docs: adds Reserved Field Names section to migration guide (#11308)
Added a new Reserved Field Names section to the migration guide.

Clarified that certain field names (`__v`, `salt`, `hash`, `file`, etc.)
are reserved for internal use and will be sanitized from the config if
used.

Included additional reserved names specific to `MongoDB`, `auth`-enabled
collections, and `upload`-enabled collections.

Added a note recommending against using field names with an underscore
(`_`) prefix, as they are reserved for internal columns and may cause
conflicts in `SQL` and other contexts.

Fixes #11159
2025-02-27 10:36:34 -05:00
Jacob Fletcher
6aa9da73f8 Revert "feat: simplify column prefs (#11390)" (#11427)
This reverts commit 69c0d09 in #11390.

In order to future proof column prefs, it probably is best to continue
to use the current shape. This change was intended to ensure that as
little transformation to URL params was made as possible for #11387, but
we will likely transform them after all.

This will ensure that we can add support for additional properties over
time, as needed. For example, if we hypothetically wanted to add a
custom `label` or similar feature to columns prefs, it would make more
sense to use explicit properties to identity `accessor` and `active`.

For example:

```ts
[
  {
    accessor: "title",
    active: true,
    label: 'Custom Label' // hypothetical
  }
]
```
2025-02-27 08:39:24 -05:00
Alessio Gravili
2a3682ff68 fix(deps): ensure Next.js 15.2.0 compatibility, upgrade nextjs and @types/react versions in monorepo (#11419)
This bumps next.js to 15.2.0 in our monorepo, as well as all @types/react and @types/react-dom versions. Additionally, it removes the obsolete `peerDependencies` property from our root package.json.

This PR also fixes 2 bugs introduced by Next.js 15.2.0. This highlights why running our test suite against the latest Next.js, to make sure Payload is compatible, version is important.

## 1. handleWhereChange running endlessly

Upgrading to Next.js 15.2.0 caused `handleWhereChange` to be continuously called by a `useEffect` when the list view filters were opened, leading to a React error - I did not investigate why upgrading the Next.js version caused that, but this PR fixes it by making use of the more predictable `useEffectEvent`.

## 2. Custom Block and Array label React key errors

Upgrading to Next.js 15.2.0 caused react key errors when rendering custom block and array row labels on the server. This has been fixed by rendering those with a key

## 3. Table React key errors

When rendering a `Table`, a React key error is thrown since Next.js 15.2.0
2025-02-27 05:56:09 +00:00
Jarrod Flesch
958e195017 feat(plugin-multi-tenant): allow customization of selector label (#11418)
### What?
Allows for custom labeling of the tenant selector shown in the sidebar.

Fixes https://github.com/payloadcms/payload/issues/11262
2025-02-26 22:39:51 -05:00
Jarrod Flesch
45cee23add feat(plugin-multi-tenant): filter users list and tenants lists (#11417)
### What?
- Adds `users` base list filtering when tenant is selected
- Adds `tenants` base list filtering when tenant is selected
2025-02-26 21:50:36 -05:00
Alessio Gravili
67b7a730ba docs: improve lexical code block documentation (#11416)
The existing code example had type errors when `strict: true` was enabled
2025-02-27 00:47:36 +00:00
Alessio Gravili
88a2841500 docs: add lexical docs for configuring jsx converters for internal links and overriding them (#11415)
This PR improves existing JSX converter docs and adds 2 new sections:
- **converting internal links** - addresses why a `"found internal link, but internalDocToHref is not provided"` error is thrown, and how to get around it
- **Overriding default JSX Converters**
2025-02-27 00:41:41 +00:00
Alessio Gravili
7e713a454a feat(richtext-lexical): allows client features to access components added to the import map by the server feature (#11414)
Lexical server features are able to add components to the import map through the `componentImports` property. As of now, the client feature did not have access to those. This is usually not necessary, as those import map entries are used internally to render custom components server-side, e.g. when a request to the form state endpoint is made.

However, in some cases, these import map entries need to be accessed by the client feature (see "Why" section below).

This PR ensures that keyed `componentImports` entries are made available to the client feature via the new `featureClientImportMap` property.

## Why?

This is a prerequisite of the lexical [wrapper blocks PR](https://github.com/payloadcms/payload/pull/9289), where wrapper block custom components need to be made to the ClientFeature. The ClientFeature is where the wrapper block node is registered - in order to generate the wrapper block node, we need access to the component
2025-02-27 00:21:15 +00:00
Jacob Fletcher
b975858e76 test: removes all unnecessary page.waitForURL methods (#11412)
Removes all unnecessary `page.waitForURL` methods within e2e tests.
These are unneeded when following a `page.goto` call because the
subsequent page load is already being awaited.

It is only a requirement when:

- Clicking a link and expecting navigation
- Expecting a redirect after a route change
- Waiting for a change in search params
2025-02-26 16:54:39 -05:00
Sasha
b540da53ec feat(storage-*): large file uploads on Vercel (#11382)
Currently, usage of Payload on Vercel has a limitation - uploads are
limited by 4.5MB file size.
This PR allows you to pass `clientUploads: true` to all existing storage
adapters
* Storage S3
* Vercel Blob
* Google Cloud Storage
* Uploadthing
* Azure Blob

And then, Payload will do uploads on the client instead. With the S3
Adapter it uses signed URLs and with Vercel Blob it does this -
https://vercel.com/guides/how-to-bypass-vercel-body-size-limit-serverless-functions#step-2:-create-a-client-upload-route.
Note that it doesn't mean that anyone can now upload files to your
storage, it still does auth checks and you can customize that with
`clientUploads.access`


https://github.com/user-attachments/assets/5083c76c-8f5a-43dc-a88c-9ddc4527d91c

Implements https://github.com/payloadcms/payload/discussions/7569
feature request.
2025-02-26 21:59:34 +02:00
Alessio Gravili
c6ab312286 chore: cleanup queues test suite (#11410)
This PR extracts each workflow of our queues test suite into its own file
2025-02-26 19:43:31 +00:00
Sasha
526e535763 fix: ensure custom IDs are returned to the result when select query exists (#11400)
Previously, behavior with custom IDs and `select` query was incorrect.
By default, the `id` field is guaranteed to be selected, even if it
doesn't exist in the `select` query, this wasn't true for custom IDs.
2025-02-26 17:05:50 +02:00
Alessio Gravili
e4712a822b perf(drizzle): use faster, direct db query for getting id to update in updateOne (#11391)
Previously, `updateOne` was using `buildFindManyArgs` and `findFirst` just to retrieve the ID of the document to update, which is a huge function that's not necessary to run just to get the document ID.

This PR refactors it to use a simple `db.select` query to retrieve the ID
2025-02-25 21:54:38 -07:00
Patrik
81fd42ef69 fix(ui): skip bulk upload thumbnail generation on non-image files (#11378)
This PR fixes an issue where bulk upload attempts to generate thumbnails
for non-image files, causing errors on the page.

The fix ensures that thumbnail generation is skipped for non-image
files, preventing unnecessary errors.

Fixes #10428
2025-02-25 16:55:44 -05:00
Sasha
6b6c289d79 fix(db-mongodb): hasNextPage with polymorphic joins (#11394)
Previously, `hasNextPage` was working incorrectly with polymorphic joins
(that have an array of `collection`) in MongoDB.

This PR fixes it and adds extra assertions to the polymorphic joins
test.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-02-25 21:22:47 +00:00
Germán Jabloñski
be52a2a223 Merge remote-tracking branch 'origin/main' into revert-11133-typescript-strict-plugin 2025-02-17 09:12:40 -03:00
Germán Jabloñski
0824b4f34c Revert "chore: add typescript-strict-plugin to the payload package for increm…"
This reverts commit 6eee787493.
2025-02-13 09:48:29 -03:00
502 changed files with 5998 additions and 2836 deletions

View File

@@ -4,7 +4,7 @@ Depending on the quality of reproduction steps, this issue may be closed if no r
### Why was this issue marked with the `invalid-reproduction` label?
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@latest -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.

View File

@@ -158,6 +158,7 @@ object with:
- `docs` an array of related documents or only IDs if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
```json
{
@@ -171,7 +172,8 @@ object with:
}
// { ... }
],
"hasNextPage": false
"hasNextPage": false,
"totalDocs": 10, // if count: true is passed
}
// other fields...
}
@@ -184,6 +186,7 @@ object with:
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
```json
{
@@ -200,7 +203,8 @@ object with:
}
// { ... }
],
"hasNextPage": false
"hasNextPage": false,
"totalDocs": 10, // if count: true is passed
}
// other fields...
}
@@ -215,10 +219,11 @@ returning. This is useful for performance reasons when you don't need the relate
The following query options are supported:
| Property | Description |
|-------------|-----------------------------------------------------------------------------------------------------|
| ----------- | --------------------------------------------------------------------------------------------------- |
| **`limit`** | The maximum related documents to be returned, default is 10. |
| **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. |
| **`sort`** | A string used to order related results |
| **`count`** | Whether include the count of related documents or not. Not included by default |
These can be applied to the local API, GraphQL, and REST API.

View File

@@ -44,3 +44,31 @@ const createdJob = await payload.jobs.queue({
},
})
```
#### Cancelling Jobs
Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks.
##### Cancel a Single Job
To cancel a specific job, use the `payload.jobs.cancelByID` method with the job's ID:
```ts
await payload.jobs.cancelByID({
id: createdJob.id,
})
```
##### Cancel Multiple Jobs
To cancel multiple jobs at once, use the `payload.jobs.cancel` method with a `Where` query:
```ts
await payload.jobs.cancel({
where: {
workflowSlug: {
equals: 'createPost',
},
},
})
```

View File

@@ -1113,6 +1113,57 @@ plugins: [
If you have custom features for `@payloadcms/richtext-lexical` you will need to migrate your code to the new API. Read more about the new API in the [documentation](https://payloadcms.com/docs/rich-text/building-custom-features).
## Reserved Field names
Payload reserves certain field names for internal use. Using any of the following names in your collections or globals will result in those fields being sanitized from the config, which can cause deployment errors. Ensure that any conflicting fields are renamed before migrating.
### General Reserved Names
- `file`
- `_id` (MongoDB only)
- `__v` (MongoDB only)
**Important Note**: It is recommended to avoid using field names with an underscore (`_`) prefix unless explicitly required by a plugin. Payload uses this prefix for internal columns, which can lead to conflicts in certain SQL conditions. The following are examples of reserved internal columns (this list is not exhaustive and other internal fields may also apply):
- `_order`
- `_path`
- `_uuid`
- `_parent_id`
- `_locale`
### Auth-Related Reserved Names
These are restricted if your collection uses `auth: true` and does not have `disableAuthStrategy: true`:
- `salt`
- `hash`
- `apiKey` (when `auth.useAPIKey: true` is enabled)
- `useAPIKey` (when `auth.useAPIKey: true` is enabled)
- `resetPasswordToken`
- `resetPasswordExpiration`
- `password`
- `email`
- `username`
### Upload-Related Reserved Names
These apply if your collection has `upload: true` configured:
- `filename`
- `mimetype`
- `filesize`
- `width`
- `height`
- `focalX`
- `focalY`
- `url`
- `thumbnailURL`
If `imageSizes` is configured, the following are also reserved:
- `sizes`
If any of these names are found in your collection / global fields, update them before migrating to avoid unexpected issues.
## Upgrade from previous beta
Reference this [community-made site](https://payload-releases-filter.vercel.app/?version=3&from=152429656&to=188243150&sort=asc&breaking=on). Set your version, sort by oldest first, enable breaking changes only.

View File

@@ -6,7 +6,7 @@ desc: Starting to build your own plugin? Find everything you need and learn best
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Building your own [Payload Plugin](./overview) is easy, and if you&apos;re already familiar with Payload then you&apos;ll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
<Banner type="success">
To use the template, run `npx create-payload-app@latest --template plugin` directly in
@@ -19,7 +19,7 @@ Our plugin template includes everything you need to build a full life-cycle plug
- A local dev environment to develop the plugin
- Test suite with integrated GitHub workflow
By abstracting your code into a plugin, you&apos;ll be able to reuse your feature across multiple projects and make it available for other developers to use.
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
## Plugins Recap
@@ -75,7 +75,7 @@ The purpose of the **dev** folder is to provide a sanitized local Payload projec
Do **not** store any of the plugin functionality in this folder - it is purely an environment to _assist_ you with developing the plugin.
If you&apos;re starting from scratch, you can easily setup a dev environment like this:
If you're starting from scratch, you can easily setup a dev environment like this:
```
mkdir dev
@@ -83,7 +83,7 @@ cd dev
npx create-payload-app@latest
```
If you&apos;re using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
```
plugins: [
@@ -96,7 +96,7 @@ If you&apos;re using the plugin template, the dev folder is built out for you an
You can add to the `dev/payload.config.ts` and build out the dev project as needed to test your plugin.
When you&apos;re ready to start development, navigate into this folder with `cd dev`
When you're ready to start development, navigate into this folder with `cd dev`
And then start the project with `pnpm dev` and pull up `http://localhost:3000` in your browser.
@@ -108,7 +108,7 @@ A good test suite is essential to ensure quality and stability in your plugin. P
Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the [Jest documentation.](https://jestjs.io/)
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you&apos;re all set!
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set!
```
let payload: Payload
@@ -160,7 +160,7 @@ export const seed = async (payload: Payload): Promise<void> => {
## Building a Plugin
Now that we have our environment setup and dev project ready to go - it&apos;s time to build the plugin!
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
```
@@ -217,7 +217,7 @@ To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend
We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly, else this can cause adverse behavior and conflicts with Payload Config and other plugins.
Let&apos;s say you want to build a plugin that adds a new collection:
Let's say you want to build a plugin that adds a new collection:
```
config.collections = [
@@ -227,7 +227,7 @@ config.collections = [
]
```
First, you need to spread the `config.collections` to ensure that we don&apos;t lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
This same logic is applied to other array and object like properties such as admin, globals and hooks:
@@ -284,7 +284,7 @@ For a better user experience, provide a way to disable the plugin without uninst
### Include tests in your GitHub CI workflow
If you&apos;ve configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
### Publish your finished plugin to npm

View File

@@ -52,7 +52,7 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
@@ -158,6 +158,16 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
rowFields?: never
tenantFieldAccess?: never
}
/**
* Customize tenant selector label
*
* Either a string or an object where the keys are locales and the values are the string labels
*/
tenantSelectorLabel?:
| Partial<{
[key in AcceptedLanguages]?: string
}>
| string
/**
* The slug for the tenant collection
*
@@ -176,6 +186,14 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter users by selected tenant
*/
useUsersTenantFilter?: boolean
}
```

View File

@@ -10,7 +10,7 @@ Lexical saves data in JSON - this is great for storage and flexibility and allow
## Lexical => JSX
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
For React-based frontends, converting Lexical content to JSX is the recommended rendering approach. Import the RichText component from @payloadcms/richtext-lexical/react and pass the Lexical content to it:
```tsx
import React from 'react'
@@ -24,46 +24,130 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
}
```
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop. In our [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) you have an example of how to use `converters` to render custom blocks, custom nodes and override existing converters.
<Banner type="default">
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.
When fetching data, ensure your `depth` setting is high enough to fully populate Lexical nodes such as uploads. The JSX converter requires fully populated data to work correctly.
</Banner>
### Converting Lexical Blocks to JSX
### Converting Internal Links
In order to convert lexical blocks or inline blocks to JSX, you will have to pass the converter for your block to the RichText component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document.
```tsx
import React from 'react'
import {
type JSXConvertersFunction,
RichText,
} from '@payloadcms/richtext-lexical/react'
import type { DefaultNodeTypes, SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
import {
type JSXConvertersFunction,
LinkJSXConverter,
RichText,
} from '@payloadcms/richtext-lexical/react'
import React from 'react'
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
const { relationTo, value } = linkNode.fields.doc!
if (typeof value !== 'object') {
throw new Error('Expected value to be an object')
}
const slug = value.slug
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
}
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
...LinkJSXConverter({ internalDocToHref }),
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Converting Lexical Blocks
To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your block to the `RichText` component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
import React from 'react'
// Extend the default node types with your custom blocks for full type safety
type NodeTypes = DefaultNodeTypes | SerializedBlockNode<MyInlineBlock | MyTextBlock>
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
// myTextBlock is the slug of the block
// Each key should match your block's slug
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
},
inlineBlocks: {
// myInlineBlock is the slug of the block
// Each key should match your inline block's slug
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
},
})
export const MyComponent = ({ lexicalData }) => {
return (
<RichText
converters={jsxConverters}
data={lexicalData.lexicalWithBlocks as SerializedEditorState}
/>
)
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Overriding Default JSX Converters
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
Example - overriding the upload node converter to use next/image:
```tsx
'use client'
import type { DefaultNodeTypes, SerializedUploadNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
import Image from 'next/image'
import React from 'react'
type NodeTypes = DefaultNodeTypes
// Custom upload converter component that uses next/image
const CustomUploadComponent: React.FC<{
node: SerializedUploadNode
}> = ({ node }) => {
if (node.relationTo === 'uploads') {
const uploadDoc = node.value
if (typeof uploadDoc !== 'object') {
return null
}
const { alt, height, url, width } = uploadDoc
return <Image alt={alt} height={height} src={url} width={width} />
}
return null
}
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
// Override the default upload converter
upload: ({ node }) => {
return <CustomUploadComponent node={node} />
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```

View File

@@ -29,9 +29,9 @@ Using the BlocksFeature, you can add both inline blocks (= can be inserted into
### Example: Code Field Block with language picker
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. Make sure to manually install `@payloadcms/ui`first.
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. First, make sure to explicitly install `@payloadcms/ui` in your project.
Field config:
Field Config:
```ts
import {
@@ -91,7 +91,6 @@ CodeComponent.tsx:
```tsx
'use client'
import type { CodeFieldClient, CodeFieldClientProps } from 'payload'
import { CodeField, useFormFields } from '@payloadcms/ui'
@@ -105,6 +104,8 @@ const languageKeyToMonacoLanguageMap = {
tsx: 'typescript',
}
type Language = keyof typeof languageKeyToMonacoLanguageMap
export const Code: React.FC<CodeFieldClientProps> = ({
autoComplete,
field,
@@ -118,10 +119,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
}) => {
const languageField = useFormFields(([fields]) => fields['language'])
const language: string =
(languageField?.value as string) || (languageField.initialValue as string) || 'typescript'
const language: Language =
(languageField?.value as Language) || (languageField?.initialValue as Language) || 'ts'
const label = languages[language as keyof typeof languages]
const label = languages[language]
const props: CodeFieldClient = useMemo<CodeFieldClient>(
() => ({
@@ -129,9 +130,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
type: 'code',
admin: {
...field.admin,
label,
editorOptions: undefined,
language: languageKeyToMonacoLanguageMap[language] || language,
},
label,
}),
[field, language, label],
)

View File

@@ -30,6 +30,7 @@ pnpm add @payloadcms/storage-vercel-blob
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
- Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
```ts
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
@@ -64,6 +65,7 @@ export default buildConfig({
| `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` |
| `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) |
| `token` | Vercel Blob storage read/write token | `''` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## S3 Storage
[`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3)
@@ -79,6 +81,7 @@ pnpm add @payloadcms/storage-s3
- Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs.
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
```ts
import { s3Storage } from '@payloadcms/storage-s3'
@@ -126,6 +129,7 @@ pnpm add @payloadcms/storage-azure
- Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website.
```ts
import { azureStorage } from '@payloadcms/storage-azure'
@@ -161,6 +165,7 @@ export default buildConfig({
| `baseURL` | Base URL for the Azure Blob storage account | |
| `connectionString` | Azure Blob storage connection string | |
| `containerName` | Azure Blob storage container name | |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## Google Cloud Storage
[`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs)
@@ -175,6 +180,7 @@ pnpm add @payloadcms/storage-gcs
- Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
```ts
import { gcsStorage } from '@payloadcms/storage-gcs'
@@ -203,13 +209,14 @@ export default buildConfig({
### Configuration Options#gcs-configuration
| Option | Description | Default |
| ------------- | --------------------------------------------------------------------------------------------------- | --------- |
| `enabled` | Whether or not to enable the plugin | `true` |
| `collections` | Collections to apply the storage to | |
| `bucket` | The name of the bucket to use | |
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
| `acl` | Access control list for files that are uploaded | `Private` |
| Option | Description | Default |
| --------------- | --------------------------------------------------------------------------------------------------- | --------- |
| `enabled` | Whether or not to enable the plugin | `true` |
| `collections` | Collections to apply the storage to | |
| `bucket` | The name of the bucket to use | |
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
| `acl` | Access control list for files that are uploaded | `Private` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## Uploadthing Storage
@@ -226,6 +233,7 @@ pnpm add @payloadcms/storage-uploadthing
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
- Get a token from Uploadthing and set it as `token` in the `options` object.
- `acl` is optional and defaults to `public-read`.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
```ts
export default buildConfig({
@@ -246,13 +254,14 @@ export default buildConfig({
### Configuration Options#uploadthing-configuration
| Option | Description | Default |
| ---------------- | ----------------------------------------------- | ------------- |
| `token` | Token from Uploadthing. Required. | |
| `acl` | Access control list for files that are uploaded | `public-read` |
| `logLevel` | Log level for Uploadthing | `info` |
| `fetch` | Custom fetch function | `fetch` |
| `defaultKeyType` | Default key type for file operations | `fileKey` |
| Option | Description | Default |
| ---------------- | ------------------------------------------------------------- | ------------- |
| `token` | Token from Uploadthing. Required. | |
| `acl` | Access control list for files that are uploaded | `public-read` |
| `logLevel` | Log level for Uploadthing | `info` |
| `fetch` | Custom fetch function | `fetch` |
| `defaultKeyType` | Default key type for file operations | `fileKey` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## Custom Storage Adapters

View File

@@ -1,7 +1,6 @@
import type { CollectionAfterLoginHook } from 'payload'
import { mergeHeaders } from '@payloadcms/next/utilities'
import { generateCookie, getCookieExpiration } from 'payload'
import { mergeHeaders, generateCookie, getCookieExpiration } from 'payload'
export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, user }) => {
const relatedOrg = await req.payload.find({

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.24.0",
"version": "3.25.0",
"private": true,
"type": "module",
"scripts": {
@@ -117,7 +117,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.1.5",
"@next/bundle-analyzer": "15.2.0",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -132,8 +132,8 @@
"@types/jest": "29.5.12",
"@types/minimist": "1.2.5",
"@types/node": "22.5.4",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/shelljs": "0.8.15",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -153,7 +153,7 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^10",
"next": "15.1.5",
"next": "15.2.0",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.50.0",
@@ -173,10 +173,6 @@
"turbo": "^2.3.3",
"typescript": "5.7.3"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
},
"packageManager": "pnpm@9.7.1",
"engines": {
"node": "^18.20.2 || >=20.9.0",

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import type { CreateOptions } from 'mongoose'
import type { Create, Document } from 'payload'
import type { Create } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -9,7 +9,7 @@ import { transform } from './utilities/transform.js'
export const create: Create = async function create(
this: MongooseAdapter,
{ collection, data, req },
{ collection, data, req, returning },
) {
const Model = this.collections[collection]
const options: CreateOptions = {
@@ -34,6 +34,9 @@ export const create: Create = async function create(
} catch (error) {
handleError({ collection, error, req })
}
if (returning === false) {
return null
}
doc = doc.toObject()

View File

@@ -8,7 +8,7 @@ import { transform } from './utilities/transform.js'
export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
{ slug, data, req },
{ slug, data, req, returning },
) {
const Model = this.globals
@@ -25,6 +25,9 @@ export const createGlobal: CreateGlobal = async function createGlobal(
}
let [result] = (await Model.create([data], options)) as any
if (returning === false) {
return null
}
result = result.toObject()

View File

@@ -16,6 +16,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
parent,
publishedLocale,
req,
returning,
snapshot,
updatedAt,
versionData,
@@ -75,6 +76,10 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
options,
)
if (returning === false) {
return null
}
doc = doc.toObject()
transform({

View File

@@ -16,6 +16,7 @@ export const createVersion: CreateVersion = async function createVersion(
parent,
publishedLocale,
req,
returning,
snapshot,
updatedAt,
versionData,
@@ -86,6 +87,10 @@ export const createVersion: CreateVersion = async function createVersion(
options,
)
if (returning === false) {
return null
}
doc = doc.toObject()
transform({

View File

@@ -1,4 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { DeleteOne } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -10,10 +10,10 @@ import { transform } from './utilities/transform.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter,
{ collection, req, select, where },
{ collection, req, returning, select, where },
) {
const Model = this.collections[collection]
const options: QueryOptions = {
const options: MongooseUpdateQueryOptions = {
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
@@ -29,6 +29,11 @@ export const deleteOne: DeleteOne = async function deleteOne(
where,
})
if (returning === false) {
await Model.deleteOne(query, options)?.lean()
return null
}
const doc = await Model.findOneAndDelete(query, options)?.lean()
if (!doc) {

View File

@@ -16,6 +16,7 @@ import type {
TypeWithVersion,
UpdateGlobalArgs,
UpdateGlobalVersionArgs,
UpdateManyArgs,
UpdateOneArgs,
UpdateVersionArgs,
} from 'payload'
@@ -53,6 +54,7 @@ import { commitTransaction } from './transactions/commitTransaction.js'
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
import { updateGlobal } from './updateGlobal.js'
import { updateGlobalVersion } from './updateGlobalVersion.js'
import { updateMany } from './updateMany.js'
import { updateOne } from './updateOne.js'
import { updateVersion } from './updateVersion.js'
import { upsert } from './upsert.js'
@@ -160,6 +162,7 @@ declare module 'payload' {
updateGlobalVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateGlobalVersionArgs<T>,
) => Promise<TypeWithVersion<T>>
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
updateVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
@@ -200,6 +203,7 @@ export function mongooseAdapter({
mongoMemoryServer,
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
updateMany,
url,
versions: {},
// DatabaseAdapter

View File

@@ -1,4 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { UpdateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -9,12 +9,12 @@ import { transform } from './utilities/transform.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
{ slug, data, options: optionsArgs = {}, req, select },
{ slug, data, options: optionsArgs = {}, req, returning, select },
) {
const Model = this.globals
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
const options: QueryOptions = {
const options: MongooseUpdateQueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -28,6 +28,11 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
transform({ adapter: this, data, fields, globalSlug: slug, operation: 'write' })
if (returning === false) {
await Model.updateOne({ globalType: slug }, data, options)
return null
}
const result: any = await Model.findOneAndUpdate({ globalType: slug }, data, options)
transform({ adapter: this, data: result, fields, globalSlug: slug, operation: 'read' })

View File

@@ -1,4 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { MongooseUpdateQueryOptions } from 'mongoose'
import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload'
@@ -17,6 +17,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
locale,
options: optionsArgs = {},
req,
returning,
select,
versionData,
where,
@@ -28,7 +29,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
const flattenedFields = buildVersionGlobalFields(this.payload.config, currentGlobal, true)
const options: QueryOptions = {
const options: MongooseUpdateQueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -49,6 +50,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
transform({ adapter: this, data: versionData, fields, operation: 'write' })
if (returning === false) {
await VersionModel.updateOne(query, versionData, options)
return null
}
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) {

View File

@@ -0,0 +1,61 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { UpdateMany } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
import { transform } from './utilities/transform.js'
export const updateMany: UpdateMany = async function updateMany(
this: MongooseAdapter,
{ collection, data, locale, options: optionsArgs = {}, req, returning, select, where },
) {
const Model = this.collections[collection]
const fields = this.payload.collections[collection].config.fields
const options: MongooseUpdateQueryOptions = {
...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
select,
}),
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
locale,
where,
})
transform({ adapter: this, data, fields, operation: 'write' })
try {
await Model.updateMany(query, data, options)
} catch (error) {
handleError({ collection, error, req })
}
if (returning === false) {
return null
}
const result = await Model.find(query, {}, options)
transform({
adapter: this,
data: result,
fields,
operation: 'read',
})
return result
}

View File

@@ -1,4 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { UpdateOne } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -11,12 +11,22 @@ import { transform } from './utilities/transform.js'
export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter,
{ id, collection, data, locale, options: optionsArgs = {}, req, select, where: whereArg },
{
id,
collection,
data,
locale,
options: optionsArgs = {},
req,
returning,
select,
where: whereArg,
},
) {
const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection]
const fields = this.payload.collections[collection].config.fields
const options: QueryOptions = {
const options: MongooseUpdateQueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -41,7 +51,12 @@ export const updateOne: UpdateOne = async function updateOne(
transform({ adapter: this, data, fields, operation: 'write' })
try {
result = await Model.findOneAndUpdate(query, data, options)
if (returning === false) {
await Model.updateOne(query, data, options)
return null
} else {
result = await Model.findOneAndUpdate(query, data, options)
}
} catch (error) {
handleError({ collection, error, req })
}

View File

@@ -1,4 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { MongooseUpdateQueryOptions } from 'mongoose'
import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
@@ -11,7 +11,7 @@ import { transform } from './utilities/transform.js'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
{ id, collection, locale, options: optionsArgs = {}, req, select, versionData, where },
{ id, collection, locale, options: optionsArgs = {}, req, returning, select, versionData, where },
) {
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }
@@ -26,7 +26,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
true,
)
const options: QueryOptions = {
const options: MongooseUpdateQueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -47,6 +47,11 @@ export const updateVersion: UpdateVersion = async function updateVersion(
transform({ adapter: this, data: versionData, fields, operation: 'write' })
if (returning === false) {
await VersionModel.updateOne(query, versionData, options)
return null
}
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) {

View File

@@ -4,7 +4,16 @@ import type { MongooseAdapter } from './index.js'
export const upsert: Upsert = async function upsert(
this: MongooseAdapter,
{ collection, data, locale, req, select, where },
{ collection, data, locale, req, returning, select, where },
) {
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, select, where })
return this.updateOne({
collection,
data,
locale,
options: { upsert: true },
req,
returning,
select,
where,
})
}

View File

@@ -78,6 +78,7 @@ export const buildJoinAggregation = async ({
}
const {
count = false,
limit: limitJoin = join.field.defaultLimit ?? 10,
page,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
@@ -121,6 +122,28 @@ export const buildJoinAggregation = async ({
const alias = `${as}.docs.${collectionSlug}`
aliases.push(alias)
const basePipeline = [
{
$addFields: {
relationTo: {
$literal: collectionSlug,
},
},
},
{
$match: {
$and: [
{
$expr: {
$eq: [`$${join.field.on}`, '$$root_id_'],
},
},
$match,
],
},
},
]
aggregate.push({
$lookup: {
as: alias,
@@ -129,25 +152,7 @@ export const buildJoinAggregation = async ({
root_id_: '$_id',
},
pipeline: [
{
$addFields: {
relationTo: {
$literal: collectionSlug,
},
},
},
{
$match: {
$and: [
{
$expr: {
$eq: [`$${join.field.on}`, '$$root_id_'],
},
},
$match,
],
},
},
...basePipeline,
{
$sort: {
[sortProperty]: sortDirection,
@@ -169,6 +174,24 @@ export const buildJoinAggregation = async ({
],
},
})
if (count) {
aggregate.push({
$lookup: {
as: `${as}.totalDocs.${alias}`,
from: adapter.collections[collectionSlug].collection.name,
let: {
root_id_: '$_id',
},
pipeline: [
...basePipeline,
{
$count: 'result',
},
],
},
})
}
}
aggregate.push({
@@ -179,6 +202,23 @@ export const buildJoinAggregation = async ({
},
})
if (count) {
aggregate.push({
$addFields: {
[`${as}.totalDocs`]: {
$add: aliases.map((alias) => ({
$ifNull: [
{
$first: `$${as}.totalDocs.${alias}.result`,
},
0,
],
})),
},
},
})
}
aggregate.push({
$set: {
[`${as}.docs`]: {
@@ -195,17 +235,17 @@ export const buildJoinAggregation = async ({
const sliceValue = page ? [(page - 1) * limitJoin, limitJoin] : [limitJoin]
aggregate.push({
$set: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, ...sliceValue],
$addFields: {
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
},
},
})
aggregate.push({
$addFields: {
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
$set: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, ...sliceValue],
},
},
})
@@ -222,6 +262,7 @@ export const buildJoinAggregation = async ({
}
const {
count,
limit: limitJoin = join.field.defaultLimit ?? 10,
page,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
@@ -274,6 +315,31 @@ export const buildJoinAggregation = async ({
polymorphicSuffix = '.value'
}
const addTotalDocsAggregation = (as: string, foreignField: string) =>
aggregate.push(
{
$lookup: {
as: `${as}.totalDocs`,
foreignField,
from: adapter.collections[slug].collection.name,
localField: versions ? 'parent' : '_id',
pipeline: [
{
$match,
},
{
$count: 'result',
},
],
},
},
{
$addFields: {
[`${as}.totalDocs`]: { $ifNull: [{ $first: `$${as}.totalDocs.result` }, 0] },
},
},
)
if (adapter.payload.config.localization && locale === 'all') {
adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
@@ -304,6 +370,7 @@ export const buildJoinAggregation = async ({
},
},
)
if (limitJoin > 0) {
aggregate.push({
$addFields: {
@@ -313,6 +380,10 @@ export const buildJoinAggregation = async ({
},
})
}
if (count) {
addTotalDocsAggregation(as, `${join.field.on}${code}${polymorphicSuffix}`)
}
})
} else {
const localeSuffix =
@@ -359,6 +430,11 @@ export const buildJoinAggregation = async ({
},
},
)
if (count) {
addTotalDocsAggregation(as, foreignField)
}
if (limitJoin > 0) {
aggregate.push({
$addFields: {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.24.0",
"version": "3.25.0",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -33,6 +33,7 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
@@ -185,6 +186,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
upsert: updateOne,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.24.0",
"version": "3.25.0",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -34,6 +34,7 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
@@ -120,6 +121,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
tableNameMap: new Map<string, string>(),
tables: {},
transactionOptions: args.transactionOptions || undefined,
updateMany,
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.24.0",
"version": "3.25.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -33,6 +33,7 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
@@ -186,6 +187,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
upsert: updateOne,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.24.0",
"version": "3.25.0",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export const create: Create = async function create(
this: DrizzleAdapter,
{ collection: collectionSlug, data, req, select },
{ collection: collectionSlug, data, req, select, returning },
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
@@ -25,7 +25,12 @@ export const create: Create = async function create(
req,
select,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function createGlobal<T extends Record<string, unknown>>(
this: DrizzleAdapter,
{ slug, data, req }: CreateGlobalArgs,
{ slug, data, req, returning }: CreateGlobalArgs,
): Promise<T> {
const db = await getTransaction(this, req)
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
@@ -26,8 +26,13 @@ export async function createGlobal<T extends Record<string, unknown>>(
operation: 'create',
req,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
result.globalType = slug
return result

View File

@@ -21,6 +21,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
snapshot,
updatedAt,
versionData,
returning,
}: CreateGlobalVersionArgs,
) {
const db = await getTransaction(this, req)
@@ -45,6 +46,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
req,
select,
tableName,
ignoreResult: returning === false ? 'idOnly' : false,
})
const table = this.tables[tableName]
@@ -59,5 +61,9 @@ export async function createGlobalVersion<T extends TypeWithID>(
})
}
if (returning === false) {
return null
}
return result
}

View File

@@ -22,6 +22,7 @@ export async function createVersion<T extends TypeWithID>(
snapshot,
updatedAt,
versionData,
returning,
}: CreateVersionArgs<T>,
) {
const db = await getTransaction(this, req)
@@ -72,5 +73,9 @@ export async function createVersion<T extends TypeWithID>(
})
}
if (returning === false) {
return null
}
return result
}

View File

@@ -13,7 +13,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: DrizzleAdapter,
{ collection: collectionSlug, req, select, where: whereArg },
{ collection: collectionSlug, req, select, where: whereArg, returning },
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
@@ -59,13 +59,16 @@ export const deleteOne: DeleteOne = async function deleteOne(
docToDelete = await db.query[tableName].findFirst(findManyArgs)
}
const result = transform({
adapter: this,
config: this.payload.config,
data: docToDelete,
fields: collection.flattenedFields,
joinQuery: false,
})
const result =
returning === false
? null
: transform({
adapter: this,
config: this.payload.config,
data: docToDelete,
fields: collection.flattenedFields,
joinQuery: false,
})
await this.deleteWhere({
db,

View File

@@ -2,7 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
@@ -386,6 +386,7 @@ export const traverseFields = ({
}
const {
count: shouldCount = false,
limit: limitArg = field.defaultLimit ?? 10,
page,
sort = field.defaultSort,
@@ -480,6 +481,13 @@ export const traverseFields = ({
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias))
}
if (shouldCount) {
currentArgs.extras[`${columnName}_count`] = sql`${db
.select({ count: count() })
.from(sql`${currentQuery.as(subQueryAlias)}`)
.where(sqlWhere)}`.as(`${columnName}_count`)
}
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`)) as SQLSelect
if (page && limit !== 0) {
@@ -611,6 +619,20 @@ export const traverseFields = ({
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
}).as(subQueryAlias)
if (shouldCount) {
currentArgs.extras[`${columnName}_count`] = sql`${db
.select({
count: count(),
})
.from(
sql`${db
.select(selectFields as any)
.from(newAliasTable)
.where(subQueryWhere)
.as(`${subQueryAlias}_count_subquery`)}`,
)}`.as(`${subQueryAlias}_count`)
}
currentArgs.extras[columnName] = sql`${db
.select({
result: jsonAggBuildObject(adapter, {

View File

@@ -31,9 +31,10 @@ export { buildRawSchema } from './schema/buildRawSchema.js'
export { beginTransaction } from './transactions/beginTransaction.js'
export { commitTransaction } from './transactions/commitTransaction.js'
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
export { updateOne } from './update.js'
export { updateGlobal } from './updateGlobal.js'
export { updateGlobalVersion } from './updateGlobalVersion.js'
export { updateMany } from './updateMany.js'
export { updateOne } from './updateOne.js'
export { updateVersion } from './updateVersion.js'
export { upsertRow } from './upsertRow/index.js'
export { buildCreateMigration } from './utilities/buildCreateMigration.js'

View File

@@ -1,6 +1,7 @@
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
@@ -398,7 +399,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
if (field.type === 'join') {
const { limit = field.defaultLimit ?? 10 } =
const { count, limit = field.defaultLimit ?? 10 } =
joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
// raw hasMany results from SQLite
@@ -407,8 +408,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
let fieldResult:
| { docs: unknown[]; hasNextPage: boolean }
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
| { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }
| Record<string, { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }>
if (Array.isArray(fieldData)) {
if (isLocalized && adapter.payload.config.localization) {
fieldResult = fieldData.reduce(
@@ -449,6 +450,17 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
}
if (count) {
const countPath = `${fieldName}_count`
if (typeof table[countPath] !== 'undefined') {
let value = Number(table[countPath])
if (Number.isNaN(value)) {
value = 0
}
fieldResult.totalDocs = value
}
}
result[field.name] = fieldResult
return result
}
@@ -607,6 +619,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.flattenedFields,
joinQuery,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}`,

View File

@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function updateGlobal<T extends Record<string, unknown>>(
this: DrizzleAdapter,
{ slug, data, req, select }: UpdateGlobalArgs,
{ slug, data, req, select, returning }: UpdateGlobalArgs,
): Promise<T> {
const db = await getTransaction(this, req)
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
@@ -26,8 +26,13 @@ export async function updateGlobal<T extends Record<string, unknown>>(
req,
select,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
result.globalType = slug
return result

View File

@@ -16,7 +16,16 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function updateGlobalVersion<T extends TypeWithID>(
this: DrizzleAdapter,
{ id, global, locale, req, select, versionData, where: whereArg }: UpdateGlobalVersionArgs<T>,
{
id,
global,
locale,
req,
select,
versionData,
where: whereArg,
returning,
}: UpdateGlobalVersionArgs<T>,
) {
const db = await getTransaction(this, req)
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
@@ -49,7 +58,12 @@ export async function updateGlobalVersion<T extends TypeWithID>(
select,
tableName,
where,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -0,0 +1,97 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { UpdateMany } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
import { getTransaction } from './utilities/getTransaction.js'
export const updateMany: UpdateMany = async function updateMany(
this: DrizzleAdapter,
{
collection: collectionSlug,
data,
joins: joinQuery,
locale,
req,
returning,
select,
where: whereToUse,
},
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const { joins, selectFields, where } = buildQuery({
adapter: this,
fields: collection.flattenedFields,
locale,
tableName,
where: whereToUse,
})
let idsToUpdate: (number | string)[] = []
const selectDistinctResult = await selectDistinct({
adapter: this,
db,
joins,
selectFields,
tableName,
where,
})
if (selectDistinctResult?.[0]?.id) {
idsToUpdate = selectDistinctResult?.map((doc) => doc.id)
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
} else if (whereToUse && !joins.length) {
const _db = db as LibSQLDatabase
const table = this.tables[tableName]
const docsToUpdate = await _db
.select({
id: table.id,
})
.from(table)
.where(where)
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
}
if (!idsToUpdate.length) {
return []
}
const results = []
// TODO: We need to batch this to reduce the amount of db calls. This can get very slow if we are updating a lot of rows.
for (const idToUpdate of idsToUpdate) {
const result = await upsertRow({
id: idToUpdate,
adapter: this,
data,
db,
fields: collection.flattenedFields,
ignoreResult: returning === false,
joinQuery,
operation: 'update',
req,
select,
tableName,
})
results.push(result)
}
if (returning === false) {
return null
}
return results
}

View File

@@ -1,10 +1,10 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { UpdateOne } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
@@ -12,7 +12,17 @@ import { getTransaction } from './utilities/getTransaction.js'
export const updateOne: UpdateOne = async function updateOne(
this: DrizzleAdapter,
{ id, collection: collectionSlug, data, joins: joinQuery, locale, req, select, where: whereArg },
{
id,
collection: collectionSlug,
data,
joins: joinQuery,
locale,
req,
select,
where: whereArg,
returning,
},
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
@@ -28,6 +38,7 @@ export const updateOne: UpdateOne = async function updateOne(
where: whereToUse,
})
// selectDistinct will only return if there are joins
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: [{ args: [1], method: 'limit' }],
@@ -40,22 +51,18 @@ export const updateOne: UpdateOne = async function updateOne(
if (selectDistinctResult?.[0]?.id) {
idToUpdate = selectDistinctResult?.[0]?.id
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
} else if (whereArg && !joins.length) {
const findManyArgs = buildFindManyArgs({
adapter: this,
depth: 0,
fields: collection.flattenedFields,
joinQuery: false,
select: {},
tableName,
})
const table = this.tables[tableName]
findManyArgs.where = where
const docToUpdate = await db.query[tableName].findFirst(findManyArgs)
idToUpdate = docToUpdate?.id
const docsToUpdate = await (db as LibSQLDatabase)
.select({
id: table.id,
})
.from(table)
.where(where)
.limit(1)
idToUpdate = docsToUpdate?.[0]?.id
}
const result = await upsertRow({
@@ -69,7 +76,12 @@ export const updateOne: UpdateOne = async function updateOne(
req,
select,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -16,7 +16,16 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function updateVersion<T extends TypeWithID>(
this: DrizzleAdapter,
{ id, collection, locale, req, select, versionData, where: whereArg }: UpdateVersionArgs<T>,
{
id,
collection,
locale,
req,
select,
versionData,
where: whereArg,
returning,
}: UpdateVersionArgs<T>,
) {
const db = await getTransaction(this, req)
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
@@ -47,7 +56,12 @@ export async function updateVersion<T extends TypeWithID>(
select,
tableName,
where,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -428,6 +428,10 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
}
}
if (ignoreResult === 'idOnly') {
return { id: insertedRow.id } as T
}
if (ignoreResult) {
return data as T
}

View File

@@ -12,7 +12,7 @@ type BaseArgs = {
* When true, skips reading the data back from the database and returns the input data
* @default false
*/
ignoreResult?: boolean
ignoreResult?: boolean | 'idOnly'
joinQuery?: JoinQuery
path?: string
req?: Partial<PayloadRequest>

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.24.0",
"version": "3.25.0",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.24.0",
"version": "3.25.0",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.24.0",
"version": "3.25.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.24.0",
"version": "3.25.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
@@ -45,8 +45,8 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.24.0",
"version": "3.25.0",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.24.0",
"version": "3.25.0",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.24.0",
"version": "3.25.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -107,8 +107,8 @@
"@next/eslint-plugin-next": "15.1.5",
"@payloadcms/eslint-config": "workspace:*",
"@types/busboy": "1.5.4",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"esbuild": "0.24.2",

View File

@@ -44,23 +44,26 @@ export const DefaultNavClient: React.FC<{
id = `nav-global-${slug}`
}
const LinkElement = Link || 'a'
const activeCollection =
pathname.startsWith(href) && ['/', undefined].includes(pathname[href.length])
const Label = (
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
)
if (activeCollection) {
return (
<div className={`${baseClass}__link active`} id={id} key={i}>
<div className={`${baseClass}__link-indicator`} />
{Label}
</div>
)
}
return (
<LinkElement
className={[`${baseClass}__link`, activeCollection && `active`]
.filter(Boolean)
.join(' ')}
href={href}
id={id}
key={i}
prefetch={Link ? false : undefined}
>
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
</LinkElement>
<Link className={`${baseClass}__link`} href={href} id={id} key={i} prefetch={false}>
{Label}
</Link>
)
})}
</NavGroup>

View File

@@ -93,36 +93,32 @@
}
}
nav {
a {
position: relative;
padding-block: base(0.125);
padding-inline-start: 0;
padding-inline-end: base(1.5);
display: flex;
text-decoration: none;
&:focus:not(:focus-visible) {
box-shadow: none;
font-weight: 600;
}
&:hover,
&:focus-visible {
text-decoration: underline;
}
&.active {
font-weight: normal;
padding-left: 0;
font-weight: 600;
}
}
}
&__link {
display: flex;
align-items: center;
position: relative;
padding-block: base(0.125);
padding-inline-start: 0;
padding-inline-end: base(1.5);
text-decoration: none;
&:focus:not(:focus-visible) {
box-shadow: none;
font-weight: 600;
}
&.active {
font-weight: normal;
padding-left: 0;
font-weight: 600;
}
}
a.nav__link {
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
&__link-indicator {
@@ -148,7 +144,7 @@
padding: var(--app-header-height) var(--gutter-h) base(2);
}
nav a {
&__link {
font-size: base(0.875);
line-height: base(1.5);
}

View File

@@ -4,8 +4,7 @@ import type { ImportMap, LanguageOptions, SanitizedConfig, ServerFunctionClient
import { rtlLanguages } from '@payloadcms/translations'
import { ProgressBar, RootProvider } from '@payloadcms/ui'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { getPayload, getRequestLanguage, parseCookies } from 'payload'
import { cookies as nextCookies } from 'next/headers.js'
import React from 'react'
import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
@@ -34,16 +33,16 @@ export const RootLayout = async ({
}) => {
checkDependencies()
const config = await configPromise
const headers = await getHeaders()
const cookies = parseCookies(headers)
const languageCode = getRequestLanguage({
config,
const {
cookies,
headers,
})
languageCode,
permissions,
req,
req: {
payload: { config },
},
} = await initReq({ configPromise, importMap, key: 'RootLayout' })
const theme = getRequestTheme({
config,
@@ -51,10 +50,6 @@ export const RootLayout = async ({
headers,
})
const payload = await getPayload({ config, importMap })
const { permissions, req } = await initReq(config)
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
: 'LTR'
@@ -134,11 +129,11 @@ export const RootLayout = async ({
{Array.isArray(config.admin?.components?.providers) &&
config.admin?.components?.providers.length > 0 ? (
<NestProviders
importMap={payload.importMap}
importMap={req.payload.importMap}
providers={config.admin?.components?.providers}
serverProps={{
i18n: req.i18n,
payload,
payload: req.payload,
permissions,
user: req.user,
}}

View File

@@ -13,7 +13,11 @@ import { initReq } from './initReq.js'
export const handleServerFunctions: ServerFunctionHandler = async (args) => {
const { name: fnKey, args: fnArgs, config: configPromise, importMap } = args
const { req } = await initReq(configPromise)
const { req } = await initReq({
configPromise,
importMap,
key: 'RootLayout',
})
const augmentedArgs: Parameters<ServerFunction>[0] = {
...fnArgs,

View File

@@ -1,8 +1,7 @@
import type { InitPageResult, VisibleEntities } from 'payload'
import { headers as getHeaders } from 'next/headers.js'
import { notFound } from 'next/navigation.js'
import { getPayload, isEntityHidden, parseCookies } from 'payload'
import { isEntityHidden } from 'payload'
import * as qs from 'qs-esm'
import type { Args } from './types.js'
@@ -18,31 +17,38 @@ export const initPage = async ({
importMap,
route,
searchParams,
useLayoutReq,
}: Args): Promise<InitPageResult> => {
const headers = await getHeaders()
const payload = await getPayload({ config: configPromise, importMap })
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
const {
cookies,
locale,
permissions,
req,
req: { payload },
} = await initReq({
configPromise,
importMap,
key: useLayoutReq ? 'RootLayout' : 'initPage',
overrides: {
fallbackLocale: false,
req: {
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
},
urlSuffix: `${route}${searchParams ? queryString : ''}`,
},
})
const {
collections,
globals,
routes: { admin: adminRoute },
} = payload.config
const cookies = parseCookies(headers)
const { locale, permissions, req } = await initReq(payload.config, {
fallbackLocale: false,
req: {
headers,
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
},
})
const languageOptions = Object.entries(payload.config.i18n.supportedLanguages || {}).reduce(
(acc, [language, languageConfig]) => {
if (Object.keys(payload.config.i18n.supportedLanguages).includes(language)) {

View File

@@ -20,4 +20,15 @@ export type Args = {
* The search parameters of the current route provided to all pages in Next.js.
*/
searchParams: { [key: string]: string | string[] | undefined }
/**
* If `useLayoutReq` is `true`, this page will use the cached `req` created by the root layout
* instead of creating a new one.
*
* This improves performance for pages that are able to share the same `req` as the root layout,
* as permissions do not need to be re-calculated.
*
* If the page has unique query and url params that need to be part of the `req` object, or if you
* need permissions calculation to respect those you should not use this property.
*/
useLayoutReq?: boolean
}

View File

@@ -1,5 +1,13 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { Locale, PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
import type { AcceptedLanguages, I18n, I18nClient } from '@payloadcms/translations'
import type {
ImportMap,
Locale,
Payload,
PayloadRequest,
SanitizedConfig,
SanitizedPermissions,
User,
} from 'payload'
import { initI18n } from '@payloadcms/translations'
import { headers as getHeaders } from 'next/headers.js'
@@ -11,78 +19,114 @@ import {
getRequestLanguage,
parseCookies,
} from 'payload'
import { cache } from 'react'
import { getRequestLocale } from './getRequestLocale.js'
import { selectiveCache } from './selectiveCache.js'
type Result = {
cookies: Map<string, string>
headers: Awaited<ReturnType<typeof getHeaders>>
languageCode: AcceptedLanguages
locale?: Locale
permissions: SanitizedPermissions
req: PayloadRequest
}
export const initReq = cache(async function (
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
overrides?: Parameters<typeof createLocalReq>[0],
): Promise<Result> {
const config = await configPromise
const payload = await getPayload({ config })
type PartialResult = {
i18n: I18nClient
languageCode: AcceptedLanguages
payload: Payload
responseHeaders: Headers
user: null | User
}
// Create cache instances for different parts of our application
const partialReqCache = selectiveCache<PartialResult>('partialReq')
const reqCache = selectiveCache<Result>('req')
/**
* Initializes a full request object, including the `req` object and access control.
* As access control and getting the request locale is dependent on the current URL and
*/
export const initReq = async function ({
configPromise,
importMap,
key,
overrides,
}: {
configPromise: Promise<SanitizedConfig> | SanitizedConfig
importMap: ImportMap
key: string
overrides?: Parameters<typeof createLocalReq>[0]
}): Promise<Result> {
const headers = await getHeaders()
const cookies = parseCookies(headers)
const languageCode = getRequestLanguage({
config,
cookies,
headers,
})
const partialResult = await partialReqCache.get(async () => {
const config = await configPromise
const payload = await getPayload({ config, importMap })
const languageCode = getRequestLanguage({
config,
cookies,
headers,
})
const i18n: I18nClient = await initI18n({
config: config.i18n,
context: 'client',
language: languageCode,
})
const i18n: I18nClient = await initI18n({
config: config.i18n,
context: 'client',
language: languageCode,
})
const { responseHeaders, user } = await executeAuthStrategies({
headers,
payload,
})
/**
* Cannot simply call `payload.auth` here, as we need the user to get the locale, and we need the locale to get the access results
* I.e. the `payload.auth` function would call `getAccessResults` without a fully-formed `req` object
*/
const { responseHeaders, user } = await executeAuthStrategies({
headers,
payload,
})
return {
i18n,
languageCode,
payload,
responseHeaders,
user,
}
}, 'global')
const { req: reqOverrides, ...optionsOverrides } = overrides || {}
return reqCache.get(async () => {
const { i18n, languageCode, payload, responseHeaders, user } = partialResult
const req = await createLocalReq(
{
req: {
headers,
host: headers.get('host'),
i18n: i18n as I18n,
responseHeaders,
url: `${payload.config.serverURL}`,
user,
...(reqOverrides || {}),
const { req: reqOverrides, ...optionsOverrides } = overrides || {}
const req = await createLocalReq(
{
req: {
headers,
host: headers.get('host'),
i18n: i18n as I18n,
responseHeaders,
user,
...(reqOverrides || {}),
},
...(optionsOverrides || {}),
},
...(optionsOverrides || {}),
},
payload,
)
payload,
)
const locale = await getRequestLocale({
req,
})
const locale = await getRequestLocale({
req,
})
req.locale = locale?.code
req.locale = locale?.code
const permissions = await getAccessResults({
req,
})
const permissions = await getAccessResults({
req,
})
return {
locale,
permissions,
req,
}
})
return {
cookies,
headers,
languageCode,
locale,
permissions,
req,
}
}, key)
}

View File

@@ -0,0 +1,57 @@
import { cache } from 'react'
type CachedValue = object
// Module-scoped cache container that holds all cached, stable containers
// - these may hold the stable value, or a promise to the stable value
const globalCacheContainer: Record<
string,
<TValue extends object = CachedValue>(
...args: unknown[]
) => {
value: null | Promise<TValue> | TValue
}
> = {}
/**
* Creates a selective cache function that provides more control over React's request-level caching behavior.
*
* @param namespace - A namespace to group related cached values
* @returns A function that manages cached values within the specified namespace
*/
export function selectiveCache<TValue extends object = CachedValue>(namespace: string) {
// Create a stable namespace container if it doesn't exist
if (!globalCacheContainer[namespace]) {
globalCacheContainer[namespace] = cache((...args) => ({
value: null,
}))
}
/**
* Gets or creates a cached value for a specific key within the namespace
*
* @param key - The key to identify the cached value
* @param factory - A function that produces the value if not cached
* @returns The cached or newly created value
*/
const getCached = async (factory: () => Promise<TValue>, ...cacheArgs): Promise<TValue> => {
const stableObjectFn = globalCacheContainer[namespace]
const stableObject = stableObjectFn<TValue>(...cacheArgs)
if (
stableObject?.value &&
'then' in stableObject.value &&
typeof stableObject.value?.then === 'function'
) {
return await stableObject.value
}
stableObject.value = factory()
return await stableObject.value
}
return {
get: getCached,
}
}

View File

@@ -1,19 +1,18 @@
import type {
AdminViewServerProps,
ColumnPreference,
ListPreferences,
ListQuery,
ListViewClientProps,
ListViewServerPropsOnly,
Where,
} from 'payload'
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import { isNumber } from 'payload/shared'
import {
type AdminViewServerProps,
type ColumnPreference,
type ListPreferences,
type ListQuery,
type ListViewClientProps,
type ListViewServerPropsOnly,
type Where,
} from 'payload'
import { isNumber, transformColumnsToPreferences } from 'payload/shared'
import React, { Fragment } from 'react'
import { renderListViewSlots } from './renderListViewSlots.js'
@@ -73,16 +72,15 @@ export const renderListView = async (
const query = queryFromArgs || queryFromReq
let columns: ColumnPreference[]
if (query.columns) {
try {
columns = JSON.parse(query?.columns as string) as ColumnPreference[]
} catch (error) {
console.error('Error parsing columns from URL:', error) // eslint-disable-line no-console
}
}
const columns: ColumnPreference[] = transformColumnsToPreferences(
query?.columns as ColumnPreference[] | string,
)
/**
* @todo: find a pattern to avoid setting preferences on hard navigation, i.e. direct links, page refresh, etc.
* This will ensure that prefs are only updated when explicitly set by the user
* This could potentially be done by injecting a `sessionID` into the params and comparing it against a session cookie
*/
const listPreferences = await upsertPreferences<ListPreferences>({
key: `${collectionSlug}-list`,
req,
@@ -216,7 +214,7 @@ export const renderListView = async (
<Fragment>
<HydrateAuthProvider permissions={permissions} />
<ListQueryProvider
columns={columnState.map(({ accessor, active }) => ({ [accessor]: active }))}
columns={transformColumnsToPreferences(columnState)}
data={data}
defaultLimit={limit}
defaultSort={sort}

View File

@@ -58,6 +58,7 @@ export const NotFoundPage = async ({
redirectUnauthenticatedUser: true,
route: formatAdminURL({ adminRoute, path: '/not-found' }),
searchParams,
useLayoutReq: true,
})
const params = await paramsPromise

View File

@@ -14,7 +14,7 @@ export { generateVerifyMetadata } from './meta.js'
export async function Verify({ initPageResult, params, searchParams }: AdminViewServerProps) {
// /:collectionSlug/verify/:token
const [collectionSlug, token] = params.segments
const [collectionSlug, verify, token] = params.segments
const { locale, permissions, req } = initPageResult
const {

View File

@@ -1,6 +1,7 @@
import type {
Document,
DocumentViewServerProps,
Locale,
OptionObject,
SanitizedCollectionPermission,
SanitizedGlobalPermission,
@@ -142,26 +143,29 @@ export async function VersionView(props: DocumentViewServerProps) {
}
}
const selectedLocales: OptionObject[] = []
let selectedLocales: OptionObject[] = []
if (localization) {
let locales: Locale[] = []
if (localeCodesFromParams) {
for (const code of localeCodesFromParams) {
const locale = localization.locales.find((locale) => locale.code === code)
if (locale) {
selectedLocales.push({
label: locale.label,
value: locale.code,
})
if (!locale) {
continue
}
locales.push(locale)
}
} else {
for (const { code, label } of localization.locales) {
selectedLocales.push({
label,
value: code,
})
}
locales = localization.locales
}
if (localization.filterAvailableLocales) {
locales = (await localization.filterAvailableLocales({ locales, req })) || []
}
selectedLocales = locales.map((locale) => ({
label: locale.label,
value: locale.code,
}))
}
const latestVersion =

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.24.0",
"version": "3.25.0",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.24.0",
"version": "3.25.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -74,7 +74,7 @@
"build": "rimraf .dist && rimraf tsconfig.tsbuildinfo && pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:esbuild",
"build:esbuild": "echo skipping esbuild",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "concurrently --group \"tsc --emitDeclarationOnly --outDir dist\" \"tsc-strict\"",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf -g {dist,*.tsbuildinfo}",
"clean:cache": "rimraf node_modules/.cache",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
@@ -96,7 +96,6 @@
"dataloader": "2.2.3",
"deepmerge": "4.3.1",
"file-type": "19.3.0",
"fractional-indexing": "3.2.0",
"get-tsconfig": "4.8.1",
"http-status": "2.1.0",
"image-size": "1.2.0",
@@ -125,15 +124,13 @@
"@types/pluralize": "0.0.33",
"@types/uuid": "10.0.0",
"@types/ws": "^8.5.10",
"concurrently": "9.1.2",
"copyfiles": "2.4.1",
"cross-env": "7.0.3",
"esbuild": "0.24.2",
"graphql-http": "^1.22.0",
"react-datepicker": "7.6.0",
"rimraf": "6.0.1",
"sharp": "0.32.6",
"typescript-strict-plugin": "2.4.4"
"sharp": "0.32.6"
},
"peerDependencies": {
"graphql": "^16.8.1"

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { GenericLanguages, I18n } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'

View File

@@ -1,5 +1,3 @@
// @ts-strict-ignore
import type { I18nClient } from '@payloadcms/translations'
import type { ClientField, Field, FieldTypes, Tab } from '../../fields/config/types.js'

View File

@@ -3,6 +3,7 @@ import type { SanitizedConfig } from '../../config/types.js'
import type { PaginatedDocs } from '../../database/types.js'
import type { CollectionSlug, ColumnPreference } from '../../index.js'
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
import type { ColumnsFromURL } from '../../utilities/transformColumnPreferences.js'
export type DefaultServerFunctionArgs = {
importMap: ImportMap
@@ -38,7 +39,11 @@ export type ServerFunctionHandler = (
) => Promise<unknown>
export type ListQuery = {
columns?: ColumnPreference[]
/*
* This is an of strings, i.e. `['title', '-slug']`
* Use `transformColumnsToPreferences` to convert it back and forth
*/
columns?: ColumnsFromURL
limit?: string
page?: string
/*

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { SanitizedCollectionConfig } from './../collections/config/types.js'
type CookieOptions = {

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import crypto from 'crypto'
const algorithm = 'aes-256-ctr'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { AllOperations, PayloadRequest } from '../types/index.js'
import type { Permissions, SanitizedPermissions } from './types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { CollectionConfig } from '../collections/config/types.js'
import type { Field, TabAsField } from '../fields/config/types.js'
import type { PayloadRequest } from '../types/index.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { Auth } from './types.js'
export const getLoginOptions = (

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import crypto from 'crypto'
import { status as httpStatus } from 'http-status'
import { URL } from 'url'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { CollectionSlug, Payload, RequestContext } from '../../../index.js'
import type { PayloadRequest } from '../../../types/index.js'
import type { Result } from '../forgotPassword.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type {
AuthOperationsFromCollectionSlug,
Collection,

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { decodeJwt } from 'jose'
import type { Collection } from '../../collections/config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import url from 'url'
import type { BeforeOperationHook, Collection } from '../../collections/config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type {

View File

@@ -48,6 +48,7 @@ export const verifyEmailOperation = async (args: Args): Promise<boolean> => {
_verified: true,
},
req,
returning: false,
})
if (shouldCommit) {

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { URL } from 'url'
import type { Collection } from '../collections/config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import crypto from 'crypto'
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { jwtVerify } from 'jose'
import type { Payload, Where } from '../../types/index.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import crypto from 'crypto'
import scmp from 'scmp'

View File

@@ -1,5 +1,3 @@
// @ts-strict-ignore
import type { AdminViewConfig } from '../../admin/views/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { AddToImportMap, Imports, InternalImportMap } from './index.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
/* eslint-disable @typescript-eslint/no-unused-expressions */
import type { PayloadComponent, SanitizedConfig } from '../../config/types.js'
import type { Block, Field, Tab } from '../../fields/config/types.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { PayloadComponent } from '../../config/types.js'
export function parsePayloadComponent(PayloadComponent: PayloadComponent): {

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
/* eslint-disable no-console */
import { Cron } from 'croner'
import minimist from 'minimist'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import nextEnvImport from '@next/env'
import { findUpSync } from '../utilities/findUp.js'

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import type { ParsedArgs } from 'minimist'
import type { SanitizedConfig } from '../config/types.js'

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