Compare commits
69 Commits
autogenera
...
chore/bump
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
070c4f7e55 | ||
|
|
b74b14e119 | ||
|
|
3a7cd717b2 | ||
|
|
3287f7062f | ||
|
|
a9eca3a785 | ||
|
|
71e3c7839b | ||
|
|
a66f90ebb6 | ||
|
|
272914c818 | ||
|
|
466dcd7189 | ||
|
|
a72fa869f3 | ||
|
|
3523c2c6a6 | ||
|
|
112e081d8f | ||
|
|
eab9770315 | ||
|
|
4d7c1d45fa | ||
|
|
37bfc63da2 | ||
|
|
18ff9cbdb1 | ||
|
|
ae9e5e19ad | ||
|
|
7aa3c5ea6b | ||
|
|
a0fb3353c6 | ||
|
|
101f7658f7 | ||
|
|
9853f27667 | ||
|
|
e0046bba59 | ||
|
|
f1d9b44161 | ||
|
|
09916ad18e | ||
|
|
a90ae9d42b | ||
|
|
d19412f62d | ||
|
|
bd557a97d5 | ||
|
|
97e2e77ff4 | ||
|
|
acae547ddf | ||
|
|
ec34e64261 | ||
|
|
f079eced8a | ||
|
|
b809c98966 | ||
|
|
b9ffbc6994 | ||
|
|
09782be0e0 | ||
|
|
b270901fa6 | ||
|
|
c7b14bd44d | ||
|
|
83319be752 | ||
|
|
77210251f4 | ||
|
|
750210fabe | ||
|
|
6d831475a0 | ||
|
|
e109491dbe | ||
|
|
dee9abd5c1 | ||
|
|
5c54d9a567 | ||
|
|
36e7c59b4e | ||
|
|
9adbbde9a8 | ||
|
|
8ad22eb1c0 | ||
|
|
b76844dac9 | ||
|
|
f7ed8e90e1 | ||
|
|
e6aad5adfc | ||
|
|
4ebd3ce668 | ||
|
|
fae113b799 | ||
|
|
e87521a376 | ||
|
|
8880d705e3 | ||
|
|
018bdad247 | ||
|
|
816fb28f55 | ||
|
|
857e984fbb | ||
|
|
d47b753898 | ||
|
|
308cb64b9c | ||
|
|
6c735effff | ||
|
|
fd42ad5f52 | ||
|
|
a58ff57e4f | ||
|
|
06d937e903 | ||
|
|
8e93ad8f5f | ||
|
|
f310c90211 | ||
|
|
dc793d1d14 | ||
|
|
f9c73ad5f2 | ||
|
|
760cfadaad | ||
|
|
d29bdfc10f | ||
|
|
f34eb228c4 |
10
.github/workflows/main.yml
vendored
10
.github/workflows/main.yml
vendored
@@ -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,11 @@ 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
|
||||
@@ -320,6 +321,7 @@ jobs:
|
||||
- plugin-import-export
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- sort
|
||||
- versions
|
||||
- uploads
|
||||
env:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -60,31 +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). |
|
||||
| `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 |
|
||||
| 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._
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
cors: {
|
||||
origins: ['http://localhost:3000']
|
||||
origins: ['http://localhost:3000'],
|
||||
headers: ['x-custom-header']
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -103,12 +103,12 @@ 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). |
|
||||
| `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
|
||||
|
||||
@@ -135,11 +135,11 @@ 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). |
|
||||
| `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
|
||||
|
||||
|
||||
@@ -271,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
|
||||
|
||||
@@ -67,6 +67,11 @@ To install a Database Adapter, you can run **one** of the following commands:
|
||||
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
|
||||
@@ -75,7 +80,7 @@ To install a Database Adapter, you can run **one** of the following commands:
|
||||
|
||||
#### 2. Copy Payload files into your Next.js app folder
|
||||
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](<https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)>) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
|
||||
47
docs/local-api/access-control.mdx
Normal file
47
docs/local-api/access-control.mdx
Normal 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.
|
||||
360
docs/local-api/server-functions.mdx
Normal file
360
docs/local-api/server-functions.mdx
Normal file
@@ -0,0 +1,360 @@
|
||||
---
|
||||
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
|
||||
|
||||
Coming soon…
|
||||
|
||||
## 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 })
|
||||
}
|
||||
```
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -58,8 +58,8 @@ Query Presets are subject to the same [Access Control](../access-control/overvie
|
||||
|
||||
Access Control for Query Presets can be customized in two ways:
|
||||
|
||||
1. [Collection Access Control](#static-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](#dynamic-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"payload": "3.11.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "7.45.4",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.34.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"qs-esm": "7.0.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/graphql": "latest",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"payload": "latest",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"payload-app": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.15.2",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"payload": "beta",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.34.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"payload": "latest",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/graphql": "latest",
|
||||
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -120,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:*",
|
||||
@@ -135,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",
|
||||
@@ -156,16 +156,16 @@
|
||||
"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",
|
||||
"sharp": "0.34.0",
|
||||
"shelljs": "0.8.5",
|
||||
"slash": "3.0.0",
|
||||
"sort-package-json": "^2.10.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? {}, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -158,6 +158,7 @@ export const findMany = async function find({
|
||||
data,
|
||||
fields,
|
||||
joinQuery,
|
||||
tableName,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -196,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,
|
||||
|
||||
@@ -23,8 +23,10 @@ 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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type Args = {
|
||||
aliasTable?: Table
|
||||
fields: FlattenedField[]
|
||||
joins: BuildQueryJoinAliases
|
||||
locale: string
|
||||
locale?: string
|
||||
parentIsLocalized: boolean
|
||||
selectFields: Record<string, GenericColumn>
|
||||
selectLocale?: boolean
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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) },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
@@ -428,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,
|
||||
@@ -856,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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -45,8 +45,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -104,16 +104,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"
|
||||
},
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client'
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { ChevronIcon, Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { ChevronIcon, FieldDiffLabel, Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Label from '../Label/index.js'
|
||||
import './index.scss'
|
||||
import { countChangedFields, countChangedFieldsInRows } from '../utilities/countChangedFields.js'
|
||||
|
||||
@@ -100,7 +99,7 @@ export const DiffCollapser: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Label>
|
||||
<FieldDiffLabel>
|
||||
<button
|
||||
aria-label={isCollapsed ? 'Expand' : 'Collapse'}
|
||||
className={`${baseClass}__toggle-button`}
|
||||
@@ -115,7 +114,7 @@ export const DiffCollapser: React.FC<Props> = ({
|
||||
{t('version:changedFieldsCount', { count: changeCount })}
|
||||
</Pill>
|
||||
)}
|
||||
</Label>
|
||||
</FieldDiffLabel>
|
||||
<div className={contentClassNames}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
BaseVersionField,
|
||||
ClientField,
|
||||
ClientFieldSchemaMap,
|
||||
Field,
|
||||
FieldDiffClientProps,
|
||||
FieldDiffServerProps,
|
||||
FieldTypes,
|
||||
FlattenedBlock,
|
||||
PayloadComponent,
|
||||
PayloadRequest,
|
||||
SanitizedFieldPermissions,
|
||||
VersionField,
|
||||
} from 'payload'
|
||||
import type { DiffMethod } from 'react-diff-viewer-continued'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { dequal } from 'dequal/lite'
|
||||
import {
|
||||
type BaseVersionField,
|
||||
type ClientField,
|
||||
type ClientFieldSchemaMap,
|
||||
type Field,
|
||||
type FieldDiffClientProps,
|
||||
type FieldDiffServerProps,
|
||||
type FieldTypes,
|
||||
type FlattenedBlock,
|
||||
MissingEditorProp,
|
||||
type PayloadComponent,
|
||||
type PayloadRequest,
|
||||
type SanitizedFieldPermissions,
|
||||
type VersionField,
|
||||
} from 'payload'
|
||||
import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared'
|
||||
|
||||
import { diffMethods } from './fields/diffMethods.js'
|
||||
@@ -238,7 +239,24 @@ const buildVersionField = ({
|
||||
return null
|
||||
}
|
||||
|
||||
const CustomComponent = field?.admin?.components?.Diff ?? customDiffComponents?.[field.type]
|
||||
let CustomComponent = customDiffComponents?.[field.type]
|
||||
if (field?.type === 'richText') {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
if (field.editor.CellComponent) {
|
||||
CustomComponent = field.editor.DiffComponent
|
||||
}
|
||||
}
|
||||
if (field?.admin?.components?.Diff) {
|
||||
CustomComponent = field.admin.components.Diff
|
||||
}
|
||||
|
||||
const DefaultComponent = diffComponents?.[field.type]
|
||||
|
||||
const baseVersionField: BaseVersionField = {
|
||||
|
||||
@@ -7,12 +7,11 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { FieldDiffLabel, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import React from 'react'
|
||||
import ReactDiffViewer from 'react-diff-viewer-continued'
|
||||
|
||||
import Label from '../../Label/index.js'
|
||||
import './index.scss'
|
||||
import { diffStyles } from '../styles.js'
|
||||
|
||||
@@ -169,10 +168,10 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Label>
|
||||
<FieldDiffLabel>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{getTranslation(label, i18n)}
|
||||
</Label>
|
||||
</FieldDiffLabel>
|
||||
<ReactDiffViewer
|
||||
hideLineNumbers
|
||||
newValue={versionToRender}
|
||||
|
||||
@@ -3,10 +3,9 @@ import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Option, SelectField, SelectFieldDiffClientComponent } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import Label from '../../Label/index.js'
|
||||
import './index.scss'
|
||||
import { diffStyles } from '../styles.js'
|
||||
import { DiffViewer } from './DiffViewer/index.js'
|
||||
@@ -103,10 +102,10 @@ export const Select: SelectFieldDiffClientComponent = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Label>
|
||||
<FieldDiffLabel>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{'label' in field && getTranslation(field.label || '', i18n)}
|
||||
</Label>
|
||||
</FieldDiffLabel>
|
||||
<DiffViewer
|
||||
comparisonToRender={comparisonToRender}
|
||||
diffMethod={diffMethod}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import type { TextFieldDiffClientComponent } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import Label from '../../Label/index.js'
|
||||
import './index.scss'
|
||||
import { diffStyles } from '../styles.js'
|
||||
import { DiffViewer } from './DiffViewer/index.js'
|
||||
@@ -34,12 +33,12 @@ export const Text: TextFieldDiffClientComponent = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Label>
|
||||
<FieldDiffLabel>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{'label' in field &&
|
||||
typeof field.label !== 'function' &&
|
||||
getTranslation(field.label || '', i18n)}
|
||||
</Label>
|
||||
</FieldDiffLabel>
|
||||
<DiffViewer
|
||||
comparisonToRender={comparisonToRender}
|
||||
diffMethod={diffMethod}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const diffStyles = {
|
||||
import type { ReactDiffViewerStylesOverride } from 'react-diff-viewer-continued'
|
||||
|
||||
export const diffStyles: ReactDiffViewerStylesOverride = {
|
||||
diffContainer: {
|
||||
minWidth: 'unset',
|
||||
},
|
||||
@@ -26,4 +28,11 @@ export const diffStyles = {
|
||||
wordRemovedBackground: 'var(--theme-error-200)',
|
||||
},
|
||||
},
|
||||
wordAdded: {
|
||||
color: 'var(--theme-success-600)',
|
||||
},
|
||||
wordRemoved: {
|
||||
color: 'var(--theme-error-600)',
|
||||
textDecorationLine: 'line-through',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,16 +56,6 @@ export const withPayload = (nextConfig = {}, options = {}) => {
|
||||
...(nextConfig?.outputFileTracingIncludes || {}),
|
||||
'**/*': [...(nextConfig?.outputFileTracingIncludes?.['**/*'] || []), '@libsql/client'],
|
||||
},
|
||||
experimental: {
|
||||
...(nextConfig?.experimental || {}),
|
||||
turbo: {
|
||||
...(nextConfig?.experimental?.turbo || {}),
|
||||
resolveAlias: {
|
||||
...(nextConfig?.experimental?.turbo?.resolveAlias || {}),
|
||||
'payload-mock-package': 'payload-mock-package',
|
||||
},
|
||||
},
|
||||
},
|
||||
// We disable the poweredByHeader here because we add it manually in the headers function below
|
||||
...(nextConfig?.poweredByHeader !== false ? { poweredByHeader: false } : {}),
|
||||
headers: async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
@@ -98,7 +98,7 @@
|
||||
"file-type": "19.3.0",
|
||||
"get-tsconfig": "4.8.1",
|
||||
"http-status": "2.1.0",
|
||||
"image-size": "1.2.0",
|
||||
"image-size": "2.0.2",
|
||||
"jose": "5.9.6",
|
||||
"json-schema-to-typescript": "15.0.3",
|
||||
"minimist": "1.2.8",
|
||||
@@ -131,7 +131,7 @@
|
||||
"graphql-http": "^1.22.0",
|
||||
"react-datepicker": "7.6.0",
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.34.0",
|
||||
"typescript-strict-plugin": "2.4.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -5,12 +5,17 @@ import type { JSONSchema4 } from 'json-schema'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
|
||||
import type { ValidationFieldError } from '../errors/ValidationError.js'
|
||||
import type { FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
|
||||
import type {
|
||||
FieldAffectingData,
|
||||
RichTextField,
|
||||
RichTextFieldClient,
|
||||
Validate,
|
||||
} from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { RequestContext } from '../index.js'
|
||||
import type { JsonObject, PayloadRequest, PopulateType } from '../types/index.js'
|
||||
import type { RichTextFieldClientProps } from './fields/RichText.js'
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
import type { RichTextFieldClientProps, RichTextFieldServerProps } from './fields/RichText.js'
|
||||
import type { FieldDiffClientProps, FieldDiffServerProps, FieldSchemaMap } from './types.js'
|
||||
|
||||
export type AfterReadRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
@@ -248,7 +253,15 @@ export type RichTextAdapter<
|
||||
ExtraFieldProperties = any,
|
||||
> = {
|
||||
CellComponent: PayloadComponent<never>
|
||||
FieldComponent: PayloadComponent<never, RichTextFieldClientProps>
|
||||
/**
|
||||
* Component that will be displayed in the version diff view.
|
||||
* If not provided, richtext content will be diffed as JSON.
|
||||
*/
|
||||
DiffComponent?: PayloadComponent<
|
||||
FieldDiffServerProps<RichTextField, RichTextFieldClient>,
|
||||
FieldDiffClientProps<RichTextFieldClient>
|
||||
>
|
||||
FieldComponent: PayloadComponent<RichTextFieldServerProps, RichTextFieldClientProps>
|
||||
} & RichTextAdapterBase<Value, AdapterProps, ExtraFieldProperties>
|
||||
|
||||
export type RichTextAdapterProvider<
|
||||
|
||||
@@ -13,8 +13,12 @@ export type Data = {
|
||||
export type Row = {
|
||||
blockType?: string
|
||||
collapsed?: boolean
|
||||
customComponents?: {
|
||||
RowLabel?: React.ReactNode
|
||||
}
|
||||
id: string
|
||||
isLoading?: boolean
|
||||
lastRenderedPath?: string
|
||||
}
|
||||
|
||||
export type FilterOptionsResult = {
|
||||
@@ -22,6 +26,13 @@ export type FilterOptionsResult = {
|
||||
}
|
||||
|
||||
export type FieldState = {
|
||||
/**
|
||||
* This is used to determine if the field was added by the server.
|
||||
* This ensures the field is not ignored by the client when merging form state.
|
||||
* This can happen because the current local state is treated as the source of truth.
|
||||
* See `mergeServerFormState` for more details.
|
||||
*/
|
||||
addedByServer?: boolean
|
||||
customComponents?: {
|
||||
/**
|
||||
* This is used by UI fields, as they can have arbitrary components defined if used
|
||||
@@ -34,7 +45,6 @@ export type FieldState = {
|
||||
Error?: React.ReactNode
|
||||
Field?: React.ReactNode
|
||||
Label?: React.ReactNode
|
||||
RowLabels?: React.ReactNode[]
|
||||
}
|
||||
disableFormData?: boolean
|
||||
errorMessage?: string
|
||||
@@ -46,21 +56,17 @@ export type FieldState = {
|
||||
fieldSchema?: Field
|
||||
filterOptions?: FilterOptionsResult
|
||||
initialValue?: unknown
|
||||
passesCondition?: boolean
|
||||
requiresRender?: boolean
|
||||
rows?: Row[]
|
||||
/**
|
||||
* The `serverPropsToIgnore` obj is used to prevent the various properties from being overridden across form state requests.
|
||||
* This can happen when queueing a form state request with `requiresRender: true` while the another is already processing.
|
||||
* For example:
|
||||
* 1. One "add row" action will set `requiresRender: true` and dispatch a form state request
|
||||
* 2. Another "add row" action will set `requiresRender: true` and queue a form state request
|
||||
* 3. The first request will return with `requiresRender: false`
|
||||
* 4. The second request will be dispatched with `requiresRender: false` but should be `true`
|
||||
* To fix this, only merge the `requiresRender` property if the previous state has not set it to `true`.
|
||||
* See the `mergeServerFormState` function for implementation details.
|
||||
* The path of the field when its custom components were last rendered.
|
||||
* This is used to denote if a field has been rendered, and if so,
|
||||
* what path it was rendered under last.
|
||||
*
|
||||
* If this path is undefined, or, if it is different
|
||||
* from the current path of a given field, the field's components will be re-rendered.
|
||||
*/
|
||||
serverPropsToIgnore?: Array<keyof FieldState>
|
||||
lastRenderedPath?: string
|
||||
passesCondition?: boolean
|
||||
rows?: Row[]
|
||||
valid?: boolean
|
||||
validate?: Validate
|
||||
value?: unknown
|
||||
@@ -95,6 +101,13 @@ export type BuildFormStateArgs = {
|
||||
*/
|
||||
language?: keyof SupportedLanguages
|
||||
locale?: string
|
||||
/**
|
||||
* If true, will not render RSCs and instead return a simple string in their place.
|
||||
* This is useful for environments that lack RSC support, such as Jest.
|
||||
* Form state can still be built, but any server components will be omitted.
|
||||
* @default false
|
||||
*/
|
||||
mockRSCs?: boolean
|
||||
operation?: 'create' | 'update'
|
||||
/*
|
||||
If true, will render field components within their state object
|
||||
|
||||
@@ -99,7 +99,14 @@ export const updateDocument = async <
|
||||
const password = data?.password
|
||||
const shouldSaveDraft =
|
||||
Boolean(draftArg && collectionConfig.versions.drafts) && data._status !== 'published'
|
||||
const shouldSavePassword = Boolean(password && collectionConfig.auth && !shouldSaveDraft)
|
||||
const shouldSavePassword = Boolean(
|
||||
password &&
|
||||
collectionConfig.auth &&
|
||||
(!collectionConfig.auth.disableLocalStrategy ||
|
||||
(typeof collectionConfig.auth.disableLocalStrategy === 'object' &&
|
||||
collectionConfig.auth.disableLocalStrategy.enableFields)) &&
|
||||
!shouldSaveDraft,
|
||||
)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Handle potentially locked documents
|
||||
|
||||
@@ -106,24 +106,26 @@ export const addOrderableFieldsAndHook = (
|
||||
collection.hooks.beforeChange = []
|
||||
}
|
||||
|
||||
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, operation, req }) => {
|
||||
// Only set _order on create, not on update (unless explicitly provided)
|
||||
if (operation === 'create') {
|
||||
for (const orderableFieldName of orderableFieldNames) {
|
||||
if (!data[orderableFieldName]) {
|
||||
const lastDoc = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
select: { [orderableFieldName]: true },
|
||||
sort: `-${orderableFieldName}`,
|
||||
})
|
||||
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, originalDoc, req }) => {
|
||||
for (const orderableFieldName of orderableFieldNames) {
|
||||
if (!data[orderableFieldName] && !originalDoc?.[orderableFieldName]) {
|
||||
const lastDoc = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
select: { [orderableFieldName]: true },
|
||||
sort: `-${orderableFieldName}`,
|
||||
where: {
|
||||
[orderableFieldName]: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const lastOrderValue = lastDoc.docs[0]?.[orderableFieldName] || null
|
||||
data[orderableFieldName] = generateKeyBetween(lastOrderValue, null)
|
||||
}
|
||||
const lastOrderValue = lastDoc.docs[0]?.[orderableFieldName] || null
|
||||
data[orderableFieldName] = generateKeyBetween(lastOrderValue, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +165,7 @@ export const addOrderableEndpoint = (config: SanitizedConfig) => {
|
||||
}
|
||||
if (
|
||||
typeof target !== 'object' ||
|
||||
typeof target.id !== 'string' ||
|
||||
typeof target.id === 'undefined' ||
|
||||
typeof target.key !== 'string'
|
||||
) {
|
||||
return new Response(JSON.stringify({ error: 'target must be an object with id and key' }), {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Payload } from '../index.js'
|
||||
import type { PathToQuery } from './queryValidation/types.js'
|
||||
|
||||
import { APIError, type Payload, type SanitizedCollectionConfig } from '../index.js'
|
||||
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
type Field,
|
||||
@@ -151,21 +152,12 @@ export function getLocalizedPaths({
|
||||
}
|
||||
|
||||
switch (matchedField.type) {
|
||||
case 'json':
|
||||
case 'richText': {
|
||||
const upcomingSegments = pathSegments.slice(i + 1).join('.')
|
||||
lastIncompletePath.complete = true
|
||||
lastIncompletePath.path = upcomingSegments
|
||||
? `${currentPath}.${upcomingSegments}`
|
||||
: currentPath
|
||||
return paths
|
||||
}
|
||||
|
||||
case 'join':
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
// If this is a polymorphic relation,
|
||||
// We only support querying directly (no nested querying)
|
||||
if (typeof matchedField.relationTo !== 'string') {
|
||||
if (matchedField.type !== 'join' && typeof matchedField.relationTo !== 'string') {
|
||||
const lastSegmentIsValid =
|
||||
['relationTo', 'value'].includes(pathSegments[pathSegments.length - 1]) ||
|
||||
pathSegments.length === 1 ||
|
||||
@@ -188,7 +180,16 @@ export function getLocalizedPaths({
|
||||
.join('.')
|
||||
|
||||
if (nestedPathToQuery) {
|
||||
const relatedCollection = payload.collections[matchedField.relationTo].config
|
||||
let relatedCollection: SanitizedCollectionConfig
|
||||
if (matchedField.type === 'join') {
|
||||
if (Array.isArray(matchedField.collection)) {
|
||||
throw new APIError('Not supported')
|
||||
}
|
||||
|
||||
relatedCollection = payload.collections[matchedField.collection].config
|
||||
} else {
|
||||
relatedCollection = payload.collections[matchedField.relationTo as string].config
|
||||
}
|
||||
|
||||
const remainingPaths = getLocalizedPaths({
|
||||
collectionSlug: relatedCollection.slug,
|
||||
@@ -208,6 +209,15 @@ export function getLocalizedPaths({
|
||||
|
||||
break
|
||||
}
|
||||
case 'json':
|
||||
case 'richText': {
|
||||
const upcomingSegments = pathSegments.slice(i + 1).join('.')
|
||||
lastIncompletePath.complete = true
|
||||
lastIncompletePath.path = upcomingSegments
|
||||
? `${currentPath}.${upcomingSegments}`
|
||||
: currentPath
|
||||
return paths
|
||||
}
|
||||
|
||||
default: {
|
||||
if (i + 1 === pathSegments.length) {
|
||||
|
||||
@@ -13,6 +13,7 @@ type Args = {
|
||||
errors?: { path: string }[]
|
||||
overrideAccess: boolean
|
||||
policies?: EntityPolicies
|
||||
polymorphicJoin?: boolean
|
||||
req: PayloadRequest
|
||||
versionFields?: FlattenedField[]
|
||||
where: Where
|
||||
@@ -52,6 +53,7 @@ export async function validateQueryPaths({
|
||||
collections: {},
|
||||
globals: {},
|
||||
},
|
||||
polymorphicJoin,
|
||||
req,
|
||||
versionFields,
|
||||
where,
|
||||
@@ -77,6 +79,7 @@ export async function validateQueryPaths({
|
||||
overrideAccess,
|
||||
path,
|
||||
policies,
|
||||
polymorphicJoin,
|
||||
req,
|
||||
val,
|
||||
versionFields,
|
||||
|
||||
@@ -21,6 +21,7 @@ type Args = {
|
||||
parentIsLocalized?: boolean
|
||||
path: string
|
||||
policies: EntityPolicies
|
||||
polymorphicJoin?: boolean
|
||||
req: PayloadRequest
|
||||
val: unknown
|
||||
versionFields?: FlattenedField[]
|
||||
@@ -39,6 +40,7 @@ export async function validateSearchParam({
|
||||
parentIsLocalized,
|
||||
path: incomingPath,
|
||||
policies,
|
||||
polymorphicJoin,
|
||||
req,
|
||||
val,
|
||||
versionFields,
|
||||
@@ -102,6 +104,10 @@ export async function validateSearchParam({
|
||||
errors.push({ path })
|
||||
}
|
||||
|
||||
if (polymorphicJoin && path === 'relationTo') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!overrideAccess && fieldAffectsData(field)) {
|
||||
if (collectionSlug) {
|
||||
if (!policies.collections[collectionSlug]) {
|
||||
@@ -140,8 +146,10 @@ export async function validateSearchParam({
|
||||
const segments = fieldPath.split('.')
|
||||
|
||||
let fieldAccess
|
||||
|
||||
if (versionFields) {
|
||||
fieldAccess = policies[entityType][entitySlug]
|
||||
|
||||
if (segments[0] === 'parent' || segments[0] === 'version') {
|
||||
segments.shift()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SanitizedCollectionConfig, SanitizedJoin } from '../collections/config/types.js'
|
||||
import type { FlattenedField } from '../fields/config/types.js'
|
||||
import type { JoinQuery, PayloadRequest } from '../types/index.js'
|
||||
|
||||
import executeAccess from '../auth/executeAccess.js'
|
||||
@@ -67,6 +68,7 @@ const sanitizeJoinFieldQuery = async ({
|
||||
collectionConfig: joinCollectionConfig,
|
||||
errors,
|
||||
overrideAccess,
|
||||
polymorphicJoin: Array.isArray(join.field.collection),
|
||||
req,
|
||||
// incoming where input, but we shouldn't validate generated from the access control.
|
||||
where: joinQuery.where,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { en } from '@payloadcms/translations/languages/en'
|
||||
import { status as httpStatus } from 'http-status'
|
||||
|
||||
import type { LabelFunction, StaticLabel } from '../config/types.js'
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
|
||||
import { APIError } from './APIError.js'
|
||||
|
||||
@@ -28,6 +29,10 @@ export class ValidationError extends APIError<{
|
||||
errors: ValidationFieldError[]
|
||||
global?: string
|
||||
id?: number | string
|
||||
/**
|
||||
* req needs to be passed through (if you have one) in order to resolve label functions that may be part of the errors array
|
||||
*/
|
||||
req?: Partial<PayloadRequest>
|
||||
},
|
||||
t?: TFunction,
|
||||
) {
|
||||
@@ -37,8 +42,36 @@ export class ValidationError extends APIError<{
|
||||
? en.translations.error.followingFieldsInvalid_one
|
||||
: en.translations.error.followingFieldsInvalid_other
|
||||
|
||||
const req = results.req
|
||||
// delete to avoid logging the whole req
|
||||
delete results['req']
|
||||
|
||||
super(
|
||||
`${message} ${results.errors.map((f) => f.label || f.path).join(', ')}`,
|
||||
`${message} ${results.errors
|
||||
.map((f) => {
|
||||
if (f.label) {
|
||||
if (typeof f.label === 'function') {
|
||||
if (!req || !req.i18n || !req.t) {
|
||||
return f.path
|
||||
}
|
||||
|
||||
return f.label({ i18n: req.i18n, t: req.t })
|
||||
}
|
||||
|
||||
if (typeof f.label === 'object') {
|
||||
if (req?.i18n?.language) {
|
||||
return f.label[req.i18n.language]
|
||||
}
|
||||
|
||||
return f.label[Object.keys(f.label)[0]]
|
||||
}
|
||||
|
||||
return f.label
|
||||
}
|
||||
|
||||
return f.path
|
||||
})
|
||||
.join(', ')}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
results,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
|
||||
import { APIError } from '../../errors/index.js'
|
||||
import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
|
||||
import { flattenAllFields } from '../../utilities/flattenAllFields.js'
|
||||
import { getFieldByPath } from '../../utilities/getFieldByPath.js'
|
||||
import { traverseFields } from '../../utilities/traverseFields.js'
|
||||
import {
|
||||
fieldShouldBeLocalized,
|
||||
@@ -74,86 +76,40 @@ export const sanitizeJoinField = ({
|
||||
if (!joinCollection) {
|
||||
throw new InvalidFieldJoin(field)
|
||||
}
|
||||
let joinRelationship: RelationshipField | UploadField
|
||||
|
||||
const pathSegments = field.on.split('.') // Split the schema path into segments
|
||||
let currentSegmentIndex = 0
|
||||
|
||||
let localized = false
|
||||
// Traverse fields and match based on the schema path
|
||||
traverseFields({
|
||||
callback: ({ field, next, parentIsLocalized }) => {
|
||||
if (!('name' in field) || !field.name) {
|
||||
return
|
||||
}
|
||||
const currentSegment = pathSegments[currentSegmentIndex]
|
||||
// match field on path segments
|
||||
if ('name' in field && field.name === currentSegment) {
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
localized = true
|
||||
const fieldIndex = currentSegmentIndex
|
||||
|
||||
join.getForeignPath = ({ locale }) => {
|
||||
return pathSegments.reduce((acc, segment, index) => {
|
||||
let result = `${acc}${segment}`
|
||||
|
||||
if (index === fieldIndex) {
|
||||
result = `${result}.${locale}`
|
||||
}
|
||||
|
||||
if (index !== pathSegments.length - 1) {
|
||||
result = `${result}.`
|
||||
}
|
||||
|
||||
return result
|
||||
}, '')
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is the last segment in the path
|
||||
if (
|
||||
(currentSegmentIndex === pathSegments.length - 1 &&
|
||||
'type' in field &&
|
||||
field.type === 'relationship') ||
|
||||
field.type === 'upload'
|
||||
) {
|
||||
joinRelationship = field // Return the matched field
|
||||
next()
|
||||
return true
|
||||
} else {
|
||||
// Move to the next path segment and continue traversal
|
||||
currentSegmentIndex++
|
||||
}
|
||||
} else {
|
||||
// skip fields in non-matching path segments
|
||||
next()
|
||||
return
|
||||
}
|
||||
},
|
||||
config: config as unknown as SanitizedConfig,
|
||||
fields: joinCollection.fields,
|
||||
parentIsLocalized: false,
|
||||
const relationshipField = getFieldByPath({
|
||||
fields: flattenAllFields({ cache: true, fields: joinCollection.fields }),
|
||||
path: field.on,
|
||||
})
|
||||
|
||||
if (!joinRelationship) {
|
||||
if (
|
||||
!relationshipField ||
|
||||
(relationshipField.field.type !== 'relationship' && relationshipField.field.type !== 'upload')
|
||||
) {
|
||||
throw new InvalidFieldJoin(join.field)
|
||||
}
|
||||
|
||||
if (!joinRelationship.index && !joinRelationship.unique) {
|
||||
joinRelationship.index = true
|
||||
if (relationshipField.pathHasLocalized) {
|
||||
join.getForeignPath = ({ locale }) => {
|
||||
return relationshipField.localizedPath.replace('<locale>', locale)
|
||||
}
|
||||
}
|
||||
|
||||
if (!relationshipField.field.index && !relationshipField.field.unique) {
|
||||
relationshipField.field.index = true
|
||||
}
|
||||
|
||||
if (validateOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
join.targetField = joinRelationship
|
||||
join.targetField = relationshipField.field
|
||||
|
||||
// override the join field localized property to use whatever the relationship field has
|
||||
// or if it's nested to a localized array / blocks / tabs / group
|
||||
field.localized = localized
|
||||
field.localized = relationshipField.field.localized
|
||||
// override the join field hasMany property to use whatever the relationship field has
|
||||
field.hasMany = joinRelationship.hasMany
|
||||
field.hasMany = relationshipField.field.hasMany
|
||||
|
||||
// @ts-expect-error converting JoinField to FlattenedJoinField to track targetField
|
||||
field.targetField = join.targetField
|
||||
|
||||
@@ -57,7 +57,7 @@ import type {
|
||||
EmailFieldLabelServerComponent,
|
||||
FieldDescriptionClientProps,
|
||||
FieldDescriptionServerProps,
|
||||
FieldDiffClientComponent,
|
||||
FieldDiffClientProps,
|
||||
FieldDiffServerProps,
|
||||
GroupFieldClientProps,
|
||||
GroupFieldLabelClientComponent,
|
||||
@@ -326,7 +326,7 @@ type Admin = {
|
||||
components?: {
|
||||
Cell?: PayloadComponent<DefaultServerCellComponentProps, DefaultCellComponentProps>
|
||||
Description?: PayloadComponent<FieldDescriptionServerProps, FieldDescriptionClientProps>
|
||||
Diff?: PayloadComponent<FieldDiffServerProps, FieldDiffClientComponent>
|
||||
Diff?: PayloadComponent<FieldDiffServerProps, FieldDiffClientProps>
|
||||
Field?: PayloadComponent<FieldClientComponent | FieldServerComponent>
|
||||
/**
|
||||
* The Filter component has to be a client component
|
||||
|
||||
@@ -77,6 +77,7 @@ export const beforeChange = async <T extends JsonObject>({
|
||||
collection: collection?.slug,
|
||||
errors,
|
||||
global: global?.slug,
|
||||
req,
|
||||
},
|
||||
req.t,
|
||||
)
|
||||
|
||||
@@ -1508,5 +1508,5 @@ export { getLatestCollectionVersion } from './versions/getLatestCollectionVersio
|
||||
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
|
||||
export { saveVersion } from './versions/saveVersion.js'
|
||||
export type { SchedulePublishTaskInput } from './versions/schedule/types.js'
|
||||
export type { TypeWithVersion } from './versions/types.js'
|
||||
export type { SchedulePublish, TypeWithVersion } from './versions/types.js'
|
||||
export { deepMergeSimple } from '@payloadcms/translations/utilities'
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
// @ts-strict-ignore
|
||||
import fs from 'fs/promises'
|
||||
import sizeOfImport from 'image-size'
|
||||
import { promisify } from 'util'
|
||||
import { imageSize } from 'image-size'
|
||||
import { imageSizeFromFile } from 'image-size/fromFile'
|
||||
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
import type { ProbedImageSize } from './types.js'
|
||||
|
||||
import { temporaryFileTask } from './tempFile.js'
|
||||
|
||||
const { imageSize } = sizeOfImport
|
||||
const imageSizePromise = promisify(imageSize)
|
||||
|
||||
export async function getImageSize(file: PayloadRequest['file']): Promise<ProbedImageSize> {
|
||||
if (file.tempFilePath) {
|
||||
return imageSizePromise(file.tempFilePath)
|
||||
return imageSizeFromFile(file.tempFilePath)
|
||||
}
|
||||
|
||||
// Tiff file do not support buffers or streams, so we must write to file first
|
||||
@@ -22,7 +19,7 @@ export async function getImageSize(file: PayloadRequest['file']): Promise<Probed
|
||||
const dimensions = await temporaryFileTask(
|
||||
async (filepath: string) => {
|
||||
await fs.writeFile(filepath, file.data)
|
||||
return imageSizePromise(filepath)
|
||||
return imageSizeFromFile(filepath)
|
||||
},
|
||||
{ extension: 'tiff' },
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import type { User } from '../../auth/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { TaskConfig } from '../../queues/config/types/taskTypes.js'
|
||||
import type { SchedulePublishTaskInput } from './types.js'
|
||||
|
||||
@@ -87,11 +88,15 @@ export const getSchedulePublishTask = ({
|
||||
name: 'locale',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'doc',
|
||||
type: 'relationship',
|
||||
relationTo: collections,
|
||||
},
|
||||
...(collections.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'doc',
|
||||
type: 'relationship',
|
||||
relationTo: collections,
|
||||
} satisfies Field,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'global',
|
||||
type: 'select',
|
||||
|
||||
@@ -8,6 +8,23 @@ export type Autosave = {
|
||||
interval?: number
|
||||
}
|
||||
|
||||
export type SchedulePublish = {
|
||||
/**
|
||||
* Define a date format to use for the time picker.
|
||||
*
|
||||
* @example 'hh:mm' will give a 24 hour clock
|
||||
*
|
||||
* @default 'h:mm aa' which is a 12 hour clock
|
||||
*/
|
||||
timeFormat?: string
|
||||
/**
|
||||
* Intervals for the time picker.
|
||||
*
|
||||
* @default 5
|
||||
*/
|
||||
timeIntervals?: number
|
||||
}
|
||||
|
||||
export type IncomingDrafts = {
|
||||
/**
|
||||
* Enable autosave to automatically save progress while documents are edited.
|
||||
@@ -17,7 +34,7 @@ export type IncomingDrafts = {
|
||||
/**
|
||||
* Allow for editors to schedule publish / unpublish events in the future.
|
||||
*/
|
||||
schedulePublish?: boolean
|
||||
schedulePublish?: boolean | SchedulePublish
|
||||
/**
|
||||
* Set validate to true to validate draft documents when saved.
|
||||
*
|
||||
@@ -35,7 +52,7 @@ export type SanitizedDrafts = {
|
||||
/**
|
||||
* Allow for editors to schedule publish / unpublish events in the future.
|
||||
*/
|
||||
schedulePublish: boolean
|
||||
schedulePublish: boolean | SchedulePublish
|
||||
/**
|
||||
* Set validate to true to validate draft documents when saved.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -64,8 +64,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/find-node-modules": "^2.1.2",
|
||||
"@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": {
|
||||
|
||||
@@ -88,11 +88,11 @@ export const getFields = ({ collection, prefix }: Args): Field[] => {
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
...(existingSizeURLField || ({} as any)),
|
||||
...(existingSizeURLField || {}),
|
||||
...baseURLField,
|
||||
},
|
||||
],
|
||||
}
|
||||
} as Field
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export const getFields = ({
|
||||
fields: [
|
||||
...(adapter.fields || []),
|
||||
{
|
||||
...(existingSizeURLField || ({} as any)),
|
||||
...(existingSizeURLField || {}),
|
||||
...baseURLField,
|
||||
hooks: {
|
||||
afterRead: [
|
||||
@@ -124,7 +124,7 @@ export const getFields = ({
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
} as Field
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ export const getAfterDeleteHook = ({
|
||||
try {
|
||||
const filesToDelete: string[] = [
|
||||
doc.filename,
|
||||
...Object.values(doc?.sizes || []).map((resizedFileData) => resizedFileData?.filename),
|
||||
...Object.values(doc?.sizes || []).map(
|
||||
(resizedFileData) => resizedFileData?.filename as string,
|
||||
),
|
||||
]
|
||||
|
||||
const promises = filesToDelete.map(async (filename) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const getAfterReadHook =
|
||||
let url = value
|
||||
|
||||
if (disablePayloadAccessControl && filename) {
|
||||
url = await adapter.generateURL({
|
||||
url = await adapter.generateURL?.({
|
||||
collection,
|
||||
data,
|
||||
filename,
|
||||
|
||||
@@ -29,7 +29,7 @@ export const getBeforeChangeHook =
|
||||
if (typeof originalDoc.sizes === 'object') {
|
||||
filesToDelete = filesToDelete.concat(
|
||||
Object.values(originalDoc?.sizes || []).map(
|
||||
(resizedFileData) => resizedFileData?.filename,
|
||||
(resizedFileData) => resizedFileData?.filename as string,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -67,9 +67,6 @@ export const cloudStoragePlugin =
|
||||
if ('clientUploadContext' in args.params) {
|
||||
return adapter.staticHandler(req, args)
|
||||
}
|
||||
|
||||
// Otherwise still skip staticHandler
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export const initClientUploads = <ExtraProps extends Record<string, unknown>, T>
|
||||
clientProps: {
|
||||
collectionSlug,
|
||||
enabled,
|
||||
extra: extraClientHandlerProps ? extraClientHandlerProps(collection) : undefined,
|
||||
extra: extraClientHandlerProps ? extraClientHandlerProps(collection!) : undefined,
|
||||
prefix,
|
||||
serverHandlerPath,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [{ "path": "../payload" }, { "path": "../ui" }]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
@@ -67,8 +67,8 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -36,7 +36,7 @@ export const sendEmail = async (
|
||||
|
||||
if (emails && emails.length) {
|
||||
const formattedEmails: FormattedEmail[] = await Promise.all(
|
||||
emails.map(async (email: Email): Promise<FormattedEmail | null> => {
|
||||
emails.map(async (email: Email): Promise<FormattedEmail> => {
|
||||
const {
|
||||
bcc: emailBCC,
|
||||
cc: emailCC,
|
||||
|
||||
@@ -23,6 +23,7 @@ export const generateSubmissionCollection = (
|
||||
},
|
||||
relationTo: formSlug,
|
||||
required: true,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
validate: async (value, { req: { payload }, req }) => {
|
||||
/* Don't run in the client side */
|
||||
if (!payload) {
|
||||
@@ -40,7 +41,7 @@ export const generateSubmissionCollection = (
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return 'Cannot create this submission because this form does not exist.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const DynamicFieldSelector: React.FC<
|
||||
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter((field) => field !== null)
|
||||
setOptions(allNonPaymentFields)
|
||||
}
|
||||
}, [fields, getDataByPath])
|
||||
@@ -40,9 +40,8 @@ export const DynamicFieldSelector: React.FC<
|
||||
<SelectField
|
||||
{...props}
|
||||
field={{
|
||||
name: props?.field?.name,
|
||||
options,
|
||||
...(props.field || {}),
|
||||
options,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -57,11 +57,11 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col
|
||||
],
|
||||
})
|
||||
|
||||
if (redirect.fields[2].type !== 'row') {
|
||||
redirect.fields[2].label = 'Custom URL'
|
||||
if (redirect.fields[2]!.type !== 'row') {
|
||||
redirect.fields[2]!.label = 'Custom URL'
|
||||
}
|
||||
|
||||
redirect.fields[2].admin = {
|
||||
redirect.fields[2]!.admin = {
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const replaceDoubleCurlys = (str: string, variables?: EmailVariables): st
|
||||
return variables.map(({ field, value }) => `${field} : ${value}`).join(' <br /> ')
|
||||
} else if (variable === '*:table') {
|
||||
return keyValuePairToHtmlTable(
|
||||
variables.reduce((acc, { field, value }) => {
|
||||
variables.reduce<Record<string, string>>((acc, { field, value }) => {
|
||||
acc[field] = value
|
||||
return acc
|
||||
}, {}),
|
||||
|
||||
@@ -106,7 +106,7 @@ export const serializeSlate = (children?: Node[], submissionData?: any): string
|
||||
`
|
||||
case 'link':
|
||||
return `
|
||||
<a href={${escapeHTML(replaceDoubleCurlys(node.url, submissionData))}}>
|
||||
<a href={${escapeHTML(replaceDoubleCurlys(node.url!, submissionData))}}>
|
||||
${serializeSlate(node.children, submissionData)}
|
||||
</a>
|
||||
`
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
},
|
||||
"references": [{ "path": "../payload" }, { "path": "../ui" }]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-multi-tenant",
|
||||
"version": "3.32.0",
|
||||
"version": "3.34.0",
|
||||
"description": "Multi Tenant plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user