Compare commits

...

143 Commits

Author SHA1 Message Date
Sasha
219aebbec0 setup config 2025-04-18 20:00:05 +03:00
Dan Ribbens
df7a3692f7 fix(plugin-search): delete does not also delete the search doc (#12148)
The plugin-search collection uses an `afterDelete` hook to remove search
records from the database. Since a deleted document in postgres causes
cascade updates for the foreign key, the query for the document by
relationship was not returning the record to be deleted.

The solution was to change the delete hook to `beforeDelete` for the
search enabled collections. This way we purge records before the main
document so the search document query can find and delete the record as
expected.

An alternative solution in #9623 would remove the `req` so the delete
query could still find the document, however, this just works outside of
transactions which isn't desirable.

fixes https://github.com/payloadcms/payload/issues/9443
2025-04-18 09:47:36 -04:00
Corey Larson
b750ba4509 fix(ui): reflect default sort in join tables (#12084)
<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->

### What?

This PR ensures defaultSort is reflected in join tables.

### Why?

Currently, default sort is not reflected in the join table state. The
data _is_ sorted correctly, but the table state sort is undefined. This
is mainly an issue for join fields with `orderable: true` because you
can't re-order the table until `order` is the selected sort column.

### How?

Added `defaultSort` prop to the `<ListQueryProvider />` in the
`<RelationshipTable />` and ensured the default state gets set in
`<ListQueryProvider />` when `modifySearchParams` is false.

**Before:**

<img width="1390" alt="Screenshot 2025-04-11 at 2 33 19 AM"
src="https://github.com/user-attachments/assets/4a008d98-d308-4397-a35a-69795e5a6070"
/>

**After:**

<img width="1362" alt="Screenshot 2025-04-11 at 3 04 07 AM"
src="https://github.com/user-attachments/assets/4748e354-36e4-451f-83e8-6f84fe58d5b5"
/>

Fixes #12083

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-04-18 07:10:48 -03:00
Patrik
d55306980e feat: adds beforeDocumentControls slot to allow custom component injection next to document controls (#12104)
### What

This PR introduces a new `beforeDocumentControls` slot to the edit view
of both collections and globals.

It allows injecting one or more custom components next to the document
control buttons (e.g., Save, Publish, Save Draft) in the admin UI —
useful for adding context, additional buttons, or custom UI elements.

#### Usage

##### For collections: 

```
admin: {
  components: {
    edit: {
      beforeDocumentControls: ['/path/to/CustomComponent'],
    },
  },
},
```

##### For globals:

```
admin: {
  components: {
    elements: {
      beforeDocumentControls: ['/path/to/CustomComponent'],
    },
  },
},
```
2025-04-17 15:23:17 -04:00
Patrik
34ea6ec14f feat: adds showSaveDraftButton option to show draft button with autosave enabled (#12150)
This adds a new `showSaveDraftButton` option to the
`versions.drafts.autosave` config for collections and globals.

By default, the "Save as draft" button is hidden when autosave is
enabled. This new option allows the button to remain visible for manual
saves while autosave is active.

Also updates the admin UI logic to conditionally render the button when
this flag is set, and updates the documentation with an example usage.
2025-04-17 14:45:10 -04:00
Elliot DeNolf
17d5168728 chore(release): v3.35.1 [skip ci] 2025-04-17 11:02:39 -04:00
Jessica Chowdhury
ed50a79643 fix(next): missing @payloadcms/next/auth export (#12144)
Follow up to #11900. The `@payloadcms/next/auth` export was missing from
the published package.json because it was excluded from the
`publishConfig` property.
2025-04-17 10:55:11 -04:00
Sasha
0a59707ea0 chore(db-postgres): improve table name length exceeded error message (#12142)
Improves the error message when table name length exceeds 63 characters
with the tip that you can use the `dbName` property.
2025-04-17 13:55:12 +00:00
Elliot DeNolf
bcbb912d50 chore(release): v3.35.0 [skip ci] 2025-04-16 15:52:57 -04:00
Sasha
1c99f46e4f feat: queriable / sortable / useAsTitle virtual fields linked with a relationship field (#11805)
This PR adds an ability to specify a virtual field in this way
```js
{
  slug: 'posts',
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
  ],
},
{
  slug: 'virtual-relations',
  fields: [
    {
      name: 'postTitle',
      type: 'text',
      virtual: 'post.title',
    },
    {
      name: 'post',
      type: 'relationship',
      relationTo: 'posts',
    },
  ],
},
```

Then, every time you query `virtual-relations`, `postTitle` will be
automatically populated (even if using `depth: 0`) on the db level. This
field also, unlike `virtual: true` is available for querying / sorting /
`useAsTitle`.

Also, the field can be deeply nested to 2 or more relationships, for
example:
```
{
  name: 'postCategoryTitle',
  type: 'text',
  virtual: 'post.category.title',
},
```

Where the current collection has `post` - a relationship to `posts`, the
collection `posts` has `category` that's a relationship to `categories`
and finally `categories` has `title`.
2025-04-16 15:46:18 -04:00
Patrik
c877b1ad43 feat: threads operation through field condition function (#12132)
This PR updates the field `condition` function property to include a new
`operation` argument.

The `operation` arg provides a string relating to which operation the
field type is currently executing within.

#### Changes:

- Added `operation: Operation` in the Condition type.
- Updated relevant condition checks to ensure correct parameter usage.
2025-04-16 15:38:53 -04:00
Philipp Schneider
4426625b83 perf(ui): prevent blockType: "$undefined" from being sent through the network (#12131)
Removes `$undefined` strings from being sent through the network when
sending form state requests. When adding new array rows, we assign
`blockType: undefined` which is stringified to `"$undefined"`. This is
unnecessary, as simply not sending this property is equivalent, and this
is only a requirement for blocks. This change will save on request size,
albeit minimal.

| Before | After |
|--|--|
|<img width="1267" alt="Untitled"
src="https://github.com/user-attachments/assets/699f38bd-7db9-4a52-931d-084b8af8530f"
/> | <img width="1285" alt="image"
src="https://github.com/user-attachments/assets/986ecd4c-f22d-4143-ad38-0c5f52439c67"
/> |
2025-04-16 15:03:35 -04:00
Tylan Davis
23628996d0 chore: adjusts ChevronIcon styling to match other icons (#12133)
### What?

Adjusts the `ChevronIcon` component to match the sizing of other icons
in the `ui` package. Also adds various styling adjustments to places
where icons are used.

### Why?

Using the `ChevronIcon` in other elements currently requires different
styling to make it consistent with other icons. This will make it so
that any usage of the any icons is consistent across components.

### How?

Resizes the `ChevronIcon` components and updates styling throughout the
admin panel.
2025-04-16 17:24:10 +00:00
Kristian Djaković
b9832f40e4 docs: fix syntax issue in blocks field (#11855)
<!--

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

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

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

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

-->

### What?

This PR fixes the config example in the block field page.

### Why?

The syntax was incorrect

### How?

Missing object property
2025-04-16 10:27:42 -04:00
Jacob Fletcher
a675c04c99 fix: respects boolean query preset constraints (#12124)
Returning a boolean value from a constraint-level access control
function does nothing. For example:

```ts
{
  label: 'Noone',
  value: 'noone',
  access: () => false,
},
```

This is because we were only handling query objects, disregarding any
boolean values. The fix is to check if the query is a boolean, and if
so, format a query object to return.
2025-04-16 09:16:43 -04:00
James Mikrut
e79b20363e fix: ensures cors headers are run against custom endpoints (#12091)
Restores goal of #10597 and reverts #10718

This is a more surgical way of adding CORS headers to custom endpoints
2025-04-16 09:15:39 -04:00
Jacob Fletcher
21599b87f5 fix(ui): stale paths on custom components within rows (#11973)
When server rendering custom components within form state, those
components receive a path that is correct at render time, but
potentially stale after manipulating array and blocks rows. This causes
the field to briefly render incorrect values while the form state
request is in flight.

The reason for this is that paths are passed as a prop statically into
those components. Then when we manipulate rows, form state is modified,
potentially changing field paths. The component's `path` prop, however,
hasn't changed. This means it temporarily points to the wrong field in
form state, rendering the data of another row until the server responds
with a freshly rendered component.

This is not an issue with default Payload fields as they are rendered on
the client and can be passed dynamic props.

This is only an issue within custom server components, including rich
text fields which are treated as custom components. Since they are
rendered on the server and passed to the client, props are inaccessible
after render.

The fix for this is to provide paths dynamically through context. This
way as we make changes to form state, there is a mechanism in which
server components can receive the updated path without waiting on its
props to update.
2025-04-15 15:23:51 -04:00
Dan Ribbens
e90ff72b37 fix: reordering draft documents causes data loss (#12109)
Re-ordering documents with drafts uses `payload.update()` with `select:
{ id: true }` and that causes draft versions of those docs to be updated
without any data. I've removed the `select` optimization to prevent data
loss.

Fixes #12097
2025-04-15 12:09:55 -04:00
Tobias Odendahl
babf4f965d fix(richtext-lexical): allow to indent children even if their parents are not indentable (#12042)
### What?
Allows to indent children in richtext-lexical if the parent of that
child is not indentable. Changes the behavior introduced in
https://github.com/payloadcms/payload/pull/11739

### Why?
If there is a document structure with e.g. `tableNode > list > listItem`
and indentation of `tableNode` is disabled, it should still be possible
to indent the list items.

### How?
Disable the indent button only if indentation of one of the selected
nodes itself is disabled.
2025-04-15 09:02:41 -03:00
Dan Ribbens
6572bf4ae1 fix(db-sqlite): text field converts to floating point number (#12107)
### What?

Converts numbers passed to a text field to avoid the database/drizzle
from converting it incorrectly.

### Why?

If you have a hook that passes a value to another field you can
experience this problem where drizzle converts a number value for a text
field to a floating point number in sqlite for example.

### How?

Adds logic to `transform/write/traverseFields.ts` to cast text field
values to string.
2025-04-14 17:05:08 -04:00
Adler Weber
da7be35a15 feat(db-postgres): dependency inject pg to allow Sentry instrumentation (#11478)
### What?

I changed the interface of `@payloadcms/db-postgres` to allow a user to
(optionally) inject their own `pg` module.

### Why?

I noticed that `@payloadcms/sentry-plugin` wasn't instrumenting
Payload's database queries through the [local payload
API](https://payloadcms.com/docs/local-api/overview):


![image](https://github.com/user-attachments/assets/425691f5-cf7e-4625-89e0-6d07dda9cbc0)

This is because Sentry applies a patch to the `pg` driver on import. For
whatever reason, it doesn't patch `pg` when imported by dependencies
(e.g. `@payloadcms/db-postgres`). After applying this fix, I can see the
underlying query traces!


![image](https://github.com/user-attachments/assets/fb6f9aef-13d9-41b1-b4cc-36c565d15930)
2025-04-14 15:27:53 -04:00
Sam Wheeler
55d00e2b1d feat(ui): add option for rendering the relationship field as list drawer (#11553)
### What?

This PR adds the ability to use the ListDrawer component for selecting
related collections for the relationship field instead of the default
drop down interface. This exposes the advanced filtering options that
the list view provides and provides a good interface for searching for
the correct relationship when the workflows may be more complicated.
I've added an additional "selectionType" prop to the relationship field
admin config that defaults to "dropdown" for compatability with the
existing implementation but "drawer" can be passed in as well which
enables using the ListDrawer for selecting related components.

### Why?

Adding the ability to search through the list view enables advanced
workflows or handles edge cases when just using the useAsTitle may not
be informative enough to find the related record that the user wants.
For example, if we have a collection of oscars nominations and are
trying to relate the nomination to the person who recieved the
nomination there may be multiple actors with the same name (Michelle
Williams, for example:
[https://www.imdb.com/name/nm0931329/](https://www.imdb.com/name/nm0931329/),
[https://www.imdb.com/name/nm0931332/](https://www.imdb.com/name/nm0931332/)).
It would be hard to search through the current dropdown ui to choose the
correct person, but in the list view the user could use other fields to
identify the correct person such as an imdb id, description, or anything
else they have in the collection for that person. Other advanced
workflows could be if there are multiple versions of a record in a
collection and the user wants to select the most recent one or just
anything where the user needs to see more details about the record that
they are setting up the relationship to.

### How?

This implementation just re-uses the useListDrawer hook and the
ListDrawer component so the code changes are pretty minimal. The main
change is a new onListSelect handler that gets passed into the
ListDrawer and handles updating the value in the field when a record is
selected in the ListDrawer.

There were also a two things that I didn't implement as they would
require broader code changes 1) Bulk select from the ListDrawer when a
relationship is hasMany - when using bulkSelect in the list drawer the
relatedCollection doesn't get returned so this doesn't work for
polymorphic relationships. Updating this would involve changing the
useListDrawer hook 2) Hide values that are already selected from the
ListDrawer - potentially possible by modifying the filterOptions and
passing in an additional filter but honestly it may not be desired
behaviour to hide values from the ListDrawer as this could be confusing
for the user if they don't see records that they are expected to see
(maybe if anything make them unselectable and indicate that they are
disabled). Currently if an already selected value gets selected the
selected value gets replaced by the new value



https://github.com/user-attachments/assets/fee164da-4270-4612-9304-73ccf34ccf69

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-04-14 14:37:09 -04:00
AoiYamada
5b554e5256 fix(templates): missing default value in select field (#11715)
### What?
The default value is hardcoded instead of respecting the value filled in
the form setting

Fixes #
pass it down from props

Co-authored-by: Pan <kpkong@hk01.com>
2025-04-14 12:38:40 -04:00
Edgar Guerra
85e6edf21e fix(translations): add missing Catalan translations (#10682)
### What?
There are some missing translations in Catalan, both related to the word
Collections, which in Catalan is "Col·leccions".
### Why?
To contribute to the Catalan language as a developer and native speaker
;)
### How?
Updated the wording in the `ca.ts` translations object, also removed
`catalan` from `not implemented languages` comment
2025-04-14 11:21:27 -04:00
Tobias Odendahl
b354d00aa4 feat(ui): use defaultDepth in API view (#11950)
### What?
Respects the defaultDepth setting in the admin UI API view.
 
### Why?
The current default is hardcoded to `1` with no configuration option.
This can lead to performance issues on documents with a lot of related
large documents. Having the ability to define a different default can
prevent this issue.

### How?
Set the depth in the API view to `config.defaultDepth` as default.

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-04-14 10:39:04 -04:00
Jessica Chowdhury
c661d33b13 docs: minor formatting tweaks to server function examples (#12102)
Misc formatting tweaks for server function examples in docs.
2025-04-14 13:05:16 +01:00
Jessica Chowdhury
6b349378e0 feat: adds and exports reusable auth server functions (#11900)
### What
Adds exportable server functions for `login`, `logout` and `refresh`
that are fully typed and ready to use.

### Why
Creating server functions for these auth operations require the
developer to manually set and handle the cookies / auth JWT. This can be
a complex and involved process - instead we want to provide an option
that will handle the cookies internally and simplify the process for the
user.

### How
Three re-usable functions can be exported from
`@payload/next/server-functions`:
- login
- logout
- refresh

Examples of how to use these functions will be added to the docs
shortly, along with more in-depth info on server functions.
2025-04-14 09:47:08 +01:00
Paul
39462bc6b9 chore: assign available port to env variable in dev suite (#12092)
Previously when the port number was bumped up (eg `3001`) in our dev
suite, HMR wouldn't work because it couldn't reliably read the new used
port and it would default to `3000`.

This assigns it properly to the env var and fixes that issues so HMR in
our dev suite works on other ports too.

Testing steps:
- Have a local instance of dev suite running already on port 3000
- New repo run dev, it will bump to `3001`
- Make any config change and you will see that HMR does not work without
this fix
2025-04-11 19:24:24 +00:00
Paul
3a7cd717b2 fix(ui): issue with schedule publish disappearing on autosave collections (#12078)
Fixes an issue where an autosave being triggered would turn off the
ability to schedule a publish. This happened because we check against
`modified` on the form but with autosave modified is always true.

Now we make an exception for autosave enabled collections when checking
the modified state.
2025-04-11 10:43:40 -04:00
Slava Nossar
3287f7062f fix(ui): use route.api from config in OrderableTable (#12081)
### What?
`OrderableTable` doesn't respect a user-sepcified `routes.api` value and
instead uses the default `/api`

### Why?
See #12080

### How?
Gets `config` via `useConfig`, and uses `config.routes.api` in the
`fetch` for reordering.

Fixes #12080
2025-04-11 06:03:39 -03:00
Corey Larson
a9eca3a785 fix: correct typo in error message and remove console.log (#12082)
### What?

This PR corrects a typo in an error message and removes a console.log from the `orderBeforeChangeHook` hook.

### Why?

An error message contains a typo, and every time I reorder an orderable collection, `do not enter` gets logged.

<img width="153" alt="Screenshot 2025-04-11 at 1 11 29 AM" src="https://github.com/user-attachments/assets/13ae106b-0bb9-4421-9083-330d3b6f356d" />
2025-04-11 08:42:39 +00:00
alexrah
71e3c7839b fix(db-postgres): use correct export path for codegen in createSchemaGenerator (#12043)
following changes made by Commit a6f7ef8

> feat(db-*): export types from main export (#11914)
In 3.0, we made the decision to export all types from the main package
export (e.g. `payload/types` => `payload`). This improves type
discoverability by IDEs and simplifies importing types.

> This PR does the same for our db adapters, which still have a separate
`/types` subpath export. While those are kept for
backwards-compatibility, we can remove them in 4.0.


a6f7ef837a


the script responsible for generating file generated-schema.ts was not
updated to reflect this change in export paths

drizzle/src/utilities/createSchemaGenerator.ts

CURRENT 
```typescript
    const finalDeclaration = `
declare module '${this.packageName}/types' {
  export interface GeneratedDatabaseSchema {
    schema: DatabaseSchema
  }
}
```

AFTER THIS PULL REQUEST
```typescript
    const finalDeclaration = `
declare module '${this.packageName}' {
  export interface GeneratedDatabaseSchema {
    schema: DatabaseSchema
  }
}
```

this pull request fixes the generation of generated-schema.ts avoiding
errors while building for production with command
```bash
npm run build
```
![Screenshot 2025-04-08 at 17 00
11](https://github.com/user-attachments/assets/203de476-0f8f-4d65-90e6-58c50bd3e2a6)
2025-04-11 10:58:55 +03:00
Germán Jabloñski
a66f90ebb6 chore: separate Lexical tests into dedicated suite (#12047)
Lexical tests comprise almost half of the collections in the fields
suite, and are starting to become complex to manage.

They are sometimes related to other auxiliary collections, so
refactoring one test sometimes breaks another, seemingly unrelated one.

In addition, the fields suite is very large, taking a long time to
compile. This will make it faster.

Some ideas for future refactorings:
- 3 main collections: defaultFeatures, fully featured, and legacy.
Legacy is the current one that has multiple editors and could later be
migrated to the first two.
- Avoid collections with more than 1 editor.
- Create reseed buttons to restore the editor to certain states, to
avoid a proliferation of collections and documents.
- Reduce the complexity of the three auxiliary collections (text, array,
upload), which are rarely or never used and have many fields designed
for tests in the fields suite.
2025-04-10 20:47:26 -03:00
Elliot DeNolf
272914c818 chore(release): v3.34.0 [skip ci] 2025-04-10 15:38:35 -04:00
Sasha
466dcd7189 feat: support where querying by join fields (#12075)
### What?
This PR adds support for `where` querying by the join field (don't
confuse with `where` querying of related docs via `joins.where`)

Previously, this didn't work:
```
const categories = await payload.find({
  collection: 'categories',
  where: { 'relatedPosts.title': { equals: 'my-title' } },
})
```

### Why?
This is crucial for bi-directional relationships, can be used for access
control.

### How?
Implements `where` handling for join fields the same as we do for
relationships. In MongoDB it's not as efficient as it can be, the old PR
that improves it and can be updated later is here
https://github.com/payloadcms/payload/pull/8858

Fixes https://github.com/payloadcms/payload/discussions/9683
2025-04-10 15:30:40 -04:00
Germán Jabloñski
a72fa869f3 chore(plugin-seo): enable TypeScript strict (#11933) 2025-04-10 15:12:44 -04:00
Paul
3523c2c6a6 templates: update readme on blank template and blank template variations for Vercel (#12070)
Updated the readmes on our blank template so it's closer to what we have
on the website
template.

Updated the Vercel variation ones as well because those are used
directly for the Vercel marketplace.
2025-04-10 19:11:28 +01:00
Patrik
112e081d8f fix(ui): ensure file field is only serialized at top-level for upload-enabled collections (#12074)
This fixes an issue where fields with the name `file` was being
serialized as a top-level field in multipart form data even when the
collection was not upload-enabled. This caused the value of `file` (when
used as a regular field like a text, array, etc.) to be stripped from
the `_payload`.

- Updated `createFormData` to only delete `data.file` and serialize it
at the top level if `docConfig.upload` is defined.
- This prevents unintended loss of `file` field values for non-upload
collections.

The `file` field now remains safely nested in `_payload` unless it's
part of an upload-enabled collection.
2025-04-10 17:37:10 +00:00
Paul
eab9770315 feat: add support for time format config on scheduled publish (#12073)
This PR adds a new `SchedulePublish` config type on our schedulePublish
configuration in versions from being just boolean.

Two new options are supported:
- `timeFormat` which controls the formatting of the time slots, allowing
users to change from a 12-hour clock to a 24-hour clock (default to 12
hour)
- `timeIntervals` which controls the generated time slots (default 5)

Example configuration:

```
versions: {
  drafts: {
    schedulePublish: {
      timeFormat: 'HH:mm',
      timeIntervals: 5,
    },
  },
},
```
2025-04-10 18:22:21 +01:00
Jacob Fletcher
4d7c1d45fa fix(ui): form state race conditions (#12026)
Fixes form state race conditions. Modifying state while a request is in
flight or while the response is being processed could result in those
changes being overridden.

This was happening for a few reasons:

1. Our merge logic was incorrect. We were disregarding local changes to
state that may have occurred while form state requests are pending. This
was because we were iterating over local state, then while building up
new state, we were ignoring any fields that did not exist in the server
response, like this:
    
    ```ts
    for (const [path, newFieldState] of Object.entries(existingState)) {
    
      if (!incomingState[path]) {
        continue
      }
      
      // ...
    }
    ```

To fix this, we need to use local state as the source of truth. Then
when the server state arrives, we need to iterate over _that_. If a
field matches in local state, merge in any new properties. This will
ensure all changes to the underlying state are preserved, including any
potential addition or deletions.
    
However, this logic breaks down if the server might have created _new_
fields, like when populating array rows. This means they, too, would be
ignored. To get around this, there is a new `addedByServer` property
that flags new fields to ensure they are kept.
    
This new merge strategy also saves an additional loop over form state.
    
1. We were merging form state based on a mutable ref. This meant that
changes made within one action cause concurrent actions to have dirty
reads. The fix for this is to merge in an isolated manner by copying
state. This will remove any object references. It is generally not good
practice to mutate state without setting it, anyways, as this causes
mismatches between what is rendered and what is in memory.
    
1. We were merging server form state directly within an effect, then
replacing state entirely. This meant that if another action took place
at the exact moment in time _after_ merge but _before_ dispatch, the
results of that other action would be completely overridden. The fix for
this is to perform the merge within the reducer itself. This will ensure
that we are working with a trustworthy snapshot of state at the exact
moment in time that the action was invoked, and that React can properly
queue the event within its lifecycle.
2025-04-10 12:11:54 -04:00
Paul
37bfc63da2 chore(deps): bump image-size to 2.0.2 version (#12063)
Bumps our `image-size` dependency to 2.0.2 which includes the [DDOS
fix](https://github.com/payloadcms/payload/pull/12040) previously
released.

The [2.0](https://github.com/image-size/image-size/releases/tag/v2.0.0)
of this library comes with some benefits such as no dependencies and
improved performance.
2025-04-10 14:29:44 +01:00
Patrik
18ff9cbdb1 fix(ui): adds multi select inputs for text fields in where builder (#12054)
### What?

The `in` & `not_in` operators were not properly working for `text`
fields as this operator requires an array of values for it's input.

### How?

Conditionally renders a multi select input for `text` fields when
filtering by `in` & `not_in` operators.
2025-04-10 08:54:50 -04:00
Germán Jabloñski
ae9e5e19ad ci: add sort and hooks suites to the e2e tests matrix (#12023)
Trying to understand why bug #12002 arose, I found that both the `sort`
and `hooks` test suites are not running in CI.

I'm adding those 2 suites to the array, though later we should find a
way to automate this so it doesn't happen again. Manually rewriting all
test suites in the GitHub action is error-prone. It's very easy to
forget to add it when creating a new test suite
2025-04-10 09:51:24 -03:00
Sasha
7aa3c5ea6b fix: cannot define a join field when the target relationship is nested to a second or higher tab (#12041)
Fixes https://github.com/payloadcms/payload/issues/11720
2025-04-10 15:36:03 +03:00
Jessica Chowdhury
a0fb3353c6 fix: image previews getting stuck in list view when paginating (#12062)
### What?
In the List View, row data related to images and relationships gets
stuck when you go from one page to another.

### Why?
The `key` we are providing is not unique and not triggering the DOM to
update.

### How?
Uses the `row id` as a unique key prop to each table row to ensure
proper re-rendering of rows during pagination.

#### Testing
Adds e2e test to `upload` test suite. You can recreate the issue using
the `upload` test suite and new `list view preview` collection.
2025-04-10 13:18:10 +01:00
Philipp Schneider
101f7658f7 perf(richtext-lexical): debounce field onChange handler (#12046)
On devices without a top-notch CPU, typing in the rich text editor is
laggy even in the very basic community test suite's "Post" collection.
Lags can be up to multiple seconds. This lag can be reproduced by e.g.
throttling the CPU by 6x on a MacBook Pro with M1 Pro chip and 32GB of
RAM. Typing at regular speed already stutters, and the Chromium
performance monitor shows 100% peak CPU utilization. Under the same
circumstances, the Lexical rich text editor on
https://playground.lexical.dev/ does not exhibit the same laggy UI
reactions.

The issue was narrowed down to the editor state serialization that was
so far executed on every change in `Field.tsx` and utilizing more than 1
frame's worth of CPU time.

This PR attempts to address the issue by asking the browser to queue the
work in moments where it doesn't interfere with UI responsiveness, via
`requestIdleCallback`.

To verify this change, simulate a slow CPU by setting `CPU: 6x slowdown`
in the Chromium `Performance` Dev Tool panel, and then type into the
community test suite's example post's rich text field.

I did not collect exhaustive benchmarks, since numbers are system
specific and the impact of the code change is simple to verify.

Demos:

Before, whole words are not appearing while typing, but then appear all
at once, INP is 6s, and CPU at 100% basically the whole interaction
time:


https://github.com/user-attachments/assets/535653d5-c9e6-4189-a0e0-f71d39c43c31

After: Most letters appear without delay, individual letters can be
slightly delayed, but INP is much more reasonable 350ms, and CPU has
enough bandwidth to drop below 100% utilization:


https://github.com/user-attachments/assets/e627bf50-b441-41de-b3a3-7ba5443ff049

⬆️ This recording is from an earlier solution attempt with 500ms
debouncing. The current approach with `requestIdleCallback` increases
CPU usage back to a close 100%, but the INP is further reduced to 2xxms
on my machine, and the perceived UI laggyness is comparable to this
recording.

---

This PR only addresses the rich text editor, because that's where the
performance was a severe usability deal-breaker for real world usage.
Presumably other input fields where users trigger a lot of change events
in succession such as text, textarea, number, and JSON fields might also
benefit from similar debouncing.
2025-04-10 08:41:37 -03:00
Jarrod Flesch
9853f27667 fix(ui): orderable table rendering (#12066)
Adds components used in the renderTable component to the client exports.
2025-04-09 23:48:45 -04:00
Alessio Gravili
e0046bba59 chore(deps): bump next.js to 15.3.0 and related dependencies (#12067)
This unblocks https://github.com/payloadcms/payload/pull/11376 and
guarantees support for Next.js 15.3.0
2025-04-09 21:42:45 +00:00
Alessio Gravili
f1d9b44161 fix(richtext-lexical): diff component css was not included in css bundle (#12028)
Currently, the lexical version diff component is completely unstyled, as
the scss was never included in our css bundle. This PR ensures that the
diff component scss is included in our css bundle
2025-04-09 18:32:21 +00:00
Patrik
09916ad18e fix(ui): adds multi select inputs for number fields in where builder (#12053)
### What?

The `in` & `not_in` operators were not properly working for `number`
fields as this operator requires an array of values for it's input.

### How?

Conditionally renders a multi select input for `number` fields when
filtering by `in` & `not_in` operators.
2025-04-09 13:26:18 -04:00
Jessica Chowdhury
a90ae9d42b docs: formatting tweaks for local api docs (#12064)
More formatting cleanup for new Local API / server function docs.
2025-04-09 17:01:29 +01:00
Tylan Davis
d19412f62d docs: adjust formatting on Local API - Server Functions documentation (#12058)
### What?

Adjusts markdown formatting on Local API - Server Functions
documentation

### Why?

Some unnecessary characters and duplicate headline values causing issues
on website frontend.

### How?

Removes unnecessary characters and adds unique anchor tags for duplicate
headlines.
2025-04-09 09:27:51 -04:00
Jacob Fletcher
bd557a97d5 test: optimistic form state rows (#12055)
Adds tests for #11961.
2025-04-08 20:56:24 -06:00
Germán Jabloñski
97e2e77ff4 chore: run dev:generate-types (#11994) 2025-04-08 17:25:29 -03:00
Paul
acae547ddf chore(deps): bump image-size package for security update (#12040)
[v1.2.1](https://github.com/image-size/image-size/releases/tag/v1.2.1)
releases a security patch for the `image-size` package
2025-04-08 13:33:42 -04:00
Jessica Chowdhury
ec34e64261 fix(ui): resets value in where builder when operator changes (#11136)
### What?
The list filters in the collection view allows invalid queries. If you
enter a value and then change operator, the value will remain even if it
doesn't pass the new value field validation, and an error is thrown.

### Why?
The value isn't reset or revalidated on operator change. It is reset on
field change.

### How?
Resets the value field when the operator changes.

Fixes #10648
2025-04-08 14:52:11 +01:00
Jessica Chowdhury
f079eced8a fix: array minRow validation should not show when non-required with no rows (#12037)
### What?
UI only issue: An array row with `required: false` and `minRows: x` was
displaying an error banner - this should only happen if one or more rows
are present.

The validation is not affected, the document still saved as expected,
but the error should not be inaccurately displayed.

### Why?
The logic for displaying the `minRow` validation error was `rows.length
> minRows` and it needs to be `rows.length > 1 && rows.length > minRows`

### How?
Updates the UI logic.

Fixes #12010
2025-04-08 13:47:29 +00:00
Jessica Chowdhury
b809c98966 docs: adds server function and access control sections to local API docs (#11902)
### What?
Adds 2 new topics to our Local API docs:
- Using server functions with local API ops
- Respecting access control

Will also be updating the server function docs with `reusable server
functions` once https://github.com/payloadcms/payload/pull/11900 is
merged.
2025-04-08 10:44:40 +01:00
Sasha
b9ffbc6994 fix: querying by polymorphic join field relationTo with overrideAccess: false (#11999)
Previously, querying by polymorphic joins `relationTo` with
`overrideAccess: false` caused an error:
```
QueryError: The following paths cannot be queried: relationTo
```

As this field actually doesn't exist in the schema. Now, under condition
that the query comes from a polymorphic join we skip checking
`relationTo` field access.
2025-04-07 20:19:43 +00:00
Sasha
09782be0e0 fix(db-postgres): long array field table aliases cause error even when dbName is used (#11995)
Fixes https://github.com/payloadcms/payload/issues/11975

Previously, this configuration was causing errors in postgres due to
long names, even though `dbName` is used:
```
{
  slug: 'aliases',
  fields: [
    {
      name: 'thisIsALongFieldNameThatWillCauseAPostgresErrorEvenThoughWeSetAShorterDBName',
      dbName: 'shortname',
      type: 'array',
      fields: [
        {
          name: 'nested_field_1',
          type: 'array',
          dbName: 'short_nested_1',
          fields: [],
        },
        {
          name: 'nested_field_2',
          type: 'text',
        },
      ],
    },
  ],
},
```

This is because we were generating Drizzle relation name (for arrays)
always based on the field path and internally, drizzle uses this name
for aliasing. Now, if `dbName` is present, we use `_{dbName}` instead
for the relation name.
2025-04-07 20:12:43 +00:00
Paul
b270901fa6 chore: add logging templates script and fix engines for pnpm v10 (#12021)
- Fixes issues with using pnpm v10 in some templates by allowing `^10`
in engines as well
- Added logging to the template generation script so we can debug the
latest version being pulled by CI
2025-04-07 15:13:54 -04:00
Patrik
c7b14bd44d fix(ui): upload edits handling for bulk uploads (#12001)
### What?

This PR addresses a bug where image edits (crop, focal point, etc.) were
not persisting correctly in bulk uploads due to shared state logic with
single uploads.

### How?

- The `Upload` component now receives `uploadEdits`, `resetUploadEdits`,
and `updateUploadEdits` as props.
- `Upload_v4` was introduced to encapsulate the actual upload logic,
making it easier to reuse and test.
- The `AddingFilesView` and `EditForm` components are responsible for
injecting the correct `uploadEdits` state, depending on context.
- Avoided unnecessary `useFormsManager` usage in `Upload`.

Fixes #11868
2025-04-07 14:06:39 -04:00
Patrik
83319be752 docs: clarify file upload example with _payload & field explanation (#12025)
### What?

This PR updates the `Uploading Files` section in the `Uploads` docs to:

- Use `_payload` in the file upload example, which is required for
non-file fields to be parsed correctly by Payload.
- Add a clear comment explaining that the fields inside `_payload`
should match the schema of the upload-enabled collection.

### Why?

These changes aim to reduce confusion when uploading files via the REST
API.

Fixes #11681
2025-04-07 14:06:03 -04:00
Said Akhrarov
77210251f4 fix(ui): prefer adminThumbnail even if file is non-image (#11948)
### What?

This PR relaxes the mimeType checks in the thumbnail and file cell
components to accommodate an `adminThumbnail` even if the file is a
non-image. This is useful when, for example, using an `adminThumbnail`
function to retrieve or generate thumbnails for files that are
non-images such as videos.

### Why?
To prioritize an admin thumbnail if/when available on file cells and
upload field thumbnails in both edit and list views.

### How?

By relaxing the mimeType checks in the `Thumbnail` component and instead
lifting that responsibility on the caller of this component. Some of
these checks were not needed as the best-fit helper utility function
will automatically select the thumbnailURL if available or revert to the
original url if no best-fit is found.

Demo of admin thumbnail being loaded on non-image while still selecting
best-fit size for images:

![chrome_2025-04-01_18-56-25](https://github.com/user-attachments/assets/befd3647-92c5-45c6-90e2-87459bca8bea)
2025-04-07 13:43:25 -04:00
Jacob Fletcher
750210fabe test: temp skip blocks e2e (#11988) 2025-04-07 11:15:08 -04:00
Elliot DeNolf
6d831475a0 templates: bump for v3.33.0 (#12003)
Manual bump of templates. Possible issue from #11992
2025-04-07 11:46:46 +01:00
Said Akhrarov
e109491dbe docs: fix and normalize links (#11993)
<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes a few links around the docs. It also normalizes some links
to use lowercase link-to sections.

### Why?
To send users to the correct location in the docs.

### How?
Changes to a few files in `docs/`
2025-04-06 01:13:56 +01:00
Omar
dee9abd5c1 docs: fix a typo (#12012)
Fix a typo

<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->
2025-04-06 00:13:03 +00:00
zy1p
5c54d9a567 docs: fix markdown link (#12000)
### What?
<img width="749" alt="image"
src="https://github.com/user-attachments/assets/a9b6243d-2c50-48bc-a1a1-6a163949ec4a"
/>

Link not showing properly.
Check
https://payloadcms.com/docs/getting-started/installation#2-copy-payload-files-into-your-nextjs-app-folder

<img width="714" alt="image"
src="https://github.com/user-attachments/assets/d1c77d5f-ed3a-4b92-94b6-86694ae7668e"
/>

SQLite Adapter could be added to this section
Check
https://payloadcms.com/docs/getting-started/installation#1-install-the-relevant-packages

### Why?
Wrong syntax

### How?
* fix markdown link
* add section for install sqlite adapter
2025-04-04 20:20:56 +00:00
Elliot DeNolf
36e7c59b4e chore(release): v3.33.0 [skip ci] 2025-04-04 14:52:55 -04:00
Dan Ribbens
9adbbde9a8 fix: postgres null value breaks orderable hook (#11997)
When postgres is used and orderable is enabled, payload cannot update
the docs to set the order correctly. This is because the sort on
postgres pushes `null` values to the top causing unique constraints to
error when two documents are updated to the same _order value.
2025-04-04 14:31:34 -04:00
Sasha
8ad22eb1c0 fix: allow custom password field when using disableLocalStrategy: true (#11893)
Fixes https://github.com/payloadcms/payload/issues/11888

Previously, if you had `disableLocalStategy: true` and a custom
`password` field, Payload would still control it in `update.ts` by
deleting. Now, we don't do that in this case, unless we have
`disableLocalStetegy.enableFields: true`.
2025-04-04 20:52:10 +03:00
Paul
b76844dac9 templates: set packageManager pnpm version for vercel templates (#11992)
There have been issues with deploying our templates to Vercel when we
rely on `engines.pnpm` configuration.

Vercel's deployments work best when we specify a `packageManager` in
`package.json` since we ship our templates without lockfiles that would
help Vercel determine the right package manager to use.

This PR adjusts the script so that it adds a `packageManager` with the
latest version of `pnpm` to our Vercel templates and removes the
`engines.pnpm` only for those variants.
2025-04-04 18:30:04 +01:00
Alessio Gravili
f7ed8e90e1 docs: fix invalid markdown (#11996) 2025-04-04 12:41:54 -04:00
Tony Tkachenko
e6aad5adfc docs: add missing comma (#11976)
Add missing comma
2025-04-04 00:04:32 +00:00
Sasha
4ebd3ce668 fix(db-postgres): deleteOne fails when the where query does not resolve to any document (#11632)
Previously, if you called `payload.db.deleteOne` with a `where` query
that does not resolve to anything, an error would be occurred.
2025-04-04 00:46:31 +03:00
James
fae113b799 chore: fix flake 2025-04-03 17:06:35 -04:00
Jacob Fletcher
e87521a376 perf(ui): significantly optimize form state component rendering, up to 96% smaller and 75% faster (#11946)
Significantly optimizes the component rendering strategy within the form
state endpoint by precisely rendering only the fields that require it.
This cuts down on server processing and network response sizes when
invoking form state requests **that manipulate array and block rows
which contain server components**, such as rich text fields, custom row
labels, etc. (results listed below).

Here's a breakdown of the issue:

Previously, when manipulating array and block fields, _all_ rows would
render any server components that might exist within them, including
rich text fields. This means that subsequent changes to these fields
would potentially _re-render_ those same components even if they don't
require it.

For example, if you have an array field with a rich text field within
it, adding the first row would cause the rich text field to render,
which is expected. However, when you add a second row, the rich text
field within the first row would render again unnecessarily along with
the new row.

This is especially noticeable for fields with many rows, where every
single row processes its server components and returns RSC data. And
this does not only affect nested rich text fields, but any custom
component defined on the field level, as these are handled in the same
way.

The reason this was necessary in the first place was to ensure that the
server components receive the proper data when they are rendered, such
as the row index and the row's data. Changing one of these rows could
cause the server component to receive the wrong data if it was not
freshly rendered.

While this is still a requirement that rows receive up-to-date props, it
is no longer necessary to render everything.

Here's a breakdown of the actual fix:

This change ensures that only the fields that are actually being
manipulated will be rendered, rather than all rows. The existing rows
will remain in memory on the client, while the newly rendered components
will return from the server. For example, if you add a new row to an
array field, only the new row will render its server components.

To do this, we send the path of the field that is being manipulated to
the server. The server can then use this path to determine for itself
which fields have already been rendered and which ones need required
rendering.

## Results

The following results were gathered by booting up the `form-state` test
suite and seeding 100 array rows, each containing a rich text field. To
invoke a form state request, we navigate to a document within the
"posts" collection, then add a new array row to the list. The result is
then saved to the file system for comparison.

| Test Suite | Collection | Number of Rows | Before | After | Percentage
Change |
|------|------|---------|--------|--------|--------|
| `form-state` | `posts` | 101 | 1.9MB / 266ms | 80KB / 70ms | ~96%
smaller / ~75% faster |

---------

Co-authored-by: James <james@trbl.design>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
2025-04-03 12:27:14 -04:00
Jacob Fletcher
8880d705e3 fix(ui): optimistic rows disappear while form state requests are pending (#11961)
When manipulating array and blocks rows on slow networks, rows can
sometimes disappear and then reappear as requests in the queue arrive.

Consider this scenario:

1. You add a row to form state: this pushes the row in local state
optimistically then triggers a long-running form state request
containing a single row
2. You add another row to form state: this pushes a second row into
local state optimistically then triggers another long-running form state
request containing two rows
3. The first form state request returns with a single row in the
response and replaces local state (which contained two rows)
4. AT THIS MOMENT IN TIME, THE SECOND ROW DISAPPEARS
5. The second form state request returns with two rows in the response
and replaces local state
6. THE UI IS NO LONGER STALE AND BOTH ROWS APPEAR AS EXPECTED

The same issue applies when deleting, moving, and duplicating rows.
Local state becomes out of sync with the form state response and is
ultimately overridden.

The issue is that when we merge the result from form state, we do not
traverse the rows themselves, and instead take the rows in their
entirety. This means that we lose local row state. Instead, we need to
compare the results with what is saved to local state and intelligently
merge them.
2025-04-03 12:23:14 -04:00
reiv
018bdad247 feat(graphql): improve non-nullability in query result types (#11952)
### What?
Makes several fields and list item types in query results (e.g. `docs`)
non-nullable.

### Why?
When dealing with code generated from a Payload GraphQL schema, it is
often necessary to use type guards and optional chaining.

For example:

```graphql
type Posts {
  docs: [Post]
  ...
}
```

This implies that the `docs` field itself is nullable and that the array
can contain nulls. In reality, neither of these is true. But because of
the types generated by tools like `graphql-code-generator`, the way to
access `posts` ends up something like this:

```ts
const posts = (query.data.docs ?? []).filter(doc => doc != null);
```

Instead, we would like the schema to be:

```graphql
type Posts {
  docs: [Post!]!
  ...
}
```


### How?
The proposed change involves adding `GraphQLNonNull` where appropriate.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-04-03 15:17:23 +00:00
Said Akhrarov
816fb28f55 feat(ui): use drag overlay in orderable table (#11959)
<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR introduces a new `DragOverlay` to the existing `OrderableTable`
component along with a few new utility components. This enables a more
fluid and seamless drag-and-drop experience for end-users who have
enabled `orderable: true` on their collections.

### Why?
Previously, the rows in the `OrderableTable` component were confined
within the table element that renders them. This is troublesome for a
few reasons:
- It clips rows when dragging even slightly outside of the bounds of the
table.
- It creates unnecessary scrollbars within the containing element as the
container is not geared for comprehensive drag-and-drop interactions.

### How?
Introducing a `DragOverlay` component gives the draggable rows an area
to render freely without clipping. This PR also introduces a new
`OrderableRow` (for rendering orderable rows in the table as well as in
a drag preview), and an `OrderableRowDragPreview` component to render a
drag-preview of the active row 1:1 as you would see in the table without
violating HTML rules.

This PR also adds an `onDragStart` event handler to the
`DraggableDroppable` component to allow for listening for the start of a
drag event, necessary for interactions with a `DragOverlay` to
communicate which row initiated the event.

Before:


[orderable-before.webm](https://github.com/user-attachments/assets/ccf32bb0-91db-44f3-8c2a-4f81bb762529)


After:


[orderable-after.webm](https://github.com/user-attachments/assets/d320e7e6-fab8-4ea4-9cb1-38b581cbc50e)


After (With overflow on page):


[orderable-overflow-y.webm](https://github.com/user-attachments/assets/418b9018-901d-4217-980c-8d04d58d19c8)
2025-04-03 10:17:19 -03:00
Sasha
857e984fbb fix(db-mongodb): querying relationships with where clause as an object with several conditions (#11953)
Fixes https://github.com/payloadcms/payload/issues/11927

When trying to use the following notation:
```ts
const { docs } = await payload.find({
  collection: 'movies',
  depth: 0,
  where: {
    'director.name': { equals: 'Director1' },
    'director.localized': { equals: 'Director1_Localized' },
  },
})
```
Currently, it respects only the latest condition and the first is
ignored.

However, this works fine:
```ts
const { docs } = await payload.find({
  collection: 'movies',
  depth: 0,
  where: {
    and: [
      {
        'director.name': { equals: 'Director1' },
      },
      {
        'director.localized': { equals: 'Director1_Localized' },
      },
    ],
  },
})
```

But this should be an equivalent to
```
 where: {
    'director.name': { equals: 'Director1' },
    'director.localized': { equals: 'Director1_Localized' },
  },
```
2025-04-03 09:07:10 -04:00
Germán Jabloñski
d47b753898 chore(plugin-cloud-storage): enable TypeScript strict (#11850) 2025-04-03 10:06:25 -03:00
Germán Jabloñski
308cb64b9c chore(richtext-lexical): add DebugJsxConverterFeature (#10856)
Display the editor content below using the JSX converter
Added for debugging reasons, similar to TreeViewFeature

usage:

```ts
    {
      name: 'content',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [...defaultFeatures, DebugJsxConverterFeature()],
      }),
    },
```
2025-04-03 09:06:07 -04:00
Germán Jabloñski
6c735effff chore(plugin-redirects): enable TypeScript strict (#11931) 2025-04-03 09:04:21 -04:00
Germán Jabloñski
fd42ad5f52 chore(plugin-nested-docs): enable TypeScript strict (#11930) 2025-04-03 09:04:04 -04:00
Germán Jabloñski
a58ff57e4f chore(plugin-form-builder): enable TypeScript strict (#11929) 2025-04-03 09:01:13 -04:00
Alessio Gravili
06d937e903 docs: fix variable names for lexical markdown conversion (#11963) 2025-04-03 09:21:27 +03:00
Sasha
8e93ad8f5f fix(storage-uploadthing): pass clientUploads.routerInputConfig to the handler (#11962)
PR https://github.com/payloadcms/payload/pull/11954 added this property
but didn't actually pass it through to the handler.
2025-04-02 23:51:30 +00:00
Sasha
f310c90211 fix(db-postgres): down migration fails because migrationTableExists doesn't check in the current transaction (#11910)
Fixes https://github.com/payloadcms/payload/issues/11882

Previously, down migration that dropped the `payload_migrations` table
was failing because `migrationTableExists` doesn't check the current
transaction, only in which you can get a `false` value result.
2025-04-03 02:33:34 +03:00
Sasha
dc793d1d14 fix: ValidationError error message when label is a function (#11904)
Fixes https://github.com/payloadcms/payload/issues/11901

Previously, when `ValidationError` `errors.path` was referring to a
field with `label` defined as a function, the error message was
generated with `[object Object]`. Now, we call that function instead.
Since the `i18n` argument is required for `StaticLabel`, this PR
introduces so you can pass a partial `req` to `ValidationError` from
which we thread `req.i18n` to the label args.
2025-04-03 00:38:54 +03:00
Sasha
f9c73ad5f2 feat(storage-uploadthing): configurable upload router input config (#11954)
Fixes https://github.com/payloadcms/payload/issues/11949 by setting the
default limit to `512MB`.
Additionally, makes this configurable via
`clientUploads.routerInputConfig`. Details are here
https://docs.uploadthing.com/file-routes#route-config
2025-04-03 00:14:08 +03:00
Sasha
760cfadaad fix: do not append doc input for scheduled publish job if it's enabled only for globals (#11892)
Fixes https://github.com/payloadcms/payload/issues/11891


Previously, if you had scheduled publish enabled only for globals, not
collections - you'd get an error on `payload generate:types`:
<img width="886" alt="image"
src="https://github.com/user-attachments/assets/78125ce8-bd89-4269-bc56-966d8e0c3968"
/>

This was caused by appending the `doc` field to the scheduled publish
job input schema with empty `collections` array. Now we skip this field
if we don't have any collections.
2025-04-03 00:12:35 +03:00
Alessio Gravili
d29bdfc10f feat(next): improved lexical richText diffing in version view (#11760)
This replaces our JSON-based richtext diffing with HTML-based richtext
diffing for lexical. It uses [this HTML diff
library](https://github.com/Arman19941113/html-diff) that I then
modified to handle diffing more complex elements like links, uploads and
relationships.

This makes it way easier to spot changes, replacing the lengthy Lexical
JSON with a clean visual diff that shows exactly what's different.

## Before

![CleanShot 2025-03-18 at 13 54
51@2x](https://github.com/user-attachments/assets/811a7c14-d592-4fdc-a1f4-07eeb78255fe)


## After


![CleanShot 2025-03-31 at 18 14
10@2x](https://github.com/user-attachments/assets/efb64da0-4ff8-4965-a458-558a18375c46)
![CleanShot 2025-03-31 at 18 14
26@2x](https://github.com/user-attachments/assets/133652ce-503b-4b86-9c4c-e5c7706d8ea6)
2025-04-02 20:10:20 +00:00
Alessio Gravili
f34eb228c4 feat(drizzle): export buildQuery and parseParams (#11935)
This exports `buildQuery` and `parseParams` from @payloadcms/drizzle
2025-04-02 18:17:39 +00:00
Sasha
e5690fcab9 fix(graphql): respect draft: true when querying joins (#11869)
The same as https://github.com/payloadcms/payload/pull/11763 but also
for GraphQL. The previous fix was working only for the Local API and
REST API due to a different method for querying joins in GraphQL.
2025-04-01 14:41:47 -04:00
Elliot DeNolf
4ac6d21ef6 chore(release): v3.32.0 [skip ci] 2025-04-01 14:27:01 -04:00
Germán Jabloñski
d963e6a54c feat: orderable collections (#11452)
Closes https://github.com/payloadcms/payload/discussions/1413

### What?

Introduces a new `orderable` boolean property on collections that allows
dragging and dropping rows to reorder them:



https://github.com/user-attachments/assets/8ee85cf0-add1-48e5-a0a2-f73ad66aa24a

### Why?

[One of the most requested
features](https://github.com/payloadcms/payload/discussions/1413).
Additionally, poorly implemented it can be very costly in terms of
performance.

This can be especially useful for implementing custom views like kanban.

### How?

We are using fractional indexing. In its simplest form, it consists of
calculating the order of an item to be inserted as the average of its
two adjacent elements.
There is [a famous article by David
Greenspan](https://observablehq.com/@dgreensp/implementing-fractional-indexing)
that solves the problem of running out of keys after several partitions.
We are using his algorithm, implemented [in this
library](https://github.com/rocicorp/fractional-indexing).

This means that if you insert, delete or move documents in the
collection, you do not have to modify the order of the rest of the
documents, making the operation more performant.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-04-01 14:11:11 -04:00
Dan Ribbens
968a066f45 fix: typescriptSchema override required to false (#11941)
### What?
Previously if you used the typescriptSchema and `returned: false`, the
field would still be required anyways.

### Why?
We were adding fields to be required on the collection without comparing
the returned schema from typescriptSchema functions.

### How?
This changes the order of logic so that `requiredFieldNames` on the
collection is only after running and checking the field schema.
2025-04-01 11:35:31 -04:00
Jacob Fletcher
373f6d1032 fix(ui): nested fields disappear when manipulating rows in form state (#11906)
Continuation of #11867. When rendering custom fields nested within
arrays or blocks, such as the Lexical rich text editor which is treated
as a custom field, these fields will sometimes disappear when form state
requests are invoked sequentially. This is especially reproducible on
slow networks.

This is different from the previous PR in that this issue is caused by
adding _rows_ back-to-back, whereas the previous issue was caused when
adding a single row followed by a change to another field.

Here's a screen recording demonstrating the issue:


https://github.com/user-attachments/assets/5ecfa9ec-b747-49ed-8618-df282e64519d

The problem is that `requiresRender` is never sent in the form state
request for row 2. This is because the [task
queue](https://github.com/payloadcms/payload/pull/11579) processes tasks
within a single `useEffect`. This forces React to batch the results of
these tasks into a single rendering cycle. So if request 1 sets state
that request 2 relies on, request 2 will never use that state since
they'll execute within the same lifecycle.

Here's a play-by-play of the current behavior:

1. The "add row" event is dispatched
    a. This sets `requiresRender: true` in form state
1. A form state request is sent with `requiresRender: true`
1. While that request is processing, another "add row" event is
dispatched
    a. This sets `requiresRender: true` in form state
    b. This adds a form state request into the queue
1. The initial form state request finishes
    a. This sets `requiresRender: false` in form state
1. The next form state request that was queued up in 3b is sent with
`requiresRender: false`
    a. THIS IS EXPECTED, BUT SHOULD ACTUALLY BE `true`!!

To fix this this, we need to ensure that the `requiresRender` property
is persisted into the second request instead of overridden. To do this,
we can add a new `serverPropsToIgnore` to form state which is read when
the processing results from the server. So if `requiresRender` exists in
`serverPropsToIgnore`, we do not merge it. This works because we
actually mutate form state in between requests. So request 2 can read
the results from request 1 without going through an additional rendering
cycle.

Here's a play-by-play of the fix:

1. The "add row" event is dispatched
    a. This sets `requiresRender: true` in form state
b. This adds a task in the queue to mutate form state with
`requiresRender: true`
1. A form state request is sent with `requiresRender: true`
1. While that request is processing, another "add row" event is
dispatched
a. This sets `requiresRender: true` in form state AND
`serverPropsToIgnore: [ "requiresRender" ]`
    c. This adds a form state request into the queue
1. The initial form state request finishes
a. This returns `requiresRender: false` from the form state endpoint BUT
IS IGNORED
1. The next form state request that was queued up in 3c is sent with
`requiresRender: true`
2025-04-01 09:54:22 -04:00
dependabot[bot]
329cd0b876 chore(deps): bump mongodb-github-action (#10921)
Bumps the github_actions group with 1 update in the / directory:
[supercharge/mongodb-github-action](https://github.com/supercharge/mongodb-github-action).
Bumps the github_actions group with 1 update in the /.github/workflows
directory:
[supercharge/mongodb-github-action](https://github.com/supercharge/mongodb-github-action).

Updates `supercharge/mongodb-github-action` from 1.11.0 to 1.12.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/supercharge/mongodb-github-action/releases">supercharge/mongodb-github-action's
releases</a>.</em></p>
<blockquote>
<h2>1.12.0</h2>
<p>Release 1.12.0</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/supercharge/mongodb-github-action/blob/main/CHANGELOG.md">supercharge/mongodb-github-action's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/superchargejs/mongodb-github-action/compare/v1.11.0...v1.12.0">1.12.0</a>
- 2025-01-05</h2>
<h3>Added</h3>
<ul>
<li>added <code>mongodb-image</code> input: this option allows you to
define a custom Docker container image. It uses <code>mongo</code> by
default, but you may specify an image from a different registry than
Docker hub. Please check the Readme for details.</li>
</ul>
<h3>Updated</h3>
<ul>
<li>bump dependencies</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="90004df786"><code>90004df</code></a>
bump node and mongodb versions</li>
<li><a
href="b5fa058527"><code>b5fa058</code></a>
bump version to 1.12.0 in readme</li>
<li><a
href="369a992ac4"><code>369a992</code></a>
update changelog</li>
<li><a
href="08d5bf96ab"><code>08d5bf9</code></a>
bump deps</li>
<li><a
href="cbbc6f8110"><code>cbbc6f8</code></a>
Merge pull request <a
href="https://redirect.github.com/supercharge/mongodb-github-action/issues/64">#64</a>
from Sam-Bate-ITV/feature/alternative_image</li>
<li><a
href="6131e7ff86"><code>6131e7f</code></a>
wording</li>
<li><a
href="1f93cb7bb1"><code>1f93cb7</code></a>
change README based on PR review</li>
<li><a
href="812452b9eb"><code>812452b</code></a>
use docker hub for CI</li>
<li><a
href="4639b459cd"><code>4639b45</code></a>
apply suggested change</li>
<li><a
href="2ae9a450cf"><code>2ae9a45</code></a>
<a
href="https://redirect.github.com/supercharge/mongodb-github-action/issues/62">#62</a>:
add option for specifying image</li>
<li>See full diff in <a
href="https://github.com/supercharge/mongodb-github-action/compare/1.11.0...1.12.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `supercharge/mongodb-github-action` from 1.11.0 to 1.12.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/supercharge/mongodb-github-action/releases">supercharge/mongodb-github-action's
releases</a>.</em></p>
<blockquote>
<h2>1.12.0</h2>
<p>Release 1.12.0</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/supercharge/mongodb-github-action/blob/main/CHANGELOG.md">supercharge/mongodb-github-action's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/superchargejs/mongodb-github-action/compare/v1.11.0...v1.12.0">1.12.0</a>
- 2025-01-05</h2>
<h3>Added</h3>
<ul>
<li>added <code>mongodb-image</code> input: this option allows you to
define a custom Docker container image. It uses <code>mongo</code> by
default, but you may specify an image from a different registry than
Docker hub. Please check the Readme for details.</li>
</ul>
<h3>Updated</h3>
<ul>
<li>bump dependencies</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="90004df786"><code>90004df</code></a>
bump node and mongodb versions</li>
<li><a
href="b5fa058527"><code>b5fa058</code></a>
bump version to 1.12.0 in readme</li>
<li><a
href="369a992ac4"><code>369a992</code></a>
update changelog</li>
<li><a
href="08d5bf96ab"><code>08d5bf9</code></a>
bump deps</li>
<li><a
href="cbbc6f8110"><code>cbbc6f8</code></a>
Merge pull request <a
href="https://redirect.github.com/supercharge/mongodb-github-action/issues/64">#64</a>
from Sam-Bate-ITV/feature/alternative_image</li>
<li><a
href="6131e7ff86"><code>6131e7f</code></a>
wording</li>
<li><a
href="1f93cb7bb1"><code>1f93cb7</code></a>
change README based on PR review</li>
<li><a
href="812452b9eb"><code>812452b</code></a>
use docker hub for CI</li>
<li><a
href="4639b459cd"><code>4639b45</code></a>
apply suggested change</li>
<li><a
href="2ae9a450cf"><code>2ae9a45</code></a>
<a
href="https://redirect.github.com/supercharge/mongodb-github-action/issues/62">#62</a>:
add option for specifying image</li>
<li>See full diff in <a
href="https://github.com/supercharge/mongodb-github-action/compare/1.11.0...1.12.0">compare
view</a></li>
</ul>
</details>
<br />


You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 09:09:06 -04:00
Germán Jabloñski
6badb5ffcf chore(live-preview): enable TypeScript strict (#11840) 2025-04-01 09:03:39 -04:00
Marcus Forsberg
5b0e0ab788 fix(translations): improve Swedish translations for query presets (#11937)
### What?
Minor changes to Swedish translations added in #11330 to keep wording in
line with changes in #11654
2025-04-01 10:31:37 +00:00
Alessio Gravili
c844b4c848 feat: configurable job queue processing order (LIFO/FIFO), allow sequential execution of jobs (#11897)
Previously, jobs were executed in FIFO order on MongoDB, and LIFO on
Postgres, with no way to configure this behavior.

This PR makes FIFO the default on both MongoDB and Postgres and
introduces the following new options to configure the processing order
globally or on a queue-by-queue basis:
- a `processingOrder` property to the jobs config
- a `processingOrder` argument to `payload.jobs.run()` to override
what's set in the jobs config

It also adds a new `sequential` option to `payload.jobs.run()`, which
can be useful for debugging.
2025-03-31 15:00:36 -06:00
Alessio Gravili
9c88af4b20 refactor(drizzle): replace query chaining with dynamic query building (#11923)
This replaces usage of our `chainMethods` helper to dynamically chain
queries with [drizzle dynamic query
building](https://orm.drizzle.team/docs/dynamic-query-building).

This is more type-safe, more readable and requires less code
2025-03-31 20:37:45 +00:00
Alessio Gravili
9a1c3cf4cc fix: support parallel job queue tasks (#11917)
This adds support for running multiple job queue tasks in parallel
within the same workflow while preventing conflicts. Previously, this
would have caused the following issues:
- Job log entries get lost - the final job log is incomplete, despite
all tasks having been executed
- Write conflicts in postgres, leading to unique constraint violation
errors

The solution involves handling job log data updates in a way that avoids
overwriting, and ensuring the final update reflects the latest job log
data. Each job log entry now initializes its own ID, so a given job log
entry’s ID remains the same across multiple, parallel task executions.

## Postgres

In Postgres, we need to enable transactions for the
`payload.db.updateJobs` operation; otherwise, two tasks updating the
same job in parallel can conflict. This happens because Postgres handles
array rows by deleting them all, then re-inserting (rather than
upserting). The rows are stored in a separate table, and the following
scenario can occur:

Op 1: deletes all job log rows
Op 2: deletes all job log rows
Op 1: inserts 200 job log rows
Op 2: insert the same 200 job log rows again => `error: “duplicate key
value violates unique constraint "payload_jobs_log_pkey”`

Because transactions were not used, the rows inserted by Op 1
immediately became visible to Op 2, causing the conflict. Enabling
transactions fixes this. In theory, it can still happen if Op 1 commits
before Op 2 starts inserting (due to the read committed isolation
level), but it should occur far less frequently.

Alongside this change, we should consider inserting the rows using an
upsert (update on conflict), which will get rid of this error
completely. That way, if the insertion of Op 1 is visible to Op 2, Op 2
will simply overwrite it, rather than erroring. Individual job entries
are immutable and job entries cannot be deleted, thus this shouldn't
corrupt any data.

## Mongo

In Mongo, the issue is addressed by ensuring that log row deletions
caused due to different log states in concurrent operations are not
merged back to the client job log, and by making sure the final update
includes all job logs.

There is no duplicate key error in Mongo because the array log resides
in the same document and duplicates are simply upserted. We cannot use
transactions in Mongo, as it appears to lock the document in a way that
prevents reliable parallel updates, leading to:

`MongoServerError: WriteConflict error: this operation conflicted with
another operation. Please retry your operation or multi-document
transaction`
2025-03-31 13:06:05 -06:00
Alessio Gravili
a083d47368 feat(db-*): return database name to unsanitized config (#11913)
You can access the database name from `sanitizedConfig.db.name`. But
currently, it' not possible to access the db name from the unsanitized
config.

Plugins only have access to the unsanitized config. This change allows
db adapters to return the db name early, which will allow plugins to
conditionally initialize db-specific functionality
2025-03-31 12:57:17 -06:00
Patrik
96289bf555 fix(next): block encoded and escaped open redirects in getSafeRedirect (#11907)
### What

This PR improves the `getSafeRedirect` utility to improve security
around open redirect handling.

### How

- Normalizes and decodes the redirect path using `decodeURIComponent`
- Catches malformed encodings with a try/catch fallback
- Blocks open redirects
2025-03-31 13:11:34 -04:00
Alessio Gravili
a6f7ef837a feat(db-*): export types from main export (#11914)
In 3.0, we made the decision to export all types from the main package
export (e.g. `payload/types` => `payload`). This improves type
discoverability by IDEs and simplifies importing types.

This PR does the same for our db adapters, which still have a separate
`/types` subpath export. While those are kept for
backwards-compatibility, we can remove them in 4.0.
2025-03-31 15:45:02 +00:00
Said Akhrarov
03d4c5b2ee test: deflake versions with autosave e2e (#11919)
<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR aims to deflake the `test/versions/e2e.spec.ts:925:5 › Versions
› Collections with draft validation › - with autosave - shows a prevent
leave alert when form is submitted but invalid` e2e test.

The issue seems to be that the `fill` call followed by a `page.reload`
sometimes conflicts with autosave which may cause the test to flake.

### Why?
To deflake this test in ci.

### How?
Adds a single `waitForAutoSaveToRunAndComplete` function call prior to
the last call to `page.reload`. In my testing, on my local machine,
adding the `waitForAutoSaveToRunAndComplete` function allows the test to
pass every time. Without this, the tests fails on my machine
consistently.
2025-03-31 09:37:43 -03:00
Nate Schneider
af8c7868d6 docs: capitalization error (#11912)
Fixed a capitalized letter at line 180
2025-03-31 10:50:36 +00:00
Alessio Gravili
d1c0989da7 perf: prefer async fs calls (#11918)
Synchronous file system operations such as `readFileSync` block the
event loop, whereas the asynchronous equivalents (like await
`fs.promises.readFile`) do not. This PR replaces certain synchronous fs
calls with their asynchronous counterparts in contexts where async
operations are already in use, improving performance by avoiding event
loop blocking.

Most of the synchronous calls were in our file upload code. Converting
them to async should theoretically free up the event loop and allow
more, other requests to run in parallel without delay
2025-03-29 10:58:54 -06:00
Said Akhrarov
70b9cab393 test: deflake indexed e2e (#11911)
### What?
This PR aims to deflake the indexed fields e2e test in
`test/fields/collections/Indexed/e2e.spec.ts`.

The issue is that this test is setup in a way where sometimes two toasts
will present themselves in the ui. The second toast assertion will fail
with a strict mode violation as the toast locator will resolve to two
elements.

### Why?
To prevent this test from flaking in ci.

### How?
Adding a new `dismissAfterAssertion` flag to the `assertToastErrors`
helper function which dismisses the toasts. This way, the toasts will
not raise the aforementioned error as they will be dismissed from the
ui.

The logic is handled in a separate loop through such that the assertions
occur first. This is done so that dismissing a toast does not surface
errors due to the order of toasts being shown changing.
2025-03-29 01:02:05 +00:00
Maxim Seshuk
4a0bc869dd fix(ui): switching languages does not update cached client config (#11725)
### What?
Fixed client config caching to properly update when switching languages
in the admin UI.

### Why?
Currently, switching languages doesn't fully update the UI because
client config stays cached with previous language translations.

### How?
Created a language-aware caching system that stores separate configs for
each language and only uses cached config when it matches the active
language.

Before:
```typescript
let cachedClientConfig: ClientConfig | null = global._payload_clientConfig

if (!cachedClientConfig) {
  cachedClientConfig = global._payload_clientConfig = null
}

export const getClientConfig = cache(
  (args: { config: SanitizedConfig; i18n: I18nClient; importMap: ImportMap }): ClientConfig => {
    if (cachedClientConfig && !global._payload_doNotCacheClientConfig) {
      return cachedClientConfig
    }
    // ... create new config ...
  }
);
```

After:
```typescript
let cachedClientConfigs: Record<string, ClientConfig> = global._payload_localizedClientConfigs

if (!cachedClientConfigs) {
  cachedClientConfigs = global._payload_localizedClientConfigs = {}
}

export const getClientConfig = cache(
  (args: { config: SanitizedConfig; i18n: I18nClient; importMap: ImportMap }): ClientConfig => {
    const { config, i18n, importMap } = args
    const currentLocale = i18n.language

    if (!global._payload_doNotCacheClientConfig && cachedClientConfigs[currentLocale]) {
      return cachedClientConfigs[currentLocale]
    }
    // ... create new config with correct translations ...
  }
);
```

Also added handling for cache clearing during HMR to ensure
compatibility with the existing system.

Fixes #11406

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-03-28 17:49:28 -04:00
Jacob Fletcher
62c4e81a1f refactor(ui): replace autosave queue pattern with useQueues hook (#11884)
Replaces the queue pattern used within autosave with the `useQueues`
hook introduced in #11579. To do this, queued tasks now accept an
options object with callbacks which can be used to tie into events of
the process, such as before it begins to prevent it from running, and
after it has finished to perform side effects.

The `useQueues` hook now also maintains an array of queued tasks as
opposed to individual refs.
2025-03-28 13:54:15 -04:00
Alessio Gravili
2b6313ed48 docs: fix invalid react-hooks docs (#11895)
Our current react-docs page is not accessible due to an mdx parsing
error, caused by a recent introduction of invalid syntax. This PR fixes
it
2025-03-28 08:39:06 +02:00
Philipp Schneider
21f7ba7b9d feat: change version view modifiedOnly default to true (#11794)
Replaces a more elaborate approach from
https://github.com/payloadcms/payload/pull/11520 with the simplest
solution, just changing the default.
2025-03-27 19:22:41 -03:00
Pranav
b863fd0915 docs: correct spelling of "it" (#11889)
Correct spelling of "it" in configuration/overview.mdx
2025-03-27 15:58:25 +00:00
Alessio Gravili
f34cc637e3 fix(richtext-lexical): incorrectly hidden fields in drawers due to incorrect permissions handling (#11883)
Lexical nested fields are currently not set-up to handle access control
on the client properly. Despite that, we were passing parent permissions
to `RenderFields`, which causes certain fields to not show up if the
document does not have `create` permission.
2025-03-26 15:04:55 -06:00
Alessio Gravili
59c9feeb45 templates: pin all payload packages, improve gen-templates script (#11841)
This PR comes with a bunch of improvements to our template generation
script that makes it safer and more reliable

- bumps all our templates
- Using `latest` as payload version in our templates has proven to be
unreliable. This updates the gen-templates script to pin all payload
packages to the latest version
- adds the missing `website` entry for our template variations, thus
ensuring its lockfile gets updated
- adds importmap generation to the gen-templates script
- adds new `script:gen-templates:build` script to verify that all
templates still build correctly
2025-03-26 20:52:53 +00:00
Paul
1578cd2425 chore(ui): added selected option as a class to list table cell (#11750)
In the Cell component for a select field such as our `_status` fields it
will now add a class eg. `selected--published` for the selected option
so it can be easily targeted with CSS.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-03-26 20:32:42 +00:00
Said Akhrarov
5ae5255ba3 perf(ui): download only images and optimize image selection for document edit view, prioritize best-fit size (#11844)
### What?

In the same vein as #11696, this PR optimizes how images are selected
for display in the document edit view. It ensures that only image files
are processed and selects the most appropriate size to minimize
unnecessary downloads and improve performance.

#### Previously:

- Non-image files were being processed unnecessarily, despite not
generating thumbnails.
- Images without a `thumbnailURL` defaulted to their original full size,
even when smaller, optimized versions were available.

#### Now:

- **Only images** are processed for thumbnails, avoiding redundant
requests for non-images.
- **The smallest available image within a target range** (`40px -
180px`) is prioritized for display.
- **If no images fit within this range**, the logic selects:
  - The next smallest larger image (if available).
- The **original** image if it is smaller than the next available larger
size.
  - The largest **smaller** image if no better fit exists.

### Why?

Prevents unnecessary downloads of non-image files, reduces bandwidth
usage by selecting more efficient image sizes and improves load times
and performance in the edit view.

### How?

- **Filters out non-image files** when determining which assets to
display.
- Uses the same algorithm as in #11696 but turns it into a reusable
function to be used in various areas around the codebase. Namely the
upload field hasOne and hasMany components.

Before (4.5mb transfer):

![edit-view-before](https://github.com/user-attachments/assets/ff3513b7-b874-48c3-bce7-8a9425243e00)

After (15.9kb transfer):

![edit-view-after](https://github.com/user-attachments/assets/fce8c463-65ae-4f1d-81b5-8781e89f06f1)
2025-03-26 16:13:52 -04:00
Alessio Gravili
98e4db07c3 fix(plugin-cloud-storage): ensure client handlers are added to import map regardless of enabled state (#11880)
There are cases when a storage plugin is disabled during development and
enabled in production. This will result in import maps that differ
depending on if they're generated during development or production.

In a lot of cases, those import maps are generated during
development-only. During production, we just re-use what was generated
locally. This will cause missing import map entries for those plugins
that are disabled during development.

This PR ensures the import map entries are added regardless of the
enabled state of those plugins. This is necessary for our
generate-templates script to not omit the vercel blob storage import map
entry.
2025-03-26 18:13:32 +00:00
Said Akhrarov
6b56343b97 docs: fix links in custom components and custom features (#11881)
### What?
Fixes a few broken links in `docs/custom-components` and
`docs/rich-text`. Also made some custom component links lowercase.

### Why?
To direct end users to the correct location in the docs.

### How?
Changes to `docs/custom-components/custom-views.mdx`,
`docs/custom-components/list-view.mdx`, and
`docs/rich-text/custom-features.mdx`.
2025-03-26 12:12:01 -06:00
Jacob Fletcher
4fc2eec301 fix(ui): query presets are available for unrelated collections (#11872)
When selecting query presets from the list drawer, all query presets are
available for selection, even if unrelated to the underlying collection.
When selecting one of these presets, the list view will crash with
client-side exceptions because the columns and filters that are applied
are incompatible.

The fix is to the thread `filterOptions` through the query presets
drawer. This will ensure that only related collections are shown.
2025-03-25 23:45:03 -04:00
Jacob Fletcher
10ac9893ad fix(ui): nested custom components sometimes disappear when queued in form state (#11867)
When rendering custom fields nested within arrays or blocks, such as the
Lexical rich text editor which is treated as a custom field, these
fields will sometimes disappear when form state requests are invoked
sequentially. This is especially reproducible on slow networks.

This is because form state invocations are placed into a [task
queue](https://github.com/payloadcms/payload/pull/11579) which aborts
the currently running tasks when a new one arrives. By doing this, local
form state is never dispatched, and the second task in the queue becomes
stale.

The fix is to _not_ abort the currently running task. This will trigger
a complete rendering cycle, and when the second task is invoked, local
state will be up to date.

Fixes #11340, #11425, and #11824.
2025-03-25 20:40:16 -04:00
Elliot DeNolf
35e6cfbdfc chore(release): v3.31.0 [skip ci] 2025-03-25 14:28:01 -04:00
Alessio Gravili
a5c3aa0e4f perf: reduce job queue db calls (#11846)
Continuation of #11489. This adds a new, optional `updateJobs` db
adapter method that reduces the amount of database calls for the jobs
queue.

## MongoDB

### Previous: running a set of 50 queued jobs
- 1x db.find (= 1x `Model.paginate`)
- 50x db.updateOne (= 50x `Model.findOneAndUpdate`)

### Now: running a set of 50 queued jobs
- 1x db.updateJobs (= 1x `Model.find` and 1x `Model.updateMany`)

**=> 51 db round trips before, 2 db round trips after**


### Previous: upon task completion
- 1x db.find (= 1x `Model.paginate`)
- 1x db.updateOne (= 1x `Model.findOneAndUpdate`)

### Now: upon task completion
- 1x db.updateJobs (= 1x `Model.findOneAndUpdate`)


**=> 2 db round trips before, 1 db round trip after**


## Drizzle (e.g. Postgres)

### running a set of 50 queued jobs
 - 1x db.query[tablename].findMany
 - 50x db.select 
 - 50x upsertRow
 
This is unaffected by this PR and will be addressed in a future PR
2025-03-25 18:09:52 +00:00
Jacob Fletcher
74f935bfb9 fix: auth fields distrupt field paths within the field schema map (#11861)
Within auth-enabled collections, we inject the `password` and
`confirmPassword` fields into the field schema map. While this is fine
within the edit view where these fields are used, this breaks field
paths within the version diff view where unnamed fields are no longer
able to lookup their corresponding config. This is because the presence
of these injected fields increments the field indices by two.

A temporary fix for this is to simply inject these fields _last_ into
the schema map. This way their presence does not disrupt field path
generation. A long term fix should be implemented, however, where these
fields actually exist on the collection config itself. This way no
config mutation would be required as the sanitized config would the
single source of truth.

To do this, we'd need to ensure that these fields do not appear in any
APIs, and that they do not generate types, etc.
2025-03-25 12:19:29 -04:00
Alessio Gravili
73fc3c607a perf(drizzle): remove unnecessary db.select call in updateOne operation (#11847)
This will improve performance when updating a single document in
postgres/drizzle, if the ID is known.

Previously, this resulted in 2 sequential operations:
- `db.select `to fetch the document by the ID
- `upsertRow` to update the document (multiple db operations)

This PR removes the unnecessary `db.select` call, as the document ID is
already known
2025-03-25 10:11:20 -06:00
Elliot DeNolf
7fb4b1324e ci: add license-check script (#11860)
Add license check script to output all licenses in use. Run with `pnpm
script:license-check`, output will be in `licenses.csv` at root.
2025-03-25 11:54:00 -04:00
Diego Satelier
61747082ef fix(plugin-seo): translation correction (#11817)
Corrected the translations that were wrong.

![es-ts-before-after](https://github.com/user-attachments/assets/37932d18-9623-4a9e-9af0-b5d770268066)
2025-03-25 15:48:25 +00:00
Sasha
93cc66d745 test: rearrange relationships test blocks properly (#11858)
Previously, many test cases in `int/relationships` were wrapped to the
"custom IDs" describe block even though they aren't related to custom
IDs at all. This rearranges them as they should be.
2025-03-25 15:35:33 +00:00
Dan Ribbens
f61f6b73c7 feat: add Armenian translation (#11857)
Original PR https://github.com/payloadcms/payload/pull/11852 thanks to
@lyovson

---------

Co-authored-by: Rafa Lyovson <rafa@lyovson.com>
2025-03-25 16:53:40 +02:00
Patrik
1081b4a0ff fix: add uuid fallback for non-secure contexts in JSON fields (#11839)
### What

The `crypto.randomUUID()` function was causing errors in non-secure
contexts (HTTP), as it is only available in secure contexts (HTTPS).

### How

Added a fallback to generate UUIDs using the `uuid` library when
`crypto.randomUUID()` is not available.

Fixes #11825
2025-03-25 10:01:04 -04:00
Patrik
234df54446 fix(next): adds safe redirect utility and apply to login redirects (#11814)
This PR introduces a new utility function, `getSafeRedirect`, to
sanitize and validate redirect paths used in the login flow.

It replaces the previous use of `encodeURIComponent` and inline string
checks with a centralized, reusable, and more secure approach.

#### `getSafeRedirect` utility:
- Ensures redirect paths start with a single `/`
- Blocks protocol-relative URLs (e.g., `//evil.com`)
- Blocks JavaScript schemes (e.g., `/javascript:alert(1)`)
- Blocks full URL redirects like `/http:` or `/https:`
2025-03-25 09:52:18 -04:00
Germán Jabloñski
fe9317a0dd chore(db-sqlite): enable TypeScript strict (#11831)
- I installed `@types/uuid` because typescript required it in a file
- In `packages/db-sqlite/src/index.ts` I see four more errors in my IDE
that don't appear when I run the typecheck in the CLI with `tsc
--noEmit`. The same thing happened in
https://github.com/payloadcms/payload/pull/11560. Also referencing
https://github.com/payloadcms/payload/pull/11226#issuecomment-2713898801
for traceability.
2025-03-24 23:41:07 -03:00
Alessio Gravili
eb1434e986 refactor(richtext-lexical): ensure field can be rendered outside EntityVisibilityProvider (#11842)
This ensures that the lexical field can be rendered without having to
wrap it inside an `EntityVisibilityProvider`, making it a bit easier to
manually render the lexical field in a custom component.
2025-03-25 00:02:24 +00:00
Germán Jabloñski
de0aaf6e91 chore(db-vercel-postgres): enable TypeScript strict (#11833)
same comment as in #11560, #11831, #11226:

> In `src/index.ts` I see four more errors in my IDE that don't appear
when I run the typecheck in the CLI with `tsc --noEmit`.
2025-03-24 21:17:15 +00:00
Alessio Gravili
3c4b3ee527 fix(next): version view breaking for deeply nested tabs, rows and collapsibles (#11808)
Fixes #11458 

Some complex, nested fields were receiving incorrect field paths and
schema paths, leading to a `"Error: No client field found"` error.

This PR ensures field paths are calculated correctly, by matching it to
how they're calculated in payload hooks.
2025-03-24 20:57:36 +00:00
Alessio Gravili
fb01b4046d fix(richtext-lexical): ensure initial state for nested lexical fields (#11837)
Lexical fields nested in other fields (e.g. groups, blocks, arrays) did
not have their initial sub-field states generated, leading in multiple
client-side fetches to fetch initial state when the page is loaded.

Before:


https://github.com/user-attachments/assets/c1d808ef-1bd3-4fb1-a9d6-d5ef81cef16d

After:


https://github.com/user-attachments/assets/0dcda515-ce68-4107-ba29-a08fff851ae3
2025-03-24 20:08:26 +00:00
Germán Jabloñski
8d374cb57d chore(admin-bar): enable TypeScript strict (#11834)
Looks like this one was bug-free! I don't know why strict was disabled
2025-03-24 17:31:09 +00:00
Jacob Fletcher
998181b986 feat: query presets (#11330)
Query Presets allow you to save and share filters, columns, and sort
orders for your collections. This is useful for reusing common or
complex filtering patterns and column configurations across your team.
Query Presets are defined on the fly by the users of your app, rather
than being hard coded into the Payload Config.

Here's a screen recording demonstrating the general workflow as it
relates to the list view. Query Presets are not exclusive to the admin
panel, however, as they could be useful in a number of other contexts
and environments.


https://github.com/user-attachments/assets/1fe1155e-ae78-4f59-9138-af352762a1d5

Each Query Preset is saved as a new record in the database under the
`payload-query-presets` collection. This will effectively make them
CRUDable and allows for an endless number of preset configurations. As
you make changes to filters, columns, limit, etc. you can choose to save
them as a new record and optionally share them with others.

Normal document-level access control will determine who can read,
update, and delete these records. Payload provides a set of sensible
defaults here, such as "only me", "everyone", and "specific users", but
you can also extend your own set of access rules on top of this, such as
"by role", etc. Access control is customizable at the operation-level,
for example you can set this to "everyone" can read, but "only me" can
update.

To enable the Query Presets within a particular collection, set
`enableQueryPresets` on that collection's config.

Here's an example:

```ts
{
  // ...
  enableQueryPresets: true
}
```

Once enabled, a new set of controls will appear within the list view of
the admin panel. This is where you can select and manage query presets.

General settings for Query Presets are configured under the root
`queryPresets` property. This is where you can customize the labels,
apply custom access control rules, etc.

Here's an example of how you might augment the access control properties
with your own custom rule to achieve RBAC:

```ts
{
  // ...
  queryPresets: {
    constraints: {
      read: [
        {
          label: 'Specific Roles',
          value: 'specificRoles',
          fields: [roles],
          access: ({ req: { user } }) => ({
            'access.update.roles': {
              in: [user?.roles],
            },
          }),
        },
      ],
    }
  }
}
```

Related: #4193 and #3092

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-03-24 13:16:39 -04:00
804 changed files with 29630 additions and 5969 deletions

View File

@@ -294,14 +294,10 @@ jobs:
- fields__collections__Email
- fields__collections__Indexed
- fields__collections__JSON
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Lexical__e2e__blocks#config.blockreferences.ts
- fields__collections__Number
- fields__collections__Point
- fields__collections__Radio
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Row
- fields__collections__Select
- fields__collections__Tabs
@@ -309,6 +305,12 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- hooks
- lexical__collections__Lexical__e2e__main
- lexical__collections__Lexical__e2e__blocks
- lexical__collections__Lexical__e2e__blocks#config.blockreferences.ts
- lexical__collections__RichText
- query-presets
- form-state
- live-preview
- localization
@@ -319,6 +321,7 @@ jobs:
- plugin-import-export
- plugin-nested-docs
- plugin-seo
- sort
- versions
- uploads
env:

View File

@@ -83,7 +83,7 @@ jobs:
echo "DATABASE_URI=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.11.0
uses: supercharge/mongodb-github-action@1.12.0
with:
mongodb-version: 6.0

2
.gitignore vendored
View File

@@ -323,3 +323,5 @@ test/databaseAdapter.js
test/.localstack
test/google-cloud-storage
test/azurestoragedata/
licenses.csv

View File

@@ -21,10 +21,9 @@ When a user starts editing a document, Payload locks it for that user. If anothe
The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing.
<Banner type="info">
{' '}
**Note:** If your application does not require document locking, you can
disable this feature for any collection or global by setting the
`lockDocuments` property to `false`.{' '}
`lockDocuments` property to `false`.
</Banner>
### Config Options

View File

@@ -718,7 +718,7 @@ The `useDocumentInfo` hook provides information about the current document being
| **`currentEditor`** | The user currently editing the document. |
| **`docConfig`** | Either the Collection or Global config of the document, depending on what is being edited. |
| **`docPermissions`** | The current document's permissions. Fallback to collection permissions when no id is present. |
| **`documentIsLocked`** | Whether the document is currently locked by another user. [More details](./locked-documents). |
| **`documentIsLocked`** | Whether the document is currently locked by another user. [More details](./locked-documents). |
| **`getDocPermissions`** | Method to retrieve document-level permissions. |
| **`getDocPreferences`** | Method to retrieve document-level user preferences. [More details](./preferences). |
| **`globalSlug`** | The slug of the global if editing a global document. |
@@ -730,7 +730,7 @@ The `useDocumentInfo` hook provides information about the current document being
| **`initialData`** | The initial data of the document. |
| **`isEditing`** | Whether the document is being edited (as opposed to created). |
| **`isInitializing`** | Whether the document info is still initializing. |
| **`isLocked`** | Whether the document is locked. [More details](./locked-documents). |
| **`isLocked`** | Whether the document is locked. [More details](./locked-documents). |
| **`lastUpdateTime`** | Timestamp of the last update to the document. |
| **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. |
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). |
@@ -739,9 +739,9 @@ The `useDocumentInfo` hook provides information about the current document being
| **`setDocumentTitle`** | Method to set the document title. |
| **`setHasPublishedDoc`** | Method to update whether the document has been published. |
| **`title`** | The title of the document. |
| **`unlockDocument`** | Method to unlock a document. [More details](./locked-documents). |
| **`unlockDocument`** | Method to unlock a document. [More details](./locked-documents). |
| **`unpublishedVersionCount`** | The number of unpublished versions of the document. |
| **`updateDocumentEditor`** | Method to update who is currently editing the document. [More details](./locked-documents). |
| **`updateDocumentEditor`** | Method to update who is currently editing the document. [More details](./locked-documents). |
| **`updateSavedDocumentData`** | Method to update the saved document data. |
| **`uploadStatus`** | Status of any uploads in progress ('idle', 'uploading', or 'failed'). |
| **`versionCount`** | The current version count of the document. |
@@ -791,17 +791,18 @@ const MyComponent: React.FC = () => {
The `useListQuery` hook returns an object with the following properties:
| Property | Description |
| ------------------------- | ------------------------------------------------------------------------ |
| **`data`** | The data that is being displayed in the List View. |
| **`defaultLimit`** | The default limit of items to display in the List View. |
| **`defaultSort`** | The default sort order of items in the List View. |
| **`handlePageChange`** | A method to handle page changes in the List View. |
| **`handlePerPageChange`** | A method to handle per page changes in the List View. |
| **`handleSearchChange`** | A method to handle search changes in the List View. |
| **`handleSortChange`** | A method to handle sort changes in the List View. |
| **`handleWhereChange`** | A method to handle where changes in the List View. |
| **`query`** | The current query that is being used to fetch the data in the List View. |
| Property | Description |
| ------------------------- | -------------------------------------------------------------------------------------- |
| **`data`** | The data that is being displayed in the List View. |
| **`defaultLimit`** | The default limit of items to display in the List View. |
| **`defaultSort`** | The default sort order of items in the List View. |
| **`handlePageChange`** | A method to handle page changes in the List View. |
| **`handlePerPageChange`** | A method to handle per page changes in the List View. |
| **`handleSearchChange`** | A method to handle search changes in the List View. |
| **`handleSortChange`** | A method to handle sort changes in the List View. |
| **`handleWhereChange`** | A method to handle where changes in the List View. |
| **`modified`** | Whether the query has been changed from its [Query Preset](../query-presets/overview). |
| **`query`** | The current query that is being used to fetch the data in the List View. |
## useSelection

View File

@@ -158,7 +158,7 @@ mutation {
```ts
const result = await payload.login({
collection: '[collection-slug]',
collection: 'collection-slug',
data: {
email: 'dev@payloadcms.com',
password: 'get-out',
@@ -166,6 +166,13 @@ const result = await payload.login({
})
```
<Banner type="success">
**Server Functions:** Payload offers a ready-to-use `login` server function
that utilizes the Local API. For integration details and examples, check out
the [Server Function
docs](../local-api/server-functions#reusable-payload-server-functions).
</Banner>
## Logout
As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way.
@@ -189,6 +196,13 @@ mutation {
}
```
<Banner type="success">
**Server Functions:** Payload provides a ready-to-use `logout` server function
that manages the user's cookie for a seamless logout. For integration details
and examples, check out the [Server Function
docs](../local-api/server-functions#reusable-payload-server-functions).
</Banner>
## Refresh
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
@@ -240,6 +254,13 @@ mutation {
}
```
<Banner type="success">
**Server Functions:** Payload exports a ready-to-use `refresh` server function
that automatically renews the user's token and updates the associated cookie.
For integration details and examples, check out the [Server Function
docs](../local-api/server-functions#reusable-payload-server-functions).
</Banner>
## Verify by Email
If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API.
@@ -270,7 +291,7 @@ mutation {
```ts
const result = await payload.verifyEmail({
collection: '[collection-slug]',
collection: 'collection-slug',
token: 'TOKEN_HERE',
})
```
@@ -308,7 +329,7 @@ mutation {
```ts
const result = await payload.unlock({
collection: '[collection-slug]',
collection: 'collection-slug',
})
```
@@ -349,7 +370,7 @@ mutation {
```ts
const token = await payload.forgotPassword({
collection: '[collection-slug]',
collection: 'collection-slug',
data: {
email: 'dev@payloadcms.com',
},

View File

@@ -11,7 +11,7 @@ keywords: authentication, config, configuration, overview, documentation, Conten
title="Simplified Authentication for Headless CMS: Unlocking Reusability in One Line"
/>
Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), all well as your own external applications, completely eliminating the need for paid, third-party platforms and services.
Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), as well as your own external applications, completely eliminating the need for paid, third-party platforms and services.
Here are some common use cases of Authentication in your own applications:

View File

@@ -60,29 +60,31 @@ export const Posts: CollectionConfig = {
The following options are available:
| Option | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
| Option | Description |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
_\* An asterisk denotes that a property is required._
@@ -176,7 +178,7 @@ The following options are available:
```ts
import type { CollectionCOnfig } from 'payload'
export const MyCollection: CollectionCOnfig = {
export const MyCollection: CollectionConfig = {
// ...
admin: {
components: {

View File

@@ -84,6 +84,7 @@ The following options are available:
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
@@ -146,7 +147,7 @@ _\* Config location detection is different between development and production en
<Banner type="warning">
**Important:** Ensure your `tsconfig.json` is properly configured for Payload
to auto-detect your config location. If if does not exist, or does not specify
to auto-detect your config location. If it does not exist, or does not specify
the proper `compilerOptions`, Payload will default to the current working
directory.
</Banner>
@@ -238,9 +239,9 @@ export default buildConfig({
// ...
// highlight-start
cors: {
origins: ['http://localhost:3000']
headers: ['x-custom-header']
}
origins: ['http://localhost:3000'],
headers: ['x-custom-header'],
},
// highlight-end
})
```

View File

@@ -55,7 +55,7 @@ For more granular control, pass a configuration object instead. Payload exposes
| `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
| `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
| `sensitive` | When true, will match if the path is case sensitive. |
| `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
| `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](../admin/metadata). |
_\* An asterisk denotes that a property is required._

View File

@@ -6,7 +6,7 @@ desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Edit View is where users interact with individual [Collection](../collections/overview) and [Global](../globals/overview) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree.
The Edit View is where users interact with individual [Collection](../configuration/collections) and [Global](../configuration/globals) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree.
The Edit View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
@@ -101,14 +101,15 @@ export const MyCollection: CollectionConfig = {
The following options are available:
| Path | Description |
| ----------------- | -------------------------------------------------------------------------------------- |
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
| `Description` | A description of the Collection. [More details](#Description). |
| `Upload` | A file upload component. [More details](#Upload). |
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Collection. [More details](#description). |
| `Upload` | A file upload component. [More details](#upload). |
#### Globals
@@ -133,13 +134,14 @@ export const MyGlobal: GlobalConfig = {
The following options are available:
| Path | Description |
| ----------------- | -------------------------------------------------------------------------------------- |
| `SaveButton` | A button that saves the current document. [More details](#SaveButton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#SaveDraftButton). |
| `PublishButton` | A button that publishes the current document. [More details](#PublishButton). |
| `PreviewButton` | A button that previews the current document. [More details](#PreviewButton). |
| `Description` | A description of the Global. [More details](#Description). |
| Path | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
| `Description` | A description of the Global. [More details](#description). |
### SaveButton
@@ -191,6 +193,73 @@ export function MySaveButton(props: SaveButtonClientProps) {
}
```
### beforeDocumentControls
The `beforeDocumentControls` property allows you to render custom components just before the default document action buttons (like Save, Publish, or Preview). This is useful for injecting custom buttons, status indicators, or any other UI elements before the built-in controls.
To add `beforeDocumentControls` components, use the `components.edit.beforeDocumentControls` property in you [Collection Config](../configuration/collections) or `components.elements.beforeDocumentControls` in your [Global Config](../configuration/globals):
#### Collections
```
export const MyCollection: CollectionConfig = {
admin: {
components: {
edit: {
// highlight-start
beforeDocumentControls: ['/path/to/CustomComponent'],
// highlight-end
},
},
},
}
```
#### Globals
```
export const MyGlobal: GlobalConfig = {
admin: {
components: {
elements: {
// highlight-start
beforeDocumentControls: ['/path/to/CustomComponent'],
// highlight-end
},
},
},
}
```
Here's an example of a custom `beforeDocumentControls` component:
#### Server Component
```tsx
import React from 'react'
import type { BeforeDocumentControlsServerProps } from 'payload'
export function MyCustomDocumentControlButton(
props: BeforeDocumentControlsServerProps,
) {
return <div>This is a custom beforeDocumentControl button (Server)</div>
}
```
#### Client Component
```tsx
'use client'
import React from 'react'
import type { BeforeDocumentControlsClientProps } from 'payload'
export function MyCustomDocumentControlButton(
props: BeforeDocumentControlsClientProps,
) {
return <div>This is a custom beforeDocumentControl button (Client)</div>
}
```
### SaveDraftButton
The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View.

View File

@@ -6,13 +6,13 @@ desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The List View is where users interact with a list of [Collection](../collections/overview) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many.
The List View is where users interact with a list of [Collection](../configuration/collections) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many.
The List View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
<Banner type="info">
**Note:** Only [Collections](../collections/overview) have a List View.
[Globals](../globals/overview) do not have a List View as they are single
**Note:** Only [Collections](../configuration/collections) have a List View.
[Globals](../configuration/globals) do not have a List View as they are single
documents.
</Banner>
@@ -90,11 +90,11 @@ The following options are available:
| Path | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforeList). |
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforeListTable). |
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterList). |
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterListTable). |
| `Description` | A component to render a description of the Collection. [More details](#Description). |
| `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforelist). |
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforelisttable). |
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterlist). |
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterlisttable). |
| `Description` | A component to render a description of the Collection. [More details](#description). |
### beforeList

View File

@@ -352,18 +352,20 @@ const config = buildConfig({
},
],
},
{
{
slug: 'collection2',
fields: [
{
name: 'editor',
type: 'richText',
editor: lexicalEditor({
BlocksFeature({
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
blocks: ['TextBlock'],
})
})
features: [
BlocksFeature({
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
blocks: ['TextBlock'],
}),
],
}),
},
],
},

View File

@@ -138,6 +138,7 @@ powerful Admin UI.
| **`name`** \* | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
| **`collection`** \* | The `slug`s having the relationship field or an array of collection slugs. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections |
| **`orderable`** | If true, enables custom ordering and joined documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
@@ -270,21 +271,6 @@ const result = await payload.find({
and blocks.
</Banner>
<Banner type="warning">
Currently, querying by the Join Field itself is not supported, meaning:
```ts
payload.find({
collection: 'categories',
where: {
'relatedPosts.title': { // relatedPosts is a join field
equals: "post"
}
}
})
```
does not work yet.
</Banner>
### Rest API
The REST API supports the same query options as the Local API. You can use the `joins` query parameter to customize the

View File

@@ -541,6 +541,7 @@ The `ctx` object:
| Property | Description |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
| **`operation`** | A string relating to which operation the field type is currently executing within. |
| **`path`** | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. |
| **`user`** | The currently authenticated user object. |

View File

@@ -94,6 +94,7 @@ The Relationship Field inherits all of the default options from the base [Field
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
| **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. |
### Sort Options

View File

@@ -63,10 +63,16 @@ To install a Database Adapter, you can run **one** of the following commands:
```
- To install the [Postgres Adapter](../database/postgres), run:
```bash
pnpm i @payloadcms/db-postgres
```
- To install the [SQLite Adapter](../database/sqlite), run:
```bash
pnpm i @payloadcms/db-sqlite
```
<Banner type="success">
**Note:** New [Database Adapters](/docs/database/overview) are becoming
available every day. Check the docs for the most up-to-date list of what's

View File

@@ -28,7 +28,7 @@ Then, you could configure two different runner strategies:
As mentioned above, you can queue jobs, but the jobs won't run unless a worker picks up your jobs and runs them. This can be done in four ways:
#### Cron jobs
### Cron jobs
You can use the `jobs.autoRun` property to configure cron jobs:
@@ -63,7 +63,7 @@ export default buildConfig({
and should not be used on serverless platforms like Vercel.
</Banner>
#### Endpoint
### Endpoint
You can execute jobs by making a fetch request to the `/api/payload-jobs/run` endpoint:
@@ -130,7 +130,7 @@ This works because Vercel automatically makes the `CRON_SECRET` environment vari
After the project is deployed to Vercel, the Vercel Cron job will automatically trigger the `/api/payload-jobs/run` endpoint in the specified schedule, running the queued jobs in the background.
#### Local API
### Local API
If you want to process jobs programmatically from your server-side code, you can use the Local API:
@@ -156,7 +156,7 @@ const results = await payload.jobs.runByID({
})
```
#### Bin script
### Bin script
Finally, you can process jobs via the bin script that comes with Payload out of the box.
@@ -169,3 +169,76 @@ In addition, the bin script allows you to pass a `--cron` flag to the `jobs:run`
```sh
npx payload jobs:run --cron "*/5 * * * *"
```
## Processing Order
By default, jobs are processed first in, first out (FIFO). This means that the first job added to the queue will be the first one processed. However, you can also configure the order in which jobs are processed.
### Jobs Configuration
You can configure the order in which jobs are processed in the jobs configuration by passing the `processingOrder` property. This mimics the Payload [sort](../queries/sort) property that's used for functionality such as `payload.find()`.
```ts
export default buildConfig({
// Other configurations...
jobs: {
tasks: [
// your tasks here
],
processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO
},
})
```
You can also set this on a queue-by-queue basis:
```ts
export default buildConfig({
// Other configurations...
jobs: {
tasks: [
// your tasks here
],
processingOrder: {
default: 'createdAt', // FIFO
queues: {
nightly: '-createdAt', // LIFO
myQueue: '-createdAt', // LIFO
},
},
},
})
```
If you need even more control over the processing order, you can pass a function that returns the processing order - this function will be called every time a queue starts processing jobs.
```ts
export default buildConfig({
// Other configurations...
jobs: {
tasks: [
// your tasks here
],
processingOrder: ({ queue }) => {
if (queue === 'myQueue') {
return '-createdAt' // LIFO
}
return 'createdAt' // FIFO
},
},
})
```
### Local API
You can configure the order in which jobs are processed in the `payload.jobs.queue` method by passing the `processingOrder` property.
```ts
const createdJob = await payload.jobs.queue({
workflow: 'createPostAndUpdate',
input: {
title: 'my title',
},
processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO
})
```

View File

@@ -0,0 +1,47 @@
---
title: Respecting Access Control with Local API Operations
label: Access Control
order: 40
desc: Learn how to implement and enforce access control in Payload's Local API operations, ensuring that the right permissions are respected during data manipulation.
keywords: server functions, local API, Payload, CMS, access control, permissions, user context, server-side logic, custom workflows, data management, headless CMS, TypeScript, Node.js, backend
---
In Payload, local API operations **override access control by default**. This means that operations will run without checking if the current user has permission to perform the action. This is useful in certain scenarios where access control is not necessary, but it is important to be aware of when to enforce it for security reasons.
### Default Behavior: Access Control Skipped
By default, **local API operations skip access control**. This allows operations to execute without the system checking if the current user has appropriate permissions. This might be helpful in admin or server-side scripts where the user context is not required to perform the operation.
#### For example:
```ts
// Access control is this operation would be skipped by default
const test = await payload.create({
collection: 'users',
data: {
email: 'test@test.com',
password: 'test',
},
})
```
### Respecting Access Control
If you want to respect access control and ensure that the operation is performed only if the user has appropriate permissions, you need to explicitly pass the `user` object and set the `overrideAccess` option to `false`.
- `overrideAccess: false`: This ensures that access control is **not skipped** and the operation respects the current user's permissions.
- `user`: Pass the authenticated user context to the operation. This ensures the system checks whether the user has the right permissions to perform the action.
```ts
const authedCreate = await payload.create({
collection: 'users',
overrideAccess: false, // This ensures access control will be applied
user, // Pass the authenticated user to check permissions
data: {
email: 'test@test.com',
password: 'test',
},
})
```
This example will only allow the document to be created if the `user` we passed has the appropriate access control permissions.

View File

@@ -0,0 +1,524 @@
---
title: Using Local API Operations with Server Functions
label: Server Functions
order: 30
desc: Learn to use Local API operations with Server Functions in Payload to manage server-side logic, data interactions, and custom workflows directly within your CMS.
keywords: server functions, local API, Payload, CMS, server-side logic, custom workflows, data management, headless CMS, TypeScript, Node.js, backend
---
In Next.js, **server functions** (previously called **server actions**) are special functions that run exclusively on the server, enabling secure backend logic execution while being callable from the frontend. These functions bridge the gap between client and server, allowing frontend components to perform backend operations without exposing sensitive logic.
### Why Use Server Functions?
- **Executing Backend Logic from the Frontend**: The Local API is designed for server environments and cannot be directly accessed from client-side code. Server functions enable frontend components to trigger backend operations securely.
- **Security Benefits**: Instead of exposing a full REST or GraphQL API, server functions restrict access to only the necessary operations, reducing potential security risks.
- **Performance Optimizations**: Next.js handles server functions efficiently, offering benefits like caching, optimized database queries, and reduced network overhead compared to traditional API calls.
- **Simplified Development Workflow**: Rather than setting up full API routes with authentication and authorization checks, server functions allow for lightweight, direct execution of necessary operations.
### When to Use Server Functions
Use server functions whenever you need to call Local API operations from the frontend. Since the Local API is only accessible from the backend, server functions act as a secure bridge, eliminating the need to expose additional API endpoints.
## Examples
All Local API operations can be used within server functions, allowing you to interact with Payload's backend securely.
For a full list of available operations, see the [Local API](https://payloadcms.com/docs/local-api/overview) overview.
In the following examples, we'll cover some common use cases, including:
- Creating a document
- Updating a document
- Handling file uploads when creating or updating a document
- Authenticating a user
### Creating a Document
First, let's create our server function. Here are some key points for this process:
- Begin by adding `'use server'` at the top of the file.
- You can still use utilities such as `getPayload()`.
- Once the function structure is in place, call the Local API operation `payload.create()` and pass in the relevant data.
- It's good practice to wrap this in a `try...catch` block for error handling.
- Finally, make sure to return the created document (don't just run the operation).
```ts
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function createPost(data) {
const payload = await getPayload({ config })
try {
const post = await payload.create({
collection: 'posts',
data,
})
return post
} catch (error) {
throw new Error(`Error creating post: ${error.message}`)
}
}
```
Now, let's look at how to call the `createPost` function we just created from the frontend in a React component when a user clicks a button:
```ts
'use client';
import React, { useState } from 'react';
import { createPost } from '../server/actions'; // import the server function
export const PostForm: React.FC = () => {
const [result, setResult] = useState<string>('');
return (
<>
<p>{result}</p>
<button
type="button"
onClick={async () => {
// Call the server function
const newPost = await createPost({ title: 'Sample Post' });
setResult('Post created: ' + newPost.title);
}}
>
Create Post
</button>
</>
);
};
```
### Updating a Document
The key points from the previous example also apply here.
To update a document instead of creating one, you would use `payload.update()` with the relevant data and **passing the document ID.**
Here's how the server function would look:
```ts
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function updatePost(id, data) {
const payload = await getPayload({ config })
try {
const post = await payload.update({
collection: 'posts',
id, // the document id is required
data,
})
return post
} catch (error) {
throw new Error(`Error updating post: ${error.message}`)
}
}
```
Here is how you would call the `updatePost` function from a frontend React component:
```ts
'use client';
import React, { useState } from 'react';
import { updatePost } from '../server/actions'; // import the server function
export const UpdatePostForm: React.FC = () => {
const [result, setResult] = useState<string>('');
return (
<>
<p>{result}</p>
<button
type="button"
onClick={async () => {
// Call the server function to update the post
const updatedPost = await updatePost('your-post-id-123', { title: 'Updated Post' });
setResult('Post updated: ' + updatedPost.title);
}}
>
Update Post
</button>
</>
);
};
```
### Authenticating a User
In this example, we will check if a user is authenticated using Payload's authentication system. Here's how it works:
- First, we use the headers function from `next/headers` to retrieve the request headers.
- Next, we pass these headers to `payload.auth()` to fetch the user's authentication details.
- If the user is authenticated, their information is returned. If not, handle the unauthenticated case accordingly.
Here's the server function to authenticate a user:
```ts
'use server'
import { headers as getHeaders } from 'next/headers'
import config from '@payload-config'
import { getPayload } from 'payload'
export const authenticateUser = async () => {
const payload = await getPayload({ config })
const headers = await getHeaders()
const { user } = await payload.auth({ headers })
if (user) {
return { hello: user.email }
}
return { hello: 'Not authenticated' }
}
```
Here's a basic example of how to call the authentication server function from the frontend to test it:
```ts
'use client';
import React, { useState } from 'react';
import { authenticateUser } from '../server/actions'; // Import the server function
export const AuthComponent: React.FC = () => {
const [userInfo, setUserInfo] = useState<string>('');
return (
<React.Fragment>
<p>{userInfo}</p>
<button
onClick={async () => {
// Call the server function to authenticate the user
const result = await authenticateUser();
setUserInfo(result.hello);
}}
type="button"
>
Check Authentication
</button>
</React.Fragment>
);
};
```
### Creating a Document with File Upload
This example demonstrates how to write a server function that creates a document with a file upload. Here are the key steps:
- Pass two arguments: **data** for the document content and **upload** for the file
- Merge the upload file into the document data as the media field
- Use `payload.create()` to create a new post document with both the document data and file
```ts
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function createPostWithUpload(data, upload) {
const payload = await getPayload({ config })
try {
// Prepare the data with the file
const postData = {
...data,
media: upload,
}
const post = await payload.create({
collection: 'posts',
data: postData,
})
return post
} catch (error) {
throw new Error(`Error creating post: ${error.message}`)
}
}
```
Here is how you would use the server function we just created in a frontend component to allow users to submit a post along with a file upload:
- The user enters the post title and selects a file to upload.
- When the form is submitted, the `handleSubmit` function checks if a file has been chosen.
- If a file is selected, it passes both the title and the file to the `createPostWithFile` server function.
- And you are done!
```ts
'use client';
import React, { useState } from 'react';
import { createPostWithUpload } from '../server/actions';
export const PostForm: React.FC = () => {
const [title, setTitle] = useState<string>('');
const [file, setFile] = useState<File | null>(null);
const [result, setResult] = useState<string>('');
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFile(e.target.files[0]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
setResult('Please upload a file.');
return;
}
try {
// Call the server function to create the post with the file
const newPost = await createPostWithUpload({ title }, file);
setResult('Post created with file: ' + newPost.title);
} catch (error) {
setResult('Error: ' + error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post Title"
/>
<input type="file" onChange={handleFileChange} />
<button type="submit">Create Post</button>
<p>{result}</p>
</form>
);
};
```
## Reusable Payload Server Functions
Managing authentication with the Local API can be tricky as you have to handle cookies and tokens yourself, and there aren't built-in logout or refresh functions since these only modify cookies. To make this easier, we provide `login`, `logout`, and `refresh` as ready-to-use server functions. They take care of the underlying complexity so you don't have to.
### Login
Logs in a user by verifying credentials and setting the authentication cookie. This function allows login via username or email, depending on the collection auth configuration.
#### Importing the `login` function
```ts
import { login } from '@payloadcms/next/auth'
```
The login function needs your Payload config, which cannot be imported in a client component. To work around this, create a simple server function like the one below, and call it from your client.
```ts
'use server'
import { login } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function loginAction({
email,
password,
}: {
email: string
password: string
}) {
try {
const result = await login({
collection: 'users',
config,
email,
password,
})
return result
} catch (error) {
throw new Error(
`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
```
#### Login from the React Client Component
```tsx
'use client'
import { useState } from 'react'
import { loginAction } from '../loginAction'
export default function LoginForm() {
const [email, setEmail] = useState<string>('')
const [password, setPassword] = useState<string>('')
return (
<form onSubmit={() => loginAction({ email, password })}>
<label htmlFor="email">Email</label>
<input
id="email"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEmail(e.target.value)
}
type="email"
value={email}
/>
<label htmlFor="password">Password</label>
<input
id="password"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
type="password"
value={password}
/>
<button type="submit">Login</button>
</form>
)
}
```
### Logout
Logs out the current user by clearing the authentication cookie.
#### Importing the `logout` function
```ts
import { logout } from '@payloadcms/next/auth'
```
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below.
```ts
'use server'
import { logout } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function logoutAction() {
try {
return await logout({ config })
} catch (error) {
throw new Error(
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
```
#### Logout from the React Client Component
```tsx
'use client'
import { logoutAction } from '../logoutAction'
export default function LogoutButton() {
return <button onClick={() => logoutFunction()}>Logout</button>
}
```
### Refresh
Refreshes the authentication token for the logged-in user.
#### Importing the `refresh` function
```ts
import { refresh } from '@payloadcms/next/auth'
```
As with login and logout, you need to pass your Payload config to this function. Create a helper server function like the one below. Passing the config directly to the client is not possible and will throw errors.
```ts
'use server'
import { refresh } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function refreshAction() {
try {
return await refresh({
collection: 'users', // pass your collection slug
config,
})
} catch (error) {
throw new Error(
`Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
```
#### Using Refresh from the React Client Component
```tsx
'use client'
import { refreshAction } from '../actions/refreshAction'
export default function RefreshTokenButton() {
return <button onClick={() => refreshFunction()}>Refresh</button>
}
```
## Error Handling in Server Functions
When using server functions, proper error handling is essential to prevent unhandled exceptions and provide meaningful feedback to the frontend.
### Best Practices#error-handling-best-practices
- Wrap Local API calls in **try/catch blocks** to catch potential errors.
- **Log errors** on the server for debugging purposes.
- Return structured **error responses** instead of exposing raw errors to the frontend.
Example of good error handling:
```ts
export async function createPost(data) {
try {
const payload = await getPayload({ config })
return await payload.create({ collection: 'posts', data })
} catch (error) {
console.error('Error creating post:', error)
return { error: 'Failed to create post' }
}
}
```
## Security Considerations
Using server functions helps prevent direct exposure of Local API operations to the frontend, but additional security best practices should be followed:
### Best Practices#security-best-practices
- **Restrict access**: Ensure that sensitive actions (like user management) are only callable by authorized users.
- **Avoid passing sensitive data**: Do not return sensitive information such as user data, passwords, etc.
- **Use authentication & authorization**: Check user roles before performing actions.
Example of restricting access based on user role:
```ts
export async function deletePost(postId, user) {
if (!user || user.role !== 'admin') {
throw new Error('Unauthorized')
}
const payload = await getPayload({ config })
return await payload.delete({ collection: 'posts', id: postId })
}
```

View File

@@ -55,18 +55,9 @@ Because _**you**_ are in complete control of who can do what with your data, you
wield that power responsibly before deploying to Production.
<Banner type="error">
**
By default, all Access Control functions require that a user is successfully logged in to
Payload to create, read, update, or delete data.
**
But, if you allow public user registration, for example, you will want to make sure that your
access control functions are more strict - permitting
**By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.**
**
only appropriate users
**
to perform appropriate actions.
But, if you allow public user registration, for example, you will want to make sure that your access control functions are more strict - permitting **only appropriate users** to perform appropriate actions.
</Banner>

View File

@@ -0,0 +1,171 @@
---
title: Query Presets
label: Overview
order: 10
desc: Query Presets allow you to save and share filters, columns, and sort orders for your collections.
keywords:
---
Query Presets allow you to save and share filters, columns, and sort orders for your [Collections](../configuration/collections). This is useful for reusing common or complex filtering patterns and/or sharing them across your team.
Each Query Preset is saved as a new record in the database under the `payload-query-presets` collection. This allows for an endless number of preset configurations, where the users of your app define the presets that are most useful to them, rather than being hard coded into the Payload Config.
Within the [Admin Panel](../admin/overview), Query Presets are applied to the List View. When enabled, new controls are displayed for users to manage presets. Once saved, these presets can be loaded up at any time and optionally shared with others.
To enable Query Presets on a Collection, use the `enableQueryPresets` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
// highlight-start
enableQueryPresets: true,
// highlight-end
}
```
## Config Options
While not required, you may want to customize the behavior of Query Presets to suit your needs, such as add custom labels or access control rules.
Settings for Query Presets are managed on the `queryPresets` property at the root of your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
// highlight-start
queryPresets: {
// ...
},
// highlight-end
})
```
The following options are available for Query Presets:
| Option | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
| `labels` | Custom labels to use for the Query Presets collection. |
## Access Control
Query Presets are subject to the same [Access Control](../access-control/overview) as the rest of Payload. This means you can use the same patterns you are already familiar with to control who can read, update, and delete presets.
Access Control for Query Presets can be customized in two ways:
1. [Collection Access Control](#collection-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
### Collection Access Control
Collection-level access control applies to _all_ presets within the Query Presets collection. Users cannot control these rules, they are written statically in your config.
To add Collection Access Control, use the `queryPresets.access` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
queryPresets: {
// ...
// highlight-start
access: {
read: ({ req: { user } }) =>
user ? user?.roles?.some((role) => role === 'admin') : false,
update: ({ req: { user } }) =>
user ? user?.roles?.some((role) => role === 'admin') : false,
},
// highlight-end
},
})
```
This example restricts all Query Presets to users with the role of `admin`.
<Banner type="warning">
**Note:** Custom access control will override the defaults on this collection,
including the requirement for a user to be authenticated. Be sure to include
any necessary checks in your custom rules unless you intend on making these
publicly accessible.
</Banner>
### Document Access Control
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each document.
When a user manages a preset, document-level access control options will be available to them in the Admin Panel for each operation.
By default, Payload provides a set of sensible defaults for all Query Presets, but you can customize these rules to suit your needs:
- **Only Me**: Only the user who created the preset can read, update, and delete it.
- **Everyone**: All users can read, update, and delete the preset.
- **Specific Users**: Only select users can read, update, and delete the preset.
#### Custom Access Control
You can augment the default access control rules with your own custom rules. This can be useful for creating more complex access control patterns that the defaults don't provide, such as for RBAC.
Adding custom access control rules requires:
1. A label to display in the dropdown
2. A set of fields to conditionally render when that option is selected
3. A function that returns the access control rules for that option
To do this, use the `queryPresets.constraints` property in your [Payload Config](../configuration/payload-config).
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
queryPresets: {
// ...
// highlight-start
constraints: {
read: {
label: 'Specific Roles',
value: 'specificRoles',
fields: [
{
name: 'roles',
type: 'select',
hasMany: true,
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
],
},
],
access: ({ req: { user } }) => ({
'access.read.roles': {
in: [user?.roles],
},
}),
},
// highlight-end
},
},
})
```
In this example, we've added a new option called `Specific Roles` that allows users to select from a list of roles. When this option is selected, the user will be prompted to select one or more roles from a list of options. The access control rule for this option is that the user operating on the preset must have one of the selected roles.
<Banner type="warning">
**Note:** Payload places your custom fields into the `access[operation]` field
group, so your rules will need to reflect this.
</Banner>
The following options are available for each constraint:
| Option | Description |
| -------- | ------------------------------------------------------------------------ |
| `label` | The label to display in the dropdown for this constraint. |
| `value` | The value to store in the database when this constraint is selected. |
| `fields` | An array of fields to render when this constraint is selected. |
| `access` | A function that determines the access control rules for this constraint. |

View File

@@ -21,7 +21,7 @@ import {
// Your richtext data here
const data: SerializedEditorState = {}
const html = convertLexicalToMarkdown({
const markdown = convertLexicalToMarkdown({
data,
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
@@ -101,7 +101,7 @@ import {
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
const html = convertMarkdownToLexical({
const lexicalJSON = convertMarkdownToLexical({
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),

View File

@@ -223,7 +223,7 @@ This allows you to add i18n translations scoped to your feature. This specific e
### Markdown Transformers#server-feature-markdown-transformers
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converters#markdown-lexical).
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converting-markdown).
```ts
import { createServerFeature } from '@payloadcms/richtext-lexical'
@@ -409,7 +409,7 @@ Explore the APIs available through ClientFeature to add the specific functionali
### Adding a client feature to the server feature
Inside of your server feature, you can provide an [import path](/docs/admin/custom-components/overview#component-paths) to the client feature like this:
Inside of your server feature, you can provide an [import path](/docs/custom-components/overview#component-paths) to the client feature like this:
```ts
import { createServerFeature } from '@payloadcms/richtext-lexical'

View File

@@ -334,12 +334,28 @@ To upload a file, use your collection's [`create`](/docs/rest-api/overview#colle
Send your request as a `multipart/form-data` request, using [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) if possible.
<Banner type="info">
**Note:** To include any additional fields (like `title`, `alt`, etc.), append
a `_payload` field containing a JSON-stringified object of the required
values. These values must match the schema of your upload-enabled collection.
</Banner>
```ts
const fileInput = document.querySelector('#your-file-input')
const formData = new FormData()
formData.append('file', fileInput.files[0])
// Replace with the fields defined in your upload-enabled collection.
// The example below includes an optional field like 'title'.
formData.append(
'_payload',
JSON.stringify({
title: 'Example Title',
description: 'An optional description for the file',
}),
)
fetch('api/:upload-slug', {
method: 'POST',
body: formData,

View File

@@ -22,6 +22,7 @@ Collections and Globals both support the same options for configuring autosave.
| Drafts Autosave Options | Description |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. |
| `showSaveDraftButton` | Set this to `true` to show the "Save as draft" button even while autosave is enabled. Defaults to `false`. |
**Example config with versions, drafts, and autosave enabled:**
@@ -50,9 +51,13 @@ export const Pages: CollectionConfig = {
drafts: {
autosave: true,
// Alternatively, you can specify an `interval`:
// Alternatively, you can specify an object to customize autosave:
// autosave: {
// Define how often the document should be autosaved (in milliseconds)
// interval: 1500,
//
// Show the "Save as draft" button even while autosave is enabled
// showSaveDraftButton: true,
// },
},
},

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.30.0",
"version": "3.35.1",
"private": true,
"type": "module",
"scripts": {
@@ -87,6 +87,8 @@
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
"script:gen-templates": "pnpm --filter scripts gen-templates",
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
"script:license-check": "pnpm --filter scripts license-check",
"script:list-published": "pnpm --filter releaser list-published",
"script:pack": "pnpm --filter scripts pack-all-to-dest",
"pretest": "pnpm build",
@@ -118,7 +120,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.2.3",
"@next/bundle-analyzer": "15.3.0",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -133,8 +135,8 @@
"@types/jest": "29.5.12",
"@types/minimist": "1.2.5",
"@types/node": "22.5.4",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/shelljs": "0.8.15",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -154,14 +156,14 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^10",
"next": "15.2.3",
"next": "15.3.0",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.50.0",
"playwright-core": "1.50.0",
"prettier": "3.5.3",
"react": "19.0.0",
"react-dom": "19.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"rimraf": "6.0.1",
"sharp": "0.32.6",
"shelljs": "0.8.5",

View File

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

View File

@@ -1,9 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ import type {
TypeWithVersion,
UpdateGlobalArgs,
UpdateGlobalVersionArgs,
UpdateManyArgs,
UpdateOneArgs,
UpdateVersionArgs,
} from 'payload'
@@ -55,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 { updateJobs } from './updateJobs.js'
import { updateMany } from './updateMany.js'
import { updateOne } from './updateOne.js'
import { updateVersion } from './updateVersion.js'
@@ -227,6 +227,7 @@ export function mongooseAdapter({
mongoMemoryServer,
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
updateJobs,
updateMany,
url,
versions: {},
@@ -272,6 +273,7 @@ export function mongooseAdapter({
}
return {
name: 'mongoose',
allowIDOnCreate,
defaultIDType: 'text',
init: adapter,

View File

@@ -2,7 +2,7 @@ import type { FilterQuery } from 'mongoose'
import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload'
import { Types } from 'mongoose'
import { APIError, getLocalizedPaths } from 'payload'
import { APIError, getFieldByPath, getLocalizedPaths } from 'payload'
import { validOperatorSet } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
@@ -138,7 +138,7 @@ export async function buildSearchParam({
throw new APIError(`Collection with the slug ${collectionSlug} was not found.`)
}
const { Model: SubModel } = getCollection({
const { collectionConfig, Model: SubModel } = getCollection({
adapter: payload.db as MongooseAdapter,
collectionSlug,
})
@@ -154,22 +154,72 @@ export async function buildSearchParam({
},
})
const result = await SubModel.find(subQuery, subQueryOptions)
const field = paths[0].field
const select: Record<string, boolean> = {
_id: true,
}
let joinPath: null | string = null
if (field.type === 'join') {
const relationshipField = getFieldByPath({
fields: collectionConfig.flattenedFields,
path: field.on,
})
if (!relationshipField) {
throw new APIError('Relationship field was not found')
}
let path = relationshipField.localizedPath
if (relationshipField.pathHasLocalized && payload.config.localization) {
path = path.replace('<locale>', locale || payload.config.localization.defaultLocale)
}
select[path] = true
joinPath = path
}
if (joinPath) {
select[joinPath] = true
}
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
const $in: unknown[] = []
result.forEach((doc) => {
const stringID = doc._id.toString()
$in.push(stringID)
result.forEach((doc: any) => {
if (joinPath) {
let ref = doc
if (Types.ObjectId.isValid(stringID)) {
$in.push(doc._id)
for (const segment of joinPath.split('.')) {
if (typeof ref === 'object' && ref) {
ref = ref[segment]
}
}
if (Array.isArray(ref)) {
for (const item of ref) {
if (item instanceof Types.ObjectId) {
$in.push(item)
}
}
} else if (ref instanceof Types.ObjectId) {
$in.push(ref)
}
} else {
const stringID = doc._id.toString()
$in.push(stringID)
if (Types.ObjectId.isValid(stringID)) {
$in.push(doc._id)
}
}
})
if (pathsToQuery.length === 1) {
return {
path,
path: joinPath ? '_id' : path,
value: { $in },
}
}

View File

@@ -81,7 +81,19 @@ export async function parseParams({
[searchParam.path]: searchParam.value,
})
} else {
result[searchParam.path] = searchParam.value
if (result[searchParam.path]) {
if (!result.$and) {
result.$and = []
}
result.$and.push({ [searchParam.path]: result[searchParam.path] })
result.$and.push({
[searchParam.path]: searchParam.value,
})
delete result[searchParam.path]
} else {
result[searchParam.path] = searchParam.value
}
}
} else if (typeof searchParam?.value === 'object') {
result = deepMergeWithCombinedArrays(result, searchParam.value ?? {}, {

View File

@@ -0,0 +1,102 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { BaseJob, UpdateJobs, Where } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
import { transform } from './utilities/transform.js'
export const updateJobs: UpdateJobs = async function updateMany(
this: MongooseAdapter,
{ id, data, limit, req, returning, sort: sortArg, where: whereArg },
) {
if (!(data?.log as object[])?.length) {
delete data.log
}
const where = id ? { id: { equals: id } } : (whereArg as Where)
const { collectionConfig, Model } = getCollection({
adapter: this,
collectionSlug: 'payload-jobs',
})
const sort: Record<string, unknown> | undefined = buildSortParam({
adapter: this,
config: this.payload.config,
fields: collectionConfig.flattenedFields,
sort: sortArg || collectionConfig.defaultSort,
timestamps: true,
})
const options: MongooseUpdateQueryOptions = {
lean: true,
new: true,
session: await getSession(this, req),
}
let query = await buildQuery({
adapter: this,
collectionSlug: collectionConfig.slug,
fields: collectionConfig.flattenedFields,
where,
})
transform({ adapter: this, data, fields: collectionConfig.fields, operation: 'write' })
let result: BaseJob[] = []
try {
if (id) {
if (returning === false) {
await Model.updateOne(query, data, options)
return null
} else {
const doc = await Model.findOneAndUpdate(query, data, options)
result = doc ? [doc] : []
}
} else {
if (typeof limit === 'number' && limit > 0) {
const documentsToUpdate = await Model.find(
query,
{},
{ ...options, limit, projection: { _id: 1 }, sort },
)
if (documentsToUpdate.length === 0) {
return null
}
query = { _id: { $in: documentsToUpdate.map((doc) => doc._id) } }
}
await Model.updateMany(query, data, options)
if (returning === false) {
return null
}
result = await Model.find(
query,
{},
{
...options,
sort,
},
)
}
} catch (error) {
handleError({ collection: collectionConfig.slug, error, req })
}
transform({
adapter: this,
data: result,
fields: collectionConfig.fields,
operation: 'read',
})
return result
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.30.0",
"version": "3.35.1",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -25,9 +25,9 @@
"default": "./src/index.ts"
},
"./types": {
"import": "./src/types.ts",
"types": "./src/types.ts",
"default": "./src/types.ts"
"import": "./src/exports/types-deprecated.ts",
"types": "./src/exports/types-deprecated.ts",
"default": "./src/exports/types-deprecated.ts"
},
"./migration-utils": {
"import": "./src/exports/migration-utils.ts",
@@ -56,7 +56,7 @@
}
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"types": "./src/index.ts",
"files": [
"dist",
"mock.js"
@@ -102,9 +102,9 @@
"default": "./dist/index.js"
},
"./types": {
"import": "./dist/types.js",
"types": "./dist/types.d.ts",
"default": "./dist/types.js"
"import": "./dist/exports/types-deprecated.js",
"types": "./dist/exports/types-deprecated.d.ts",
"default": "./dist/exports/types-deprecated.js"
},
"./migration-utils": {
"import": "./dist/exports/migration-utils.js",

View File

@@ -3,7 +3,6 @@ import type { Connect, Migration, Payload } from 'payload'
import { pushDevSchema } from '@payloadcms/drizzle'
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
import type { PostgresAdapter } from './types.js'
@@ -61,7 +60,7 @@ export const connect: Connect = async function connect(
try {
if (!this.pool) {
this.pool = new pg.Pool(this.poolOptions)
this.pool = new this.pg.Pool(this.poolOptions)
await connectWithReconnect({ adapter: this, payload: this.payload })
}

View File

@@ -0,0 +1,20 @@
import type {
Args as _Args,
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
PostgresAdapter as _PostgresAdapter,
} from '../types.js'
/**
* @deprecated - import from `@payloadcms/db-postgres` instead
*/
export type Args = _Args
/**
* @deprecated - import from `@payloadcms/db-postgres` instead
*/
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
/**
* @deprecated - import from `@payloadcms/db-postgres` instead
*/
export type PostgresAdapter = _PostgresAdapter

View File

@@ -33,6 +33,7 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateJobs,
updateMany,
updateOne,
updateVersion,
@@ -53,6 +54,7 @@ import {
} from '@payloadcms/drizzle/postgres'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
import pgDependency from 'pg'
import { fileURLToPath } from 'url'
import type { Args, PostgresAdapter } from './types.js'
@@ -129,6 +131,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
operators: operatorMap,
pg: args.pg || pgDependency,
pgSchema: adapterSchema,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
pool: undefined,
@@ -172,6 +175,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
find,
findGlobal,
findGlobalVersions,
updateJobs,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
findOne,
findVersions,
@@ -206,12 +210,18 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
}
return {
name: 'postgres',
allowIDOnCreate,
defaultIDType: payloadIDType,
init: adapter,
}
}
export type {
Args as PostgresAdapterArgs,
GeneratedDatabaseSchema,
PostgresAdapter,
} from './types.js'
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
export { geometryColumn } from '@payloadcms/drizzle/postgres'
export { sql } from 'drizzle-orm'

View File

@@ -12,6 +12,8 @@ import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
import type { Pool, PoolConfig } from 'pg'
type PgDependency = typeof import('pg')
export type Args = {
/**
* Transform the schema after it's built.
@@ -45,6 +47,7 @@ export type Args = {
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
pg?: PgDependency
pool: PoolConfig
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
@@ -74,6 +77,7 @@ type ResolveSchemaType<T> = 'schema' extends keyof T
type Drizzle = NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
export type PostgresAdapter = {
drizzle: Drizzle
pg: PgDependency
pool: Pool
poolOptions: PoolConfig
} & BasePostgresAdapter
@@ -98,6 +102,8 @@ declare module 'payload' {
initializing: Promise<void>
localesSuffix?: string
logger: DrizzleConfig['logger']
/** Optionally inject your own node-postgres. This is required if you wish to instrument the driver with @payloadcms/plugin-sentry. */
pg?: PgDependency
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.30.0",
"version": "3.35.1",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -25,9 +25,9 @@
"types": "./src/index.ts"
},
"./types": {
"import": "./src/types.ts",
"require": "./src/types.ts",
"types": "./src/types.ts"
"import": "./src/exports/types-deprecated.ts",
"require": "./src/exports/types-deprecated.ts",
"types": "./src/exports/types-deprecated.ts"
},
"./migration-utils": {
"import": "./src/exports/migration-utils.ts",
@@ -56,7 +56,7 @@
}
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"types": "./src/index.ts",
"files": [
"dist",
"mock.js"
@@ -85,6 +85,7 @@
"@payloadcms/eslint-config": "workspace:*",
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"@types/uuid": "10.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
@@ -98,9 +99,9 @@
"types": "./dist/index.d.ts"
},
"./types": {
"import": "./dist/types.js",
"require": "./dist/types.js",
"types": "./dist/types.d.ts"
"import": "./dist/exports/types-deprecated.js",
"require": "./dist/exports/types-deprecated.js",
"types": "./dist/exports/types-deprecated.d.ts"
},
"./migration-utils": {
"import": "./dist/exports/migration-utils.js",

View File

@@ -23,6 +23,9 @@ export const columnToCodeConverter: ColumnToCodeConverter = ({
case 'enum': {
let options: string[]
if ('locale' in column) {
if (!locales?.length) {
throw new Error('Locales must be defined for locale columns')
}
options = locales
} else {
options = column.options

View File

@@ -1,5 +1,5 @@
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { Connect } from 'payload'
import type { Connect, Migration } from 'payload'
import { createClient } from '@libsql/client'
import { pushDevSchema } from '@payloadcms/drizzle'
@@ -36,7 +36,8 @@ export const connect: Connect = async function connect(
}
}
} catch (err) {
this.payload.logger.error({ err, msg: `Error: cannot connect to SQLite: ${err.message}` })
const message = err instanceof Error ? err.message : String(err)
this.payload.logger.error({ err, msg: `Error: cannot connect to SQLite: ${message}` })
if (typeof this.rejectInitializing === 'function') {
this.rejectInitializing()
}
@@ -57,6 +58,6 @@ export const connect: Connect = async function connect(
}
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
await this.migrate({ migrations: this.prodMigrations as Migration[] })
}
}

View File

@@ -1,6 +1,5 @@
import type { ChainedMethods } from '@payloadcms/drizzle/types'
import type { SQLiteSelect } from 'drizzle-orm/sqlite-core'
import { chainMethods } from '@payloadcms/drizzle'
import { count, sql } from 'drizzle-orm'
import type { CountDistinct, SQLiteAdapter } from './types.js'
@@ -17,33 +16,28 @@ export const countDistinct: CountDistinct = async function countDistinct(
})
.from(this.tables[tableName])
.where(where)
return Number(countResult[0].count)
return Number(countResult[0]?.count)
}
const chainedMethods: ChainedMethods = []
let query: SQLiteSelect = db
.select({
count: sql`COUNT(1) OVER()`,
})
.from(this.tables[tableName])
.where(where)
.groupBy(this.tables[tableName].id)
.limit(1)
.$dynamic()
joins.forEach(({ condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
query = query.leftJoin(table, condition)
})
// When we have any joins, we need to count each individual ID only once.
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
const countResult = await chainMethods({
methods: chainedMethods,
query: db
.select({
count: sql`COUNT(1) OVER()`,
})
.from(this.tables[tableName])
.where(where)
.groupBy(this.tables[tableName].id)
.limit(1),
})
const countResult = await query
return Number(countResult[0].count)
return Number(countResult[0]?.count)
}

View File

@@ -74,7 +74,7 @@ export const createJSONQuery = ({
treatAsArray,
value,
}: CreateJSONQueryArgs): string => {
if (treatAsArray.includes(pathSegments[1])) {
if (treatAsArray?.includes(pathSegments[1]!) && table) {
return fromArray({
operator,
pathSegments,

View File

@@ -1,6 +1,11 @@
import type { DeleteWhere } from './types.js'
import type { DeleteWhere, SQLiteAdapter } from './types.js'
export const deleteWhere: DeleteWhere = async function deleteWhere({ db, tableName, where }) {
export const deleteWhere: DeleteWhere = async function (
// Here 'this' is not a parameter. See:
// https://www.typescriptlang.org/docs/handbook/2/classes.html#this-parameters
this: SQLiteAdapter,
{ db, tableName, where },
) {
const table = this.tables[tableName]
await db.delete(table).where(where)
}

View File

@@ -1,21 +1,23 @@
import type { DropDatabase } from './types.js'
import type { Row } from '@libsql/client'
const getTables = (adapter) => {
import type { DropDatabase, SQLiteAdapter } from './types.js'
const getTables = (adapter: SQLiteAdapter) => {
return adapter.client.execute(`SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%';`)
}
const dropTables = (adapter, rows) => {
const dropTables = (adapter: SQLiteAdapter, rows: Row[]) => {
const multi = `
PRAGMA foreign_keys = OFF;\n
${rows.map(({ name }) => `DROP TABLE IF EXISTS ${name}`).join(';\n ')};\n
${rows.map(({ name }) => `DROP TABLE IF EXISTS ${name as string}`).join(';\n ')};\n
PRAGMA foreign_keys = ON;`
return adapter.client.executeMultiple(multi)
}
export const dropDatabase: DropDatabase = async function dropDatabase({ adapter }) {
export const dropDatabase: DropDatabase = async function ({ adapter }) {
const result = await getTables(adapter)
await dropTables(adapter, result.rows)
}

View File

@@ -3,13 +3,13 @@ import { sql } from 'drizzle-orm'
import type { Execute } from './types.js'
export const execute: Execute<any> = function execute({ db, drizzle, raw, sql: statement }) {
const executeFrom = db ?? drizzle
const executeFrom = (db ?? drizzle)!
if (raw) {
const result = executeFrom.run(sql.raw(raw))
return result
} else {
const result = executeFrom.run(statement)
const result = executeFrom.run(statement!)
return result
}
}

View File

@@ -0,0 +1,79 @@
import type {
Args as _Args,
CountDistinct as _CountDistinct,
DeleteWhere as _DeleteWhere,
DropDatabase as _DropDatabase,
Execute as _Execute,
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
GenericColumns as _GenericColumns,
GenericRelation as _GenericRelation,
GenericTable as _GenericTable,
IDType as _IDType,
Insert as _Insert,
MigrateDownArgs as _MigrateDownArgs,
MigrateUpArgs as _MigrateUpArgs,
SQLiteAdapter as _SQLiteAdapter,
SQLiteSchemaHook as _SQLiteSchemaHook,
} from '../types.js'
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type SQLiteAdapter = _SQLiteAdapter
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type Args = _Args
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type CountDistinct = _CountDistinct
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type DeleteWhere = _DeleteWhere
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type DropDatabase = _DropDatabase
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type Execute<T> = _Execute<T>
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type GenericColumns = _GenericColumns
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type GenericRelation = _GenericRelation
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type GenericTable = _GenericTable
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type IDType = _IDType
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type Insert = _Insert
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type MigrateDownArgs = _MigrateDownArgs
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type MigrateUpArgs = _MigrateUpArgs
/**
* @deprecated - import from `@payloadcms/db-sqlite` instead
*/
export type SQLiteSchemaHook = _SQLiteSchemaHook

View File

@@ -34,6 +34,7 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateJobs,
updateMany,
updateOne,
updateVersion,
@@ -57,10 +58,6 @@ import { init } from './init.js'
import { insert } from './insert.js'
import { requireDrizzleKit } from './requireDrizzleKit.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
const filename = fileURLToPath(import.meta.url)
export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
@@ -92,9 +89,11 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
allowIDOnCreate,
autoIncrement: args.autoIncrement ?? false,
beforeSchemaInit: args.beforeSchemaInit ?? [],
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
client: undefined,
clientConfig: args.client,
defaultDrizzleSnapshot,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
drizzle: undefined,
features: {
json: true,
@@ -112,6 +111,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
logger: args.logger,
operators,
prodMigrations: args.prodMigrations,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
push: args.push,
rawRelations: {},
rawTables: {},
@@ -122,7 +122,9 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
sessions: {},
tableNameMap: new Map<string, string>(),
tables: {},
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
transactionOptions: args.transactionOptions || undefined,
updateJobs,
updateMany,
versionsSuffix: args.versionsSuffix || '_v',
@@ -160,6 +162,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
find,
findGlobal,
findGlobalVersions,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
findOne,
findVersions,
indexes: new Set<string>(),
@@ -175,8 +178,10 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
packageName: '@payloadcms/db-sqlite',
payload,
queryDrafts,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
rejectInitializing,
requireDrizzleKit,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
resolveInitializing,
rollbackTransaction,
updateGlobal,
@@ -188,8 +193,32 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
}
return {
name: 'sqlite',
allowIDOnCreate,
defaultIDType: payloadIDType,
init: adapter,
}
}
/**
* @todo deprecate /types subpath export in 4.0
*/
export type {
Args as SQLiteAdapterArgs,
CountDistinct,
DeleteWhere,
DropDatabase,
Execute,
GeneratedDatabaseSchema,
GenericColumns,
GenericRelation,
GenericTable,
IDType,
Insert,
MigrateDownArgs,
MigrateUpArgs,
SQLiteAdapter,
SQLiteSchemaHook,
} from './types.js'
export { sql } from 'drizzle-orm'

View File

@@ -28,7 +28,7 @@ export const init: Init = async function init(this: SQLiteAdapter) {
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
for (const tableName in this.rawTables) {
buildDrizzleTable({ adapter, locales, rawTable: this.rawTables[tableName] })
buildDrizzleTable({ adapter, locales: locales!, rawTable: this.rawTables[tableName]! })
}
buildDrizzleRelations({

View File

@@ -1,19 +1,17 @@
import type { Insert } from './types.js'
import type { Insert, SQLiteAdapter } from './types.js'
export const insert: Insert = async function insert({
db,
onConflictDoUpdate,
tableName,
values,
}): Promise<Record<string, unknown>[]> {
export const insert: Insert = async function (
// Here 'this' is not a parameter. See:
// https://www.typescriptlang.org/docs/handbook/2/classes.html#this-parameters
this: SQLiteAdapter,
{ db, onConflictDoUpdate, tableName, values },
): Promise<Record<string, unknown>[]> {
const table = this.tables[tableName]
let result
if (onConflictDoUpdate) {
result = db.insert(table).values(values).onConflictDoUpdate(onConflictDoUpdate).returning()
} else {
result = db.insert(table).values(values).returning()
}
result = await result
return result
const result = onConflictDoUpdate
? await db.insert(table).values(values).onConflictDoUpdate(onConflictDoUpdate).returning()
: await db.insert(table).values(values).returning()
// See https://github.com/payloadcms/payload/pull/11831#discussion_r2010431908
return result as Record<string, unknown>[]
}

View File

@@ -81,8 +81,9 @@ export const buildDrizzleTable: BuildDrizzleTable = ({ adapter, locales, rawTabl
}
if (column.reference) {
columns[key].references(() => adapter.tables[column.reference.table][column.reference.name], {
onDelete: column.reference.onDelete,
const ref = column.reference
columns[key].references(() => adapter.tables[ref.table][ref.name], {
onDelete: ref.onDelete,
})
}

View File

@@ -1,10 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [
{
"path": "../payload"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.30.0",
"version": "3.35.1",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -25,9 +25,9 @@
"default": "./src/index.ts"
},
"./types": {
"import": "./src/types.ts",
"types": "./src/types.ts",
"default": "./src/types.ts"
"import": "./src/exports/types-deprecated.ts",
"types": "./src/exports/types-deprecated.ts",
"default": "./src/exports/types-deprecated.ts"
},
"./migration-utils": {
"import": "./src/exports/migration-utils.ts",
@@ -56,7 +56,7 @@
}
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"types": "./src/index.ts",
"files": [
"dist",
"mock.js"
@@ -103,9 +103,9 @@
"default": "./dist/index.js"
},
"./types": {
"import": "./dist/types.js",
"types": "./dist/types.d.ts",
"default": "./dist/types.js"
"import": "./dist/exports/types-deprecated.js",
"types": "./dist/exports/types-deprecated.d.ts",
"default": "./dist/exports/types-deprecated.js"
},
"./migration-utils": {
"import": "./dist/exports/migration-utils.js",

View File

@@ -1,5 +1,5 @@
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { Connect } from 'payload'
import type { Connect, Migration } from 'payload'
import { pushDevSchema } from '@payloadcms/drizzle'
import { sql, VercelPool } from '@vercel/postgres'
@@ -60,7 +60,8 @@ export const connect: Connect = async function connect(
this.payload.logger.info('---- DROPPED TABLES ----')
}
}
} catch (err) {
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error))
if (err.message?.match(/database .* does not exist/i) && !this.disableCreateDatabase) {
// capitalize first char of the err msg
this.payload.logger.info(
@@ -69,7 +70,7 @@ export const connect: Connect = async function connect(
const isCreated = await this.createDatabase()
if (isCreated) {
await this.connect(options)
await this.connect?.(options)
return
}
} else {
@@ -101,6 +102,6 @@ export const connect: Connect = async function connect(
}
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
await this.migrate({ migrations: this.prodMigrations as Migration[] })
}
}

View File

@@ -0,0 +1,20 @@
import type {
Args as _Args,
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
VercelPostgresAdapter as _VercelPostgresAdapter,
} from '../types.js'
/**
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
*/
export type Args = _Args
/**
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
*/
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
/**
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
*/
export type VercelPostgresAdapter = _VercelPostgresAdapter

View File

@@ -1,3 +1,4 @@
import type { PgTableFn } from 'drizzle-orm/pg-core'
import type { DatabaseAdapterObj, Payload } from 'payload'
import {
@@ -33,6 +34,7 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateJobs,
updateMany,
updateOne,
updateVersion,
@@ -80,10 +82,10 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
if (args.schemaName) {
adapterSchema = pgSchema(args.schemaName)
} else {
adapterSchema = { enum: pgEnum, table: pgTable }
adapterSchema = { enum: pgEnum, table: pgTable as unknown as PgTableFn<string> }
}
const extensions = (args.extensions ?? []).reduce((acc, name) => {
const extensions = (args.extensions ?? []).reduce<Record<string, boolean>>((acc, name) => {
acc[name] = true
return acc
}, {})
@@ -97,6 +99,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
createExtensions,
defaultDrizzleSnapshot,
disableCreateDatabase: args.disableCreateDatabase ?? false,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
drizzle: undefined,
enums: {},
extensions,
@@ -123,6 +126,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
pool: undefined,
poolOptions: args.pool,
prodMigrations: args.prodMigrations,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
push: args.push,
rawRelations: {},
rawTables: {},
@@ -135,6 +139,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
tables: {},
tablesFilter: args.tablesFilter,
transactionOptions: args.transactionOptions || undefined,
updateJobs,
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter
@@ -169,6 +174,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
find,
findGlobal,
findGlobalVersions,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
findOne,
findVersions,
init,
@@ -183,8 +189,10 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
packageName: '@payloadcms/db-vercel-postgres',
payload,
queryDrafts,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
rejectInitializing,
requireDrizzleKit,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
resolveInitializing,
rollbackTransaction,
updateGlobal,
@@ -197,12 +205,21 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
}
return {
name: 'postgres',
allowIDOnCreate,
defaultIDType: payloadIDType,
init: adapter,
}
}
/**
* @todo deprecate /types subpath export in 4.0
*/
export type {
Args as VercelPostgresAdapterArgs,
GeneratedDatabaseSchema,
VercelPostgresAdapter,
} from './types.js'
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
export { geometryColumn } from '@payloadcms/drizzle/postgres'
export { sql } from 'drizzle-orm'

View File

@@ -1,10 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [
{
"path": "../payload"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.30.0",
"version": "3.35.1",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {
@@ -30,13 +30,13 @@
"default": "./src/exports/postgres.ts"
},
"./types": {
"import": "./src/types.ts",
"types": "./src/types.ts",
"default": "./src/types.ts"
"import": "./src/exports/types-deprecated.ts",
"types": "./src/exports/types-deprecated.ts",
"default": "./src/exports/types-deprecated.ts"
}
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"types": "./src/index.ts",
"files": [
"dist",
"mock.js"
@@ -81,9 +81,9 @@
"default": "./dist/exports/postgres.js"
},
"./types": {
"import": "./dist/types.js",
"types": "./dist/types.d.ts",
"default": "./dist/types.js"
"import": "./dist/exports/types-deprecated.js",
"types": "./dist/exports/types-deprecated.d.ts",
"default": "./dist/exports/types-deprecated.js"
}
},
"main": "./dist/index.js",

View File

@@ -78,7 +78,9 @@ export const createTableName = ({
if (result.length > 63) {
throw new APIError(
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}.
Tip: You can use the dbName property to reduce the table name length.
`,
)
}

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, returning },
{ collection: collectionSlug, req, returning, select, where: whereArg },
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
@@ -32,9 +32,9 @@ export const deleteOne: DeleteOne = async function deleteOne(
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: [{ args: [1], method: 'limit' }],
db,
joins,
query: ({ query }) => query.limit(1),
selectFields,
tableName,
where,
@@ -59,6 +59,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
docToDelete = await db.query[tableName].findFirst(findManyArgs)
}
if (!docToDelete) {
return null
}
const result =
returning === false
? null
@@ -68,6 +72,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
data: docToDelete,
fields: collection.flattenedFields,
joinQuery: false,
tableName,
})
await this.deleteWhere({

View File

@@ -0,0 +1,188 @@
import type {
BaseRawColumn as _BaseRawColumn,
BuildDrizzleTable as _BuildDrizzleTable,
BuildQueryJoinAliases as _BuildQueryJoinAliases,
ChainedMethods as _ChainedMethods,
ColumnToCodeConverter as _ColumnToCodeConverter,
CountDistinct as _CountDistinct,
CreateJSONQueryArgs as _CreateJSONQueryArgs,
DeleteWhere as _DeleteWhere,
DrizzleAdapter as _DrizzleAdapter,
DrizzleTransaction as _DrizzleTransaction,
DropDatabase as _DropDatabase,
EnumRawColumn as _EnumRawColumn,
Execute as _Execute,
GenericColumn as _GenericColumn,
GenericColumns as _GenericColumns,
GenericPgColumn as _GenericPgColumn,
GenericRelation as _GenericRelation,
GenericTable as _GenericTable,
IDType as _IDType,
Insert as _Insert,
IntegerRawColumn as _IntegerRawColumn,
Migration as _Migration,
PostgresDB as _PostgresDB,
RawColumn as _RawColumn,
RawForeignKey as _RawForeignKey,
RawIndex as _RawIndex,
RawRelation as _RawRelation,
RawTable as _RawTable,
RelationMap as _RelationMap,
RequireDrizzleKit as _RequireDrizzleKit,
SetColumnID as _SetColumnID,
SQLiteDB as _SQLiteDB,
TimestampRawColumn as _TimestampRawColumn,
TransactionPg as _TransactionPg,
TransactionSQLite as _TransactionSQLite,
UUIDRawColumn as _UUIDRawColumn,
VectorRawColumn as _VectorRawColumn,
} from '../types.js'
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type BaseRawColumn = _BaseRawColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type BuildDrizzleTable = _BuildDrizzleTable
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type BuildQueryJoinAliases = _BuildQueryJoinAliases
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type ChainedMethods = _ChainedMethods
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type ColumnToCodeConverter = _ColumnToCodeConverter
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type CountDistinct = _CountDistinct
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type CreateJSONQueryArgs = _CreateJSONQueryArgs
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type DeleteWhere = _DeleteWhere
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type DrizzleAdapter = _DrizzleAdapter
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type DrizzleTransaction = _DrizzleTransaction
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type DropDatabase = _DropDatabase
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type EnumRawColumn = _EnumRawColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type Execute<T> = _Execute<T>
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type GenericColumn = _GenericColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type GenericColumns<T> = _GenericColumns<T>
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type GenericPgColumn = _GenericPgColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type GenericRelation = _GenericRelation
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type GenericTable = _GenericTable
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type IDType = _IDType
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type Insert = _Insert
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type IntegerRawColumn = _IntegerRawColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type Migration = _Migration
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type PostgresDB = _PostgresDB
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type RawColumn = _RawColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type RawForeignKey = _RawForeignKey
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type RawIndex = _RawIndex
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type RawRelation = _RawRelation
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type RawTable = _RawTable
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type RelationMap = _RelationMap
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type RequireDrizzleKit = _RequireDrizzleKit
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type SetColumnID = _SetColumnID
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type SQLiteDB = _SQLiteDB
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type TimestampRawColumn = _TimestampRawColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type TransactionPg = _TransactionPg
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type TransactionSQLite = _TransactionSQLite
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type UUIDRawColumn = _UUIDRawColumn
/**
* @deprecated - import from `@payloadcms/drizzle` instead
*/
export type VectorRawColumn = _VectorRawColumn

View File

@@ -1,3 +1,6 @@
/**
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
*/
export type ChainedMethods = {
args: unknown[]
method: string
@@ -7,6 +10,8 @@ export type ChainedMethods = {
* Call and returning methods that would normally be chained together but cannot be because of control logic
* @param methods
* @param query
*
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
*/
const chainMethods = <T>({ methods, query }: { methods: ChainedMethods; query: T }): T => {
return methods.reduce((query, { args, method }) => {

View File

@@ -3,7 +3,6 @@ import type { FindArgs, FlattenedField, TypeWithID } from 'payload'
import { inArray } from 'drizzle-orm'
import type { DrizzleAdapter } from '../types.js'
import type { ChainedMethods } from './chainMethods.js'
import buildQuery from '../queries/buildQuery.js'
import { selectDistinct } from '../queries/selectDistinct.js'
@@ -62,15 +61,6 @@ export const findMany = async function find({
const orderedIDMap: Record<number | string, number> = {}
let orderedIDs: (number | string)[]
const selectDistinctMethods: ChainedMethods = []
if (orderBy) {
selectDistinctMethods.push({
args: [() => orderBy.map(({ column, order }) => order(column))],
method: 'orderBy',
})
}
const findManyArgs = buildFindManyArgs({
adapter,
collectionSlug,
@@ -84,15 +74,16 @@ export const findMany = async function find({
tableName,
versions,
})
selectDistinctMethods.push({ args: [offset], method: 'offset' })
selectDistinctMethods.push({ args: [limit], method: 'limit' })
const selectDistinctResult = await selectDistinct({
adapter,
chainedMethods: selectDistinctMethods,
db,
joins,
query: ({ query }) => {
if (orderBy) {
query = query.orderBy(() => orderBy.map(({ column, order }) => order(column)))
}
return query.offset(offset).limit(limit)
},
selectFields,
tableName,
where,
@@ -167,6 +158,7 @@ export const findMany = async function find({
data,
fields,
joinQuery,
tableName,
})
})

View File

@@ -1,5 +1,5 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
import type { SQLiteSelect, SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
import {
@@ -16,7 +16,7 @@ import {
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js'
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js'
import buildQuery from '../queries/buildQuery.js'
@@ -25,7 +25,6 @@ import { operatorMap } from '../queries/operatorMap.js'
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
import { jsonAggBuildObject } from '../utilities/json.js'
import { rawConstraint } from '../utilities/rawConstraint.js'
import { chainMethods } from './chainMethods.js'
const flattenAllWherePaths = (where: Where, paths: string[]) => {
for (const k in where) {
@@ -197,7 +196,8 @@ export const traverseFields = ({
}
}
currentArgs.with[`${path}${field.name}`] = withArray
const relationName = field.dbName ? `_${arrayTableName}` : `${path}${field.name}`
currentArgs.with[relationName] = withArray
traverseFields({
_locales: withArray.with._locales,
@@ -612,34 +612,6 @@ export const traverseFields = ({
where: joinQueryWhere,
})
const chainedMethods: ChainedMethods = []
joins.forEach(({ type, condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: type ?? 'leftJoin',
})
})
if (page && limit !== 0) {
const offset = (page - 1) * limit - 1
if (offset > 0) {
chainedMethods.push({
args: [offset],
method: 'offset',
})
}
}
if (limit !== 0) {
chainedMethods.push({
args: [limit],
method: 'limit',
})
}
const db = adapter.drizzle as LibSQLDatabase
for (let key in selectFields) {
const val = selectFields[key]
@@ -654,14 +626,29 @@ export const traverseFields = ({
selectFields.parent = newAliasTable.parent
}
const subQuery = chainMethods({
methods: chainedMethods,
query: db
.select(selectFields as any)
.from(newAliasTable)
.where(subQueryWhere)
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
}).as(subQueryAlias)
let query: SQLiteSelect = db
.select(selectFields as any)
.from(newAliasTable)
.where(subQueryWhere)
.orderBy(() => orderBy.map(({ column, order }) => order(column)))
.$dynamic()
joins.forEach(({ type, condition, table }) => {
query = query[type ?? 'leftJoin'](table, condition)
})
if (page && limit !== 0) {
const offset = (page - 1) * limit - 1
if (offset > 0) {
query = query.offset(offset)
}
}
if (limit !== 0) {
query = query.limit(limit)
}
const subQuery = query.as(subQueryAlias)
if (shouldCount) {
currentArgs.extras[`${columnName}_count`] = sql`${db

View File

@@ -23,16 +23,58 @@ export { migrateFresh } from './migrateFresh.js'
export { migrateRefresh } from './migrateRefresh.js'
export { migrateReset } from './migrateReset.js'
export { migrateStatus } from './migrateStatus.js'
export { default as buildQuery } from './queries/buildQuery.js'
export { operatorMap } from './queries/operatorMap.js'
export type { Operators } from './queries/operatorMap.js'
export { parseParams } from './queries/parseParams.js'
export { queryDrafts } from './queryDrafts.js'
export { buildDrizzleRelations } from './schema/buildDrizzleRelations.js'
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 type {
BaseRawColumn,
BuildDrizzleTable,
BuildQueryJoinAliases,
ChainedMethods,
ColumnToCodeConverter,
CountDistinct,
CreateJSONQueryArgs,
DeleteWhere,
DrizzleAdapter,
DrizzleTransaction,
DropDatabase,
EnumRawColumn,
Execute,
GenericColumn,
GenericColumns,
GenericPgColumn,
GenericRelation,
GenericTable,
IDType,
Insert,
IntegerRawColumn,
Migration,
PostgresDB,
RawColumn,
RawForeignKey,
RawIndex,
RawRelation,
RawTable,
RelationMap,
RequireDrizzleKit,
SetColumnID,
SQLiteDB,
TimestampRawColumn,
TransactionPg,
TransactionSQLite,
UUIDRawColumn,
VectorRawColumn,
} from './types.js'
export { updateGlobal } from './updateGlobal.js'
export { updateGlobalVersion } from './updateGlobalVersion.js'
export { updateJobs } from './updateJobs.js'
export { updateMany } from './updateMany.js'
export { updateOne } from './updateOne.js'
export { updateVersion } from './updateVersion.js'

View File

@@ -50,7 +50,8 @@ export async function migrateDown(this: DrizzleAdapter): Promise<void> {
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
})
const tableExists = await migrationTableExists(this)
const tableExists = await migrationTableExists(this, db)
if (tableExists) {
await payload.delete({
id: migration.id,

View File

@@ -54,7 +54,7 @@ export async function migrateRefresh(this: DrizzleAdapter) {
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
})
const tableExists = await migrationTableExists(this)
const tableExists = await migrationTableExists(this, db)
if (tableExists) {
await payload.delete({
collection: 'payload-migrations',

View File

@@ -45,7 +45,7 @@ export async function migrateReset(this: DrizzleAdapter): Promise<void> {
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
})
const tableExists = await migrationTableExists(this)
const tableExists = await migrationTableExists(this, db)
if (tableExists) {
await payload.delete({
id: migration.id,

View File

@@ -1,10 +1,9 @@
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
import { count, sql } from 'drizzle-orm'
import type { ChainedMethods } from '../types.js'
import type { BasePostgresAdapter, CountDistinct } from './types.js'
import { chainMethods } from '../find/chainMethods.js'
export const countDistinct: CountDistinct = async function countDistinct(
this: BasePostgresAdapter,
{ db, joins, tableName, where },
@@ -20,30 +19,25 @@ export const countDistinct: CountDistinct = async function countDistinct(
return Number(countResult[0].count)
}
const chainedMethods: ChainedMethods = []
let query = db
.select({
count: sql`COUNT(1) OVER()`,
})
.from(this.tables[tableName])
.where(where)
.groupBy(this.tables[tableName].id)
.limit(1)
.$dynamic()
joins.forEach(({ condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
query = query.leftJoin(table as PgTableWithColumns<any>, condition)
})
// When we have any joins, we need to count each individual ID only once.
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
const countResult = await chainMethods({
methods: chainedMethods,
query: db
.select({
count: sql`COUNT(1) OVER()`,
})
.from(this.tables[tableName])
.where(where)
.groupBy(this.tables[tableName].id)
.limit(1),
})
const countResult = await query
return Number(countResult[0].count)
}

View File

@@ -1,10 +1,16 @@
import type { SQL } from 'drizzle-orm'
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'payload'
import type {
FlattenedBlock,
FlattenedField,
NumberField,
RelationshipField,
TextField,
} from 'payload'
import { and, eq, like, sql } from 'drizzle-orm'
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
import { APIError } from 'payload'
import { APIError, getFieldByPath } from 'payload'
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import { validate as uuidValidate } from 'uuid'
@@ -338,6 +344,112 @@ export const getTableColumnFromPath = ({
})
}
case 'join': {
if (Array.isArray(field.collection)) {
throw new APIError('Not supported')
}
const newCollectionPath = pathSegments.slice(1).join('.')
if (field.hasMany) {
const relationTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}`
const { newAliasTable: aliasRelationshipTable } = getTableAlias({
adapter,
tableName: relationTableName,
})
const relationshipField = getFieldByPath({
fields: adapter.payload.collections[field.collection].config.flattenedFields,
path: field.on,
})
if (!relationshipField) {
throw new APIError('Relationship was not found')
}
addJoinTable({
condition: and(
eq(
adapter.tables[rootTableName].id,
aliasRelationshipTable[
`${(relationshipField.field as RelationshipField).relationTo as string}ID`
],
),
like(aliasRelationshipTable.path, field.on),
),
joins,
queryPath: field.on,
table: aliasRelationshipTable,
})
const relationshipConfig = adapter.payload.collections[field.collection].config
const relationshipTableName = adapter.tableNameMap.get(
toSnakeCase(relationshipConfig.slug),
)
// parent to relationship join table
const relationshipFields = relationshipConfig.flattenedFields
const { newAliasTable: relationshipTable } = getTableAlias({
adapter,
tableName: relationshipTableName,
})
joins.push({
condition: eq(aliasRelationshipTable.parent, relationshipTable.id),
table: relationshipTable,
})
return getTableColumnFromPath({
adapter,
aliasTable: relationshipTable,
collectionPath: newCollectionPath,
constraints,
// relationshipFields are fields from a different collection => no parentIsLocalized
fields: relationshipFields,
joins,
locale,
parentIsLocalized: false,
pathSegments: pathSegments.slice(1),
rootTableName: relationshipTableName,
selectFields,
selectLocale,
tableName: relationshipTableName,
value,
})
}
const newTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[field.collection].config.slug),
)
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
joins.push({
condition: eq(
newAliasTable[field.on.replaceAll('.', '_')],
aliasTable ? aliasTable.id : adapter.tables[tableName].id,
),
table: newAliasTable,
})
return getTableColumnFromPath({
adapter,
aliasTable: newAliasTable,
collectionPath: newCollectionPath,
constraintPath: '',
constraints,
fields: adapter.payload.collections[field.collection].config.flattenedFields,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
selectFields,
tableName: newTableName,
value,
})
break
}
case 'number':
case 'text': {
if (field.hasMany) {
@@ -381,7 +493,6 @@ export const getTableColumnFromPath = ({
}
break
}
case 'relationship':
case 'upload': {
const newCollectionPath = pathSegments.slice(1).join('.')
@@ -645,6 +756,7 @@ export const getTableColumnFromPath = ({
value,
})
}
break
}

View File

@@ -19,7 +19,7 @@ type Args = {
aliasTable?: Table
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale: string
locale?: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
selectLocale?: boolean

View File

@@ -1,7 +1,7 @@
import type { QueryPromise, SQL } from 'drizzle-orm'
import type { SQLiteColumn } from 'drizzle-orm/sqlite-core'
import type { PgSelect } from 'drizzle-orm/pg-core'
import type { SQLiteColumn, SQLiteSelect } from 'drizzle-orm/sqlite-core'
import type { ChainedMethods } from '../find/chainMethods.js'
import type {
DrizzleAdapter,
DrizzleTransaction,
@@ -12,13 +12,11 @@ import type {
} from '../types.js'
import type { BuildQueryJoinAliases } from './buildQuery.js'
import { chainMethods } from '../find/chainMethods.js'
type Args = {
adapter: DrizzleAdapter
chainedMethods?: ChainedMethods
db: DrizzleAdapter['drizzle'] | DrizzleTransaction
joins: BuildQueryJoinAliases
query?: (args: { query: SQLiteSelect }) => SQLiteSelect
selectFields: Record<string, GenericColumn>
tableName: string
where: SQL
@@ -29,42 +27,40 @@ type Args = {
*/
export const selectDistinct = ({
adapter,
chainedMethods = [],
db,
joins,
query: queryModifier = ({ query }) => query,
selectFields,
tableName,
where,
}: Args): QueryPromise<{ id: number | string }[] & Record<string, GenericColumn>> => {
if (Object.keys(joins).length > 0) {
if (where) {
chainedMethods.push({ args: [where], method: 'where' })
}
joins.forEach(({ condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
let query
let query: SQLiteSelect
const table = adapter.tables[tableName]
if (adapter.name === 'postgres') {
query = (db as TransactionPg)
.selectDistinct(selectFields as Record<string, GenericPgColumn>)
.from(table)
.$dynamic() as unknown as SQLiteSelect
}
if (adapter.name === 'sqlite') {
query = (db as TransactionSQLite)
.selectDistinct(selectFields as Record<string, SQLiteColumn>)
.from(table)
.$dynamic()
}
return chainMethods({
methods: chainedMethods,
query,
if (where) {
query = query.where(where)
}
joins.forEach(({ condition, table }) => {
query = query.leftJoin(table, condition)
})
return queryModifier({
query,
}) as unknown as QueryPromise<{ id: number | string }[] & Record<string, GenericColumn>>
}
}

View File

@@ -2,6 +2,7 @@ import type { CompoundIndex, FlattenedField } from 'payload'
import { InvalidConfiguration } from 'payload'
import {
array,
fieldAffectsData,
fieldIsVirtual,
fieldShouldBeLocalized,
@@ -287,7 +288,9 @@ export const traverseFields = ({
}
}
relationsToBuild.set(fieldName, {
const relationName = field.dbName ? `_${arrayTableName}` : fieldName
relationsToBuild.set(relationName, {
type: 'many',
// arrays have their own localized table, independent of the base table.
localized: false,
@@ -304,7 +307,7 @@ export const traverseFields = ({
},
],
references: ['id'],
relationName: fieldName,
relationName,
to: parentTableName,
},
}

View File

@@ -15,6 +15,7 @@ type TransformArgs = {
joinQuery?: JoinQuery
locale?: string
parentIsLocalized?: boolean
tableName: string
}
// This is the entry point to transform Drizzle output data
@@ -26,6 +27,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
fields,
joinQuery,
parentIsLocalized,
tableName,
}: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
@@ -53,6 +55,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
adapter,
blocks,
config,
currentTableName: tableName,
dataRef: {
id: data.id,
},
@@ -65,7 +68,9 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
path: '',
relationships,
table: data,
tablePath: '',
texts,
topLevelTableName: tableName,
})
deletions.forEach((deletion) => deletion())

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'
@@ -22,6 +23,7 @@ type TraverseFieldsArgs = {
* The full Payload config
*/
config: SanitizedConfig
currentTableName: string
/**
* The data reference to be mutated within this recursive function
*/
@@ -59,10 +61,12 @@ type TraverseFieldsArgs = {
* Data structure representing the nearest table from db
*/
table: Record<string, unknown>
tablePath: string
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
topLevelTableName: string
/**
* Set to a locale if this group of fields is within a localized array or block.
*/
@@ -75,6 +79,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName,
dataRef,
deletions,
fieldPrefix,
@@ -85,7 +90,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path,
relationships,
table,
tablePath,
texts,
topLevelTableName,
withinArrayOrBlockLocale,
}: TraverseFieldsArgs): T => {
const sanitizedPath = path ? `${path}.` : path
@@ -110,6 +117,14 @@ export const traverseFields = <T extends Record<string, unknown>>({
const isLocalized = fieldShouldBeLocalized({ field, parentIsLocalized })
if (field.type === 'array') {
const arrayTableName = adapter.tableNameMap.get(
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
)
if (field.dbName) {
fieldData = table[`_${arrayTableName}`]
}
if (Array.isArray(fieldData)) {
if (isLocalized) {
result[field.name] = fieldData.reduce((arrayResult, row) => {
@@ -129,6 +144,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName: arrayTableName,
dataRef: data,
deletions,
fieldPrefix: '',
@@ -138,7 +154,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale: locale,
})
@@ -175,6 +193,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName: arrayTableName,
dataRef: row,
deletions,
fieldPrefix: '',
@@ -184,7 +203,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale,
}),
)
@@ -228,11 +249,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
(block) => typeof block !== 'string' && block.slug === row.blockType,
) as FlattenedBlock | undefined)
const tableName = adapter.tableNameMap.get(
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
)
if (block) {
const blockResult = traverseFields<T>({
adapter,
blocks,
config,
currentTableName: tableName,
dataRef: row,
deletions,
fieldPrefix: '',
@@ -242,7 +268,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale: locale,
})
@@ -300,11 +328,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
delete row._index
}
const tableName = adapter.tableNameMap.get(
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
)
acc.push(
traverseFields<T>({
adapter,
blocks,
config,
currentTableName: tableName,
dataRef: row,
deletions,
fieldPrefix: '',
@@ -314,7 +347,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
tablePath: '',
texts,
topLevelTableName,
withinArrayOrBlockLocale,
}),
)
@@ -614,6 +649,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
adapter,
blocks,
config,
currentTableName,
dataRef: groupData as Record<string, unknown>,
deletions,
fieldPrefix: groupFieldPrefix,
@@ -624,7 +660,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
path: `${sanitizedPath}${field.name}`,
relationships,
table,
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
texts,
topLevelTableName,
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
})

View File

@@ -496,6 +496,10 @@ export const traverseFields = ({
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
}
if (field.type === 'text' && value && typeof value !== 'string') {
formattedValue = JSON.stringify(value)
}
if (field.type === 'date') {
if (typeof value === 'number' && !Number.isNaN(value)) {
formattedValue = new Date(value).toISOString()

View File

@@ -37,11 +37,8 @@ import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
import type { SQLiteRaw } from 'drizzle-orm/sqlite-core/query-builders/raw'
import type { QueryResult } from 'pg'
import type { ChainedMethods } from './find/chainMethods.js'
import type { Operators } from './queries/operatorMap.js'
export { ChainedMethods }
export type PostgresDB = NodePgDatabase<Record<string, unknown>>
export type SQLiteDB = LibSQLDatabase<
@@ -377,3 +374,8 @@ export type RelationMap = Map<
type: 'many' | 'one'
}
>
/**
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
*/
export type { ChainedMethods } from './find/chainMethods.js'

View File

@@ -0,0 +1,70 @@
import type { UpdateJobs, Where } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { upsertRow } from './upsertRow/index.js'
import { getTransaction } from './utilities/getTransaction.js'
export const updateJobs: UpdateJobs = async function updateMany(
this: DrizzleAdapter,
{ id, data, limit: limitArg, req, returning, sort: sortArg, where: whereArg },
) {
if (!(data?.log as object[])?.length) {
delete data.log
}
const whereToUse: Where = id ? { id: { equals: id } } : whereArg
const limit = id ? 1 : limitArg
const db = await getTransaction(this, req)
const collection = this.payload.collections['payload-jobs'].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collection.defaultSort
const jobs = await findMany({
adapter: this,
collectionSlug: 'payload-jobs',
fields: collection.flattenedFields,
limit: id ? 1 : limit,
pagination: false,
req,
sort,
tableName,
where: whereToUse,
})
if (!jobs.docs.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 job of jobs.docs) {
const updateData = {
...job,
...data,
}
const result = await upsertRow({
id: job.id,
adapter: this,
data: updateData,
db,
fields: collection.flattenedFields,
ignoreResult: returning === false,
operation: 'update',
req,
tableName,
})
results.push(result)
}
if (returning === false) {
return null
}
return results
}

View File

@@ -3,9 +3,8 @@ import type { UpdateMany } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { ChainedMethods, DrizzleAdapter } from './types.js'
import type { DrizzleAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
@@ -45,16 +44,10 @@ export const updateMany: UpdateMany = async function updateMany(
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: orderBy
? [
{
args: [() => orderBy.map(({ column, order }) => order(column))],
method: 'orderBy',
},
]
: [],
db,
joins,
query: ({ query }) =>
orderBy ? query.orderBy(() => orderBy.map(({ column, order }) => order(column))) : query,
selectFields,
tableName,
where,
@@ -69,28 +62,17 @@ export const updateMany: UpdateMany = async function updateMany(
const table = this.tables[tableName]
const query = _db.select({ id: table.id }).from(table).where(where)
const chainedMethods: ChainedMethods = []
let query = _db.select({ id: table.id }).from(table).where(where).$dynamic()
if (typeof limit === 'number' && limit > 0) {
chainedMethods.push({
args: [limit],
method: 'limit',
})
query = query.limit(limit)
}
if (orderBy) {
chainedMethods.push({
args: [() => orderBy.map(({ column, order }) => order(column))],
method: 'orderBy',
})
query = query.orderBy(() => orderBy.map(({ column, order }) => order(column)))
}
const docsToUpdate = await chainMethods({
methods: chainedMethods,
query,
})
const docsToUpdate = await query
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
}

View File

@@ -19,50 +19,51 @@ export const updateOne: UpdateOne = async function updateOne(
joins: joinQuery,
locale,
req,
returning,
select,
where: whereArg,
returning,
},
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id
const { joins, selectFields, where } = buildQuery({
adapter: this,
fields: collection.flattenedFields,
locale,
tableName,
where: whereToUse,
})
if (!idToUpdate) {
const { joins, selectFields, where } = buildQuery({
adapter: this,
fields: collection.flattenedFields,
locale,
tableName,
where: whereArg,
})
// selectDistinct will only return if there are joins
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: [{ args: [1], method: 'limit' }],
db,
joins,
selectFields,
tableName,
where,
})
// selectDistinct will only return if there are joins
const selectDistinctResult = await selectDistinct({
adapter: this,
db,
joins,
query: ({ query }) => query.limit(1),
selectFields,
tableName,
where,
})
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 table = this.tables[tableName]
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 table = this.tables[tableName]
const docsToUpdate = await (db as LibSQLDatabase)
.select({
id: table.id,
})
.from(table)
.where(where)
.limit(1)
idToUpdate = docsToUpdate?.[0]?.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({
@@ -71,12 +72,12 @@ export const updateOne: UpdateOne = async function updateOne(
data,
db,
fields: collection.flattenedFields,
ignoreResult: returning === false,
joinQuery,
operation: 'update',
req,
select,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {

View File

@@ -423,6 +423,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
path: fieldName,
},
],
req,
},
req?.t,
)
@@ -466,6 +467,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
data: doc,
fields,
joinQuery: false,
tableName,
})
return result

View File

@@ -131,7 +131,7 @@ export const createSchemaGenerator = ({
let foreignKeyDeclaration = `${sanitizeObjectKey(key)}: foreignKey({
columns: [${foreignKey.columns.map((col) => `columns['${col}']`).join(', ')}],
foreignColumns: [${foreignKey.foreignColumns.map((col) => `${accessProperty(col.table, col.name)}`).join(', ')}],
name: '${foreignKey.name}'
name: '${foreignKey.name}'
})`
if (foreignKey.onDelete) {
@@ -167,11 +167,11 @@ ${Object.entries(table.columns)
}${
extrasDeclarations.length
? `, (columns) => ({
${extrasDeclarations.join('\n ')}
${extrasDeclarations.join('\n ')}
})`
: ''
}
)
)
`
tableDeclarations.push(tableCode)
@@ -250,7 +250,7 @@ type DatabaseSchema = {
`
const finalDeclaration = `
declare module '${this.packageName}/types' {
declare module '${this.packageName}' {
export interface GeneratedDatabaseSchema {
schema: DatabaseSchema
}

View File

@@ -1,6 +1,11 @@
import type { DrizzleAdapter } from '../types.js'
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boolean> => {
import type { DrizzleAdapter, PostgresDB } from '../types.js'
export const migrationTableExists = async (
adapter: DrizzleAdapter,
db?: LibSQLDatabase | PostgresDB,
): Promise<boolean> => {
let statement
if (adapter.name === 'postgres') {
@@ -20,7 +25,7 @@ export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boo
}
const result = await adapter.execute({
drizzle: adapter.drizzle,
drizzle: db ?? adapter.drizzle,
raw: statement,
})

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLObjectType } from 'graphql'
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'
export const buildPaginatedListType = (name, docType) =>
new GraphQLObjectType({
name,
fields: {
docs: {
type: new GraphQLList(docType),
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(docType))),
},
hasNextPage: { type: GraphQLBoolean },
hasPrevPage: { type: GraphQLBoolean },
limit: { type: GraphQLInt },
nextPage: { type: GraphQLInt },
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
hasPrevPage: { type: new GraphQLNonNull(GraphQLBoolean) },
limit: { type: new GraphQLNonNull(GraphQLInt) },
nextPage: { type: new GraphQLNonNull(GraphQLInt) },
offset: { type: GraphQLInt },
page: { type: GraphQLInt },
pagingCounter: { type: GraphQLInt },
prevPage: { type: GraphQLInt },
totalDocs: { type: GraphQLInt },
totalPages: { type: GraphQLInt },
page: { type: new GraphQLNonNull(GraphQLInt) },
pagingCounter: { type: new GraphQLNonNull(GraphQLInt) },
prevPage: { type: new GraphQLNonNull(GraphQLInt) },
totalDocs: { type: new GraphQLNonNull(GraphQLInt) },
totalPages: { type: new GraphQLNonNull(GraphQLInt) },
},
})

View File

@@ -348,11 +348,15 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
name: joinName,
fields: {
docs: {
type: Array.isArray(field.collection)
? GraphQLJSON
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
type: new GraphQLNonNull(
Array.isArray(field.collection)
? GraphQLJSON
: new GraphQLList(
new GraphQLNonNull(graphqlResult.collections[field.collection].graphQL.type),
),
),
},
hasNextPage: { type: GraphQLBoolean },
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
},
}),
args: {
@@ -379,6 +383,8 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
const { limit, page, sort, where } = args
const { req } = context
const draft = Boolean(args.draft ?? context.req.query?.draft)
const fullWhere = combineQueries(where, {
[field.on]: { equals: parent._id ?? parent.id },
})
@@ -390,6 +396,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
return await req.payload.find({
collection,
depth: 0,
draft,
fallbackLocale: req.fallbackLocale,
limit,
locale: req.locale,
@@ -425,7 +432,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
...objectTypeConfig,
[formatName(field.name)]: formattedNameResolver({
type: withNullableType({
type: field?.hasMany === true ? new GraphQLList(type) : type,
type: field?.hasMany === true ? new GraphQLList(new GraphQLNonNull(type)) : type,
field,
forceNullable,
parentIsLocalized,
@@ -853,7 +860,10 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
...objectTypeConfig,
[formatName(field.name)]: formattedNameResolver({
type: withNullableType({
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
type:
field.hasMany === true
? new GraphQLList(new GraphQLNonNull(GraphQLString))
: GraphQLString,
field,
forceNullable,
parentIsLocalized,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.30.0",
"version": "3.35.1",
"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.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.30.0",
"version": "3.35.1",
"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.30.0",
"version": "3.35.1",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,9 +1,15 @@
import type { FieldSchemaJSON } from 'payload'
import type { LivePreviewMessageEvent } from './types.js'
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
import { mergeData } from './mergeData.js'
const _payloadLivePreview = {
const _payloadLivePreview: {
fieldSchema: FieldSchemaJSON | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
previousData: any
} = {
/**
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
* We need to cache this value so that it can be used across subsequent messages
@@ -18,7 +24,7 @@ const _payloadLivePreview = {
previousData: undefined,
}
export const handleMessage = async <T>(args: {
export const handleMessage = async <T extends Record<string, any>>(args: {
apiRoute?: string
depth?: number
event: LivePreviewMessageEvent<T>

View File

@@ -4,7 +4,15 @@ import type { PopulationsByCollection } from './types.js'
import { traverseFields } from './traverseFields.js'
const defaultRequestHandler = ({ apiPath, endpoint, serverURL }) => {
const defaultRequestHandler = ({
apiPath,
endpoint,
serverURL,
}: {
apiPath: string
endpoint: string
serverURL: string
}) => {
const url = `${serverURL}${apiPath}/${endpoint}`
return fetch(url, {
credentials: 'include',
@@ -19,7 +27,7 @@ const defaultRequestHandler = ({ apiPath, endpoint, serverURL }) => {
// Instead, we keep track of the old locale ourselves and trigger a re-population when it changes
let prevLocale: string | undefined
export const mergeData = async <T>(args: {
export const mergeData = async <T extends Record<string, any>>(args: {
apiRoute?: string
collectionPopulationRequestHandler?: ({
apiPath,
@@ -86,7 +94,7 @@ export const mergeData = async <T>(args: {
if (res?.docs?.length > 0) {
res.docs.forEach((doc) => {
populationsByCollection[collection].forEach((population) => {
populationsByCollection[collection]?.forEach((population) => {
if (population.id === doc.id) {
population.ref[population.accessor] = doc
}

View File

@@ -1,6 +1,6 @@
import { handleMessage } from './handleMessage.js'
export const subscribe = <T>(args: {
export const subscribe = <T extends Record<string, any>>(args: {
apiRoute?: string
callback: (data: T) => void
depth?: number

View File

@@ -1,17 +1,16 @@
import type { DocumentEvent } from 'payload'
import type { fieldSchemaToJSON } from 'payload/shared'
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
import type { PopulationsByCollection } from './types.js'
import { traverseRichText } from './traverseRichText.js'
export const traverseFields = <T>(args: {
export const traverseFields = <T extends Record<string, any>>(args: {
externallyUpdatedRelationship?: DocumentEvent
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
fieldSchema: FieldSchemaJSON
incomingData: T
localeChanged: boolean
populationsByCollection: PopulationsByCollection
result: T
result: Record<string, any>
}): void => {
const {
externallyUpdatedRelationship,
@@ -48,7 +47,7 @@ export const traverseFields = <T>(args: {
traverseFields({
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields,
fieldSchema: fieldSchema.fields!,
incomingData: incomingRow,
localeChanged,
populationsByCollection,
@@ -64,7 +63,7 @@ export const traverseFields = <T>(args: {
case 'blocks':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType]
const incomingBlockJSON = fieldSchema.blocks?.[incomingBlock.blockType]
if (!result[fieldName]) {
result[fieldName] = []
@@ -82,7 +81,7 @@ export const traverseFields = <T>(args: {
traverseFields({
externallyUpdatedRelationship,
fieldSchema: incomingBlockJSON.fields,
fieldSchema: incomingBlockJSON!.fields!,
incomingData: incomingBlock,
localeChanged,
populationsByCollection,
@@ -106,7 +105,7 @@ export const traverseFields = <T>(args: {
traverseFields({
externallyUpdatedRelationship,
fieldSchema: fieldSchema.fields,
fieldSchema: fieldSchema.fields!,
incomingData: incomingData[fieldName] || {},
localeChanged,
populationsByCollection,
@@ -166,11 +165,11 @@ export const traverseFields = <T>(args: {
incomingRelation === externallyUpdatedRelationship?.id
if (hasChanged || hasUpdated || localeChanged) {
if (!populationsByCollection[fieldSchema.relationTo]) {
populationsByCollection[fieldSchema.relationTo] = []
if (!populationsByCollection[fieldSchema.relationTo!]) {
populationsByCollection[fieldSchema.relationTo!] = []
}
populationsByCollection[fieldSchema.relationTo].push({
populationsByCollection[fieldSchema.relationTo!]?.push({
id: incomingRelation,
accessor: i,
ref: result[fieldName],
@@ -265,11 +264,11 @@ export const traverseFields = <T>(args: {
// if the new value is not empty, populate it
// otherwise set the value to null
if (newID) {
if (!populationsByCollection[fieldSchema.relationTo]) {
populationsByCollection[fieldSchema.relationTo] = []
if (!populationsByCollection[fieldSchema.relationTo!]) {
populationsByCollection[fieldSchema.relationTo!] = []
}
populationsByCollection[fieldSchema.relationTo].push({
populationsByCollection[fieldSchema.relationTo!]?.push({
id: newID,
accessor: fieldName,
ref: result as Record<string, unknown>,

View File

@@ -79,7 +79,7 @@ export const traverseRichText = ({
populationsByCollection[incomingData.relationTo] = []
}
populationsByCollection[incomingData.relationTo].push({
populationsByCollection[incomingData.relationTo]?.push({
id:
incomingData[key] && typeof incomingData[key] === 'object'
? incomingData[key].id

View File

@@ -1,9 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.30.0",
"version": "3.35.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -37,6 +37,11 @@
"types": "./src/exports/routes.ts",
"default": "./src/exports/routes.ts"
},
"./auth": {
"import": "./src/exports/auth.ts",
"types": "./src/exports/auth.ts",
"default": "./src/exports/auth.ts"
},
"./templates": {
"import": "./src/exports/templates.ts",
"types": "./src/exports/templates.ts",
@@ -104,16 +109,16 @@
"@babel/preset-env": "7.26.7",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@next/eslint-plugin-next": "15.2.3",
"@next/eslint-plugin-next": "15.3.0",
"@payloadcms/eslint-config": "workspace:*",
"@types/busboy": "1.5.4",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.2",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"babel-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"esbuild": "0.24.2",
"esbuild-sass-plugin": "3.3.1",
"eslint-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"payload": "workspace:*",
"swc-plugin-transform-remove-imports": "3.1.0"
},
@@ -151,6 +156,11 @@
"types": "./dist/exports/templates.d.ts",
"default": "./dist/exports/templates.js"
},
"./auth": {
"import": "./dist/exports/auth.js",
"types": "./dist/exports/auth.d.ts",
"default": "./dist/exports/auth.js"
},
"./utilities": {
"import": "./dist/exports/utilities.js",
"types": "./dist/exports/utilities.d.ts",

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