Compare commits
30 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d192f1414d | ||
|
|
755355ea68 | ||
|
|
58441c2bcc | ||
|
|
3918c09013 | ||
|
|
bf989e6041 | ||
|
|
57fba36257 | ||
|
|
df4661a388 | ||
|
|
03e5ae8095 | ||
|
|
d89db00295 | ||
|
|
8970c6b3a6 | ||
|
|
0574155e59 | ||
|
|
03331de2ac | ||
|
|
d64946c2e2 | ||
|
|
c41ef65a2b | ||
|
|
d38d7b8932 | ||
|
|
01ccbd48b0 | ||
|
|
61b4f2efd7 | ||
|
|
f4041ce6e2 | ||
|
|
123125185c | ||
|
|
04bd502d37 | ||
|
|
dae832c288 | ||
|
|
6cdf141380 | ||
|
|
29704428bd | ||
|
|
6c341b5661 | ||
|
|
9c530e47bb | ||
|
|
7ba19e03d6 | ||
|
|
c0aa96f59a | ||
|
|
c7bde52aba | ||
|
|
915a3ce3f5 | ||
|
|
b6867f222b |
1
.github/workflows/pr-title.yml
vendored
1
.github/workflows/pr-title.yml
vendored
@@ -46,6 +46,7 @@ jobs:
|
||||
live-preview
|
||||
live-preview-react
|
||||
next
|
||||
payload-cloud
|
||||
plugin-cloud
|
||||
plugin-cloud-storage
|
||||
plugin-form-builder
|
||||
|
||||
@@ -18,7 +18,7 @@ There are many use cases for Access Control, including:
|
||||
- Only allowing public access to posts where a `status` field is equal to `published`
|
||||
- Giving only users with a `role` field equal to `admin` the ability to delete posts
|
||||
- Allowing anyone to submit contact forms, but only logged in users to `read`, `update` or `delete` them
|
||||
- Restricting a user to only be able to see their own orders, but noone else's
|
||||
- Restricting a user to only be able to see their own orders, but no-one else's
|
||||
- Allowing users that belong to a certain organization to access only that organization's resources
|
||||
|
||||
There are three main types of Access Control in Payload:
|
||||
|
||||
@@ -8,7 +8,7 @@ keywords: admin, components, custom, documentation, Content Management System, c
|
||||
|
||||
The Payload [Admin Panel](./overview) is designed to be as minimal and straightforward as possible to allow for both easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](../configuration/overview).
|
||||
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api/overview) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
@@ -329,7 +329,7 @@ export const useMyCustomContext = () => useContext(MyCustomContext)
|
||||
|
||||
## Building Custom Components
|
||||
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api) directly on the front-end, among other things.
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api/overview) directly on the front-end, among other things.
|
||||
|
||||
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class and the [`i18n`](../configuration/i18n) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself.
|
||||
|
||||
|
||||
@@ -760,7 +760,7 @@ const LinkFromCategoryToPosts: React.FC = () => {
|
||||
|
||||
## useLocale
|
||||
|
||||
In any Custom Component you can get the selected locale object with the `useLocale` hook. `useLocale`gives you the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example:
|
||||
In any Custom Component you can get the selected locale object with the `useLocale` hook. `useLocale` gives you the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
@@ -93,7 +93,7 @@ For more granular control, pass a configuration object instead. Payload exposes
|
||||
| **`path`** \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
|
||||
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||
| **`sensitive`** | When true, will match if the path is case sensitive.
|
||||
| **`sensitive`** | When true, will match if the path is case sensitive.|
|
||||
| **`meta`** | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
@@ -133,6 +133,12 @@ The above example shows how to add a new [Root View](#root-views), but the patte
|
||||
route.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Custom views are public</strong>
|
||||
<br />
|
||||
Custom views are public by default. If your view requires a user to be logged in or to have certain access rights, you should handle that within your view component yourself.
|
||||
</Banner>
|
||||
|
||||
## Collection Views
|
||||
|
||||
Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views.
|
||||
|
||||
@@ -86,7 +86,7 @@ The following options are available:
|
||||
| **`loginWithUsername`** | Ability to allow users to login with username/password. [More](/docs/authentication/overview#login-with-username) |
|
||||
| **`maxLoginAttempts`** | Only allow a user to attempt logging in X amount of times. Automatically locks out a user from authenticating if this limit is passed. Set to `0` to disable. |
|
||||
| **`removeTokenFromResponses`** | Set to true if you want to remove the token from the returned authentication API responses such as login or refresh. |
|
||||
| **`strategies`** | Advanced - an array of custom authentification strategies to extend this collection's authentication with. [More details](./custom-strategies). |
|
||||
| **`strategies`** | Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](./custom-strategies). |
|
||||
| **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
|
||||
| **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). |
|
||||
| **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](./email#email-verification). |
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Storing data for read on the request object.
|
||||
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appeneded to the request for you.
|
||||
During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appended to the request for you.
|
||||
|
||||
### Definining Token Data
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export const Posts: CollectionConfig = {
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/collections). |
|
||||
| **`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). |
|
||||
@@ -77,6 +77,7 @@ The following options are available:
|
||||
| **`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). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ In development mode, if the configuration file is not found at the root, Payload
|
||||
|
||||
**Production Mode**
|
||||
|
||||
In production mode, Payload will first attempt to find the config file in the `outDir` of your `tsconfig.json`, and if not found, will fallback to the `rootDor` directory:
|
||||
In production mode, Payload will first attempt to find the config file in the `outDir` of your `tsconfig.json`, and if not found, will fallback to the `rootDir` directory:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -126,6 +126,6 @@ await payload.update({
|
||||
where: {
|
||||
slug: { equals: 'my-slug' }
|
||||
},
|
||||
req: { disableTransaction: true },
|
||||
disableTransaction: true,
|
||||
})
|
||||
```
|
||||
|
||||
@@ -121,23 +121,33 @@ powerful Admin UI.
|
||||
|
||||
## Config Options
|
||||
|
||||
| Option | Description |
|
||||
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`collection`** \* | The `slug`s having the relationship field. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth) |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
|
||||
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
|
||||
| **`admin`** | Admin-specific configuration. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
|
||||
| Option | Description |
|
||||
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`collection`** \* | The `slug`s having the relationship field. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
|
||||
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
## Admin Config Options
|
||||
|
||||
You can control the user experience of the join field using the `admin` config properties. The following options are supported:
|
||||
|
||||
| Option | Description |
|
||||
|------------------------|----------------------------------------------------------------------------------------|
|
||||
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
|
||||
| **`components.Label`** | Override the default Label of the Field Component. [More details](#the-label-component). |
|
||||
|
||||
## Join Field Data
|
||||
|
||||
When a document is returned that for a Join field is populated with related documents. The structure returned is an
|
||||
|
||||
@@ -83,7 +83,7 @@ The Radio Field inherits all of the default options from the base [Field Admin C
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`layout`** | Allows for the radio group to be styled as a horizonally or vertically distributed list. The default value is `horizontal`. |
|
||||
| **`layout`** | Allows for the radio group to be styled as a horizontally or vertically distributed list. The default value is `horizontal`. |
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -25,11 +25,7 @@ Right now, Payload is officially supporting two rich text editors:
|
||||
<Banner type="success">
|
||||
<strong>
|
||||
Consistent with Payload's goal of making you learn as little of Payload as possible, customizing
|
||||
and using the Rich Text Editor does not involve learning how to develop for a
|
||||
{' '}
|
||||
<em>Payload</em>
|
||||
{' '}
|
||||
rich text editor.
|
||||
and using the Rich Text Editor does not involve learning how to develop for a{' '}<em>Payload</em>{' '}rich text editor.
|
||||
</strong>
|
||||
|
||||
Instead, you can invest your time and effort into learning the underlying open-source tools that
|
||||
|
||||
@@ -46,7 +46,7 @@ export const MyUploadField: Field = {
|
||||
| Option | Description |
|
||||
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`*relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
|
||||
| **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
||||
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
|
||||
@@ -139,4 +139,4 @@ declare module 'payload' {
|
||||
}
|
||||
```
|
||||
|
||||
This will add a the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as type augmentation can mess up your types if you do it wrong.
|
||||
This will add the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as type augmentation can mess up your types if you do it wrong.
|
||||
|
||||
382
docs/jobs-queue/overview.mdx
Normal file
382
docs/jobs-queue/overview.mdx
Normal file
@@ -0,0 +1,382 @@
|
||||
---
|
||||
title: Jobs Queue
|
||||
label: Jobs Queue
|
||||
order: 10
|
||||
desc: Payload provides all you need to run job queues, which are helpful to offload long-running processes into separate workers.
|
||||
keywords: jobs queue, application framework, typescript, node, react, nextjs
|
||||
---
|
||||
|
||||
## Defining tasks
|
||||
|
||||
A task is a simple function that can be executed directly or within a workflow. The difference between tasks and functions is that tasks can be run in the background, and can be retried if they fail.
|
||||
|
||||
Tasks can either be defined within the `jobs.tasks` array in your payload config, or they can be run inline within a workflow.
|
||||
|
||||
### Defining tasks in the config
|
||||
|
||||
Simply add a task to the `jobs.tasks` array in your Payload config. A task consists of the following fields:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `slug` | Define a slug-based name for this job. This slug needs to be unique among both tasks and workflows.|
|
||||
| `handler` | The function that should be responsible for running the job. You can either pass a string-based path to the job function file, or the job function itself. If you are using large dependencies within your job, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. |
|
||||
| `inputSchema` | Define the input field schema - payload will generate a type for this schema. |
|
||||
| `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this task. By default, this is "Task" + the capitalized task slug. |
|
||||
| `outputSchema` | Define the output field schema - payload will generate a type for this schema. |
|
||||
| `label` | Define a human-friendly label for this task. |
|
||||
| `onFail` | Function to be executed if the task fails. |
|
||||
| `onSuccess` | Function to be executed if the task fails. |
|
||||
| `retries` | Specify the number of times that this step should be retried if it fails. |
|
||||
|
||||
The handler is the function, or a path to the function, that will run once the job picks up this task. The handler function should return an object with an `output` key, which should contain the output of the task.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
retries: 2,
|
||||
slug: 'createPost',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ input, job, req }) => {
|
||||
const newPost = await req.payload.create({
|
||||
collection: 'post',
|
||||
req,
|
||||
data: {
|
||||
title: input.title,
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
postID: newPost.id,
|
||||
},
|
||||
}
|
||||
},
|
||||
} as TaskConfig<'createPost'>,
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Example: defining external tasks
|
||||
|
||||
payload.config.ts:
|
||||
|
||||
```ts
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
retries: 2,
|
||||
slug: 'createPost',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: path.resolve(dirname, 'src/tasks/createPost.ts') + '#createPostHandler',
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
src/tasks/createPost.ts:
|
||||
|
||||
```ts
|
||||
import type { TaskHandler } from 'payload'
|
||||
|
||||
export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job, req }) => {
|
||||
const newPost = await req.payload.create({
|
||||
collection: 'post',
|
||||
req,
|
||||
data: {
|
||||
title: input.title,
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
postID: newPost.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Defining workflows
|
||||
|
||||
There are two types of workflows - JS-based workflows and JSON-based workflows.
|
||||
|
||||
### Defining JS-based workflows
|
||||
|
||||
A JS-based function is a function in which you decide yourself when the tasks should run, by simply calling the `runTask` function. If the job, or any task within the job, fails, the entire function will re-run.
|
||||
|
||||
Tasks that have successfully been completed will simply re-return the cached output without running again, and failed tasks will be re-run.
|
||||
|
||||
Simply add a workflow to the `jobs.wokflows` array in your Payload config. A wokflow consists of the following fields:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `slug` | Define a slug-based name for this workflow. This slug needs to be unique among both tasks and workflows.|
|
||||
| `handler` | The function that should be responsible for running the workflow. You can either pass a string-based path to the workflow function file, or workflow job function itself. If you are using large dependencies within your workflow, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. |
|
||||
| `inputSchema` | Define the input field schema - payload will generate a type for this schema. |
|
||||
| `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this workflow. By default, this is "Workflow" + the capitalized workflow slug. |
|
||||
| `label` | Define a human-friendly label for this workflow. |
|
||||
| `queue` | Optionally, define the queue name that this workflow should be tied to. Defaults to "default". |
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// ...
|
||||
]
|
||||
workflows: [
|
||||
{
|
||||
slug: 'createPostAndUpdate',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ job, runTask }) => {
|
||||
const output = await runTask({
|
||||
task: 'createPost',
|
||||
id: '1',
|
||||
input: {
|
||||
title: job.input.title,
|
||||
},
|
||||
})
|
||||
|
||||
await runTask({
|
||||
task: 'updatePost',
|
||||
id: '2',
|
||||
input: {
|
||||
post: job.taskStatus.createPost['1'].output.postID, // or output.postID
|
||||
title: job.input.title + '2',
|
||||
},
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'updatePost'>
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Running tasks inline
|
||||
|
||||
In order to run tasks inline without predefining them, you can use the `runTaskInline` function.
|
||||
|
||||
The drawbacks of this approach are that tasks cannot be re-used as easily, and the **task data stored in the job** will not be typed. In the following example, the inline task data will be stored on the job under `job.taskStatus.inline['2']` but completely untyped, as types for dynamic tasks like these cannot be generated beforehand.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// ...
|
||||
]
|
||||
workflows: [
|
||||
{
|
||||
slug: 'createPostAndUpdate',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ job, runTask }) => {
|
||||
const output = await runTask({
|
||||
task: 'createPost',
|
||||
id: '1',
|
||||
input: {
|
||||
title: job.input.title,
|
||||
},
|
||||
})
|
||||
|
||||
const { newPost } = await runTaskInline({
|
||||
task: async ({ req }) => {
|
||||
const newPost = await req.payload.update({
|
||||
collection: 'post',
|
||||
id: output.postID,
|
||||
req,
|
||||
retries: 3,
|
||||
data: {
|
||||
title: 'updated!',
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
newPost
|
||||
},
|
||||
}
|
||||
},
|
||||
id: '2',
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'updatePost'>
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Defining JSON-based workflows
|
||||
|
||||
JSON-based workflows are a way to define the tasks the workflow should run in an array. The relationships between the tasks, their run order and their conditions are defined in the JSON object, which allows payload to statically analyze the workflow and will generate more helpful graphs.
|
||||
|
||||
This functionality is not available yet, but it will be available in the future.
|
||||
|
||||
## Queueing workflows and tasks
|
||||
|
||||
In order to queue a workflow or a task (= create them and add them to the queue), you can use the `payload.jobs.queue` function.
|
||||
|
||||
Example: queueing workflows:
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
workflows: 'createPostAndUpdate',
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Example: queueing tasks:
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
task: 'createPost',
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Running workflows and tasks
|
||||
|
||||
Workflows and tasks added to the queue will not run unless a worker picks it up and runs it. This can be done in two ways:
|
||||
|
||||
### Endpoint
|
||||
|
||||
Make a fetch request to the `api/payload-jobs/run` endpoint:
|
||||
|
||||
```ts
|
||||
await fetch('/api/payload-jobs/run', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `JWT ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Local API
|
||||
|
||||
Run the payload.jobs.run function:
|
||||
|
||||
```ts
|
||||
const results = await payload.jobs.run()
|
||||
|
||||
// You can customize the queue name by passing it as an argument
|
||||
await payload.jobs.run({ queue: 'posts' })
|
||||
```
|
||||
|
||||
### Script
|
||||
|
||||
You can run the jobs:run script from the command line:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --queue default --limit 10
|
||||
```
|
||||
|
||||
#### Triggering jobs as cronjob
|
||||
|
||||
You can pass the --cron flag to the jobs:run script to run the jobs in a cronjob:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --cron "*/5 * * * *"
|
||||
```
|
||||
|
||||
### Vercel Cron
|
||||
|
||||
Vercel Cron allows scheduled tasks to be executed automatically by triggering specific endpoints. Below is a step-by-step guide to configuring Vercel Cron for running queued jobs on apps hosted on Vercel:
|
||||
|
||||
1. Add Vercel Cron Configuration: Place a vercel.json file at the root of your project with the following content:
|
||||
|
||||
```json
|
||||
{
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/payload-jobs/run",
|
||||
"schedule": "*/5 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This configuration schedules the endpoint `/api/payload-jobs/run` to be triggered every 5 minutes. This endpoint is added automatically by payload and is responsible for running the queued jobs.
|
||||
|
||||
2. Environment Variable Setup: By default, the endpoint may require a JWT token for authorization. However, Vercel Cron jobs cannot pass JWT tokens. Instead, you can use an environment variable to secure the endpoint:
|
||||
|
||||
Add a new environment variable named `CRON_SECRET` to your Vercel project settings. This should be a random string, ideally 16 characters or longer.
|
||||
|
||||
3. Modify Authentication for Job Running: Adjust the job running authorization logic in your project to accept the `CRON_SECRET` as a valid token. Modify your `payload.config.ts` file as follows:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
access: {
|
||||
run: ({ req }: { req: PayloadRequest }): boolean => {
|
||||
const authHeader = req.headers.get('authorization');
|
||||
return authHeader === `Bearer ${process.env.CRON_SECRET}`;
|
||||
},
|
||||
},
|
||||
// Other job configurations...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This code snippet ensures that the jobs can only be triggered if the correct `CRON_SECRET` is provided in the authorization header.
|
||||
|
||||
Vercel will automatically make the `CRON_SECRET` environment variable available to the endpoint when triggered by the Vercel Cron, ensuring that the jobs can be run securely.
|
||||
|
||||
After the project is deployed to Vercel, the Vercel Cron job will automatically trigger the `/api/payload-jobs/run` endpoint in the specified schedule, running the queued jobs in the background.
|
||||
@@ -52,7 +52,7 @@ const Pages: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
and done! Now, everytime this lexical editor is initialized, it converts the slate date to lexical on-the-fly. If the data is already in lexical format, it will just pass it through.
|
||||
and done! Now, every time this lexical editor is initialized, it converts the slate date to lexical on-the-fly. If the data is already in lexical format, it will just pass it through.
|
||||
|
||||
This is by far the easiest way to migrate from Slate to Lexical, although it does come with a few caveats:
|
||||
|
||||
|
||||
@@ -77,21 +77,22 @@ Both options function in exactly the same way outside of one having HMR support
|
||||
|
||||
You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in.
|
||||
|
||||
| Local Option | Description |
|
||||
|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. |
|
||||
| `data` | The data to use within the operation. Required for `create`, `update`. |
|
||||
| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. |
|
||||
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
|
||||
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
|
||||
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
|
||||
| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). |
|
||||
| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. |
|
||||
| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. |
|
||||
| `pagination` | Set to false to return all documents and avoid querying for document counts. |
|
||||
| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. |
|
||||
| `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. |
|
||||
| `disableTransaction` | When set to `true`, a [database transactions](../database/transactions) will not be initialized. |
|
||||
| Local Option | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collection` | Required for Collection operations. Specifies the Collection slug to operate against. |
|
||||
| `data` | The data to use within the operation. Required for `create`, `update`. |
|
||||
| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. |
|
||||
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
|
||||
| `select` | Specify [select](../queries/select) to control which fields to include to the result. |
|
||||
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
|
||||
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
|
||||
| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). |
|
||||
| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. |
|
||||
| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. |
|
||||
| `pagination` | Set to false to return all documents and avoid querying for document counts. |
|
||||
| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. |
|
||||
| `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. |
|
||||
| `disableTransaction` | When set to `true`, a [database transactions](../database/transactions) will not be initialized. |
|
||||
|
||||
_There are more options available on an operation by operation basis outlined below._
|
||||
|
||||
|
||||
@@ -343,12 +343,12 @@ Maps to a `checkbox` input on your front-end. Used to collect a boolean value.
|
||||
Maps to a `number` input on your front-end. Used to collect a number.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | -------- | ---------------------------------------------------- | --- | -------------- | ------ | ------------------------------- |
|
||||
| -------------- | -------- | ---------------------------------------------------- |
|
||||
| `name` | string | The name of the field. |
|
||||
| `label` | string | The label of the field. |
|
||||
| `defaultValue` | string | The default value of the field. |
|
||||
| `defaultValue` | number | The default value of the field. |
|
||||
| `width` | string | The width of the field on the front-end. |
|
||||
| `required` | checkbox | Whether or not the field is required when submitted. | | `defaultValue` | number | The default value of the field. |
|
||||
| `required` | checkbox | Whether or not the field is required when submitted. |
|
||||
|
||||
### Message
|
||||
|
||||
@@ -416,7 +416,7 @@ This plugin relies on the [email configuration](../email/overview) defined in yo
|
||||
|
||||
### Email formatting
|
||||
|
||||
The email contents supports rich text which will be serialised to HTML on the server before being sent. By default it reads the global configuration of your rich text editor.
|
||||
The email contents supports rich text which will be serialized to HTML on the server before being sent. By default it reads the global configuration of your rich text editor.
|
||||
|
||||
The email subject and body supports inserting dynamic fields from the form submission data using the `{{field_name}}` syntax. For example, if you have a field called `name` in your form, you can include this in the email body like so:
|
||||
|
||||
|
||||
@@ -81,6 +81,10 @@ export default config
|
||||
|
||||
The `collections` property is an array of collection slugs to enable syncing to search. Enabled collections receive a `beforeChange` and `afterDelete` hook that creates, updates, and deletes its respective search record as it changes over time.
|
||||
|
||||
### `localize`
|
||||
|
||||
By default, the search plugin will add `localization: true` to the `title` field of the newly added `search` collection if you have localization enabled. If you would like to disable this behavior, you can set this to `false`.
|
||||
|
||||
#### `defaultPriorities`
|
||||
|
||||
This plugin automatically adds a `priority` field to the `search` collection that can be used as the `?sort=` parameter in your queries. For example, you may want to list blog posts before pages. Or you may want one specific post to always take appear first.
|
||||
|
||||
130
docs/queries/select.mdx
Normal file
130
docs/queries/select.mdx
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: Select
|
||||
label: Select
|
||||
order: 30
|
||||
desc: Payload select determines which fields are selected to the result.
|
||||
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
You may not need the full data from your Local API / REST queries, but only some specific fields. The select fields API can help you to optimize those cases.
|
||||
|
||||
## Local API
|
||||
|
||||
To specify select in the [Local API](../local-api/overview), you can use the `select` option in your query:
|
||||
|
||||
```ts
|
||||
// Include mode
|
||||
const getPosts = async () => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
select: {
|
||||
text: true,
|
||||
// select a specific field from group
|
||||
group: {
|
||||
number: true
|
||||
},
|
||||
// select all fields from array
|
||||
array: true,
|
||||
}, // highlight-line
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
// Exclude mode
|
||||
const getPosts = async () => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
// Select everything except for array and group.number
|
||||
select: {
|
||||
array: false,
|
||||
group: {
|
||||
number: false
|
||||
}
|
||||
}, // highlight-line
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
To perform querying with `select` efficiently, it works on the database level. Because of that, your `beforeRead` and `afterRead` hooks may not receive the full `doc`.
|
||||
</Banner>
|
||||
|
||||
|
||||
## REST API
|
||||
|
||||
To specify select in the [REST API](../rest-api/overview), you can use the `select` parameter in your query:
|
||||
|
||||
```ts
|
||||
fetch('https://localhost:3000/api/posts?select[color]=true&select[group][number]=true') // highlight-line
|
||||
.then((res) => res.json())
|
||||
.then((data) => console.log(data))
|
||||
```
|
||||
|
||||
To understand the syntax, you need to understand that complex URL search strings are parsed into a JSON object. This one isn't too bad, but more complex queries get unavoidably more difficult to write.
|
||||
|
||||
For this reason, we recommend to use the extremely helpful and ubiquitous [`qs`](https://www.npmjs.com/package/qs) package to parse your JSON / object-formatted queries into query strings:
|
||||
|
||||
```ts
|
||||
import { stringify } from 'qs-esm'
|
||||
|
||||
const select = {
|
||||
text: true,
|
||||
group: {
|
||||
number: true
|
||||
}
|
||||
// This query could be much more complex
|
||||
// and QS would handle it beautifully
|
||||
}
|
||||
|
||||
const getPosts = async () => {
|
||||
const stringifiedQuery = stringify(
|
||||
{
|
||||
select, // ensure that `qs` adds the `select` property, too!
|
||||
},
|
||||
{ addQueryPrefix: true },
|
||||
)
|
||||
|
||||
const response = await fetch(`http://localhost:3000/api/posts${stringifiedQuery}`)
|
||||
// Continue to handle the response below...
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Reminder:</strong>
|
||||
This is the same for [Globals](../configuration/globals) using the `/api/globals` endpoint.
|
||||
</Banner>
|
||||
|
||||
|
||||
## `defaultPopulate` collection config property
|
||||
|
||||
The `defaultPopulate` property allows you specify which fields to select when populating the collection from another document.
|
||||
This is especially useful for links where only the `slug` is needed instead of the entire document.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor, LinkFeature } from '@payloadcms/richtext-lexical'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
|
||||
// The TSlug generic can be passed to have type safety for `defaultPopulate`.
|
||||
// If avoided, the `defaultPopulate` type resolves to `SelectType`.
|
||||
export const Pages: CollectionConfig<'pages'> = {
|
||||
slug: 'pages',
|
||||
// Specify `select`.
|
||||
defaultPopulate: {
|
||||
slug: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
@@ -6,7 +6,7 @@ desc: Payload sort allows you to order your documents by a field in ascending or
|
||||
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order. If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specificed by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields.
|
||||
Documents in Payload can be easily sorted by a specific [Field](../fields/overview). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order. If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specified by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields.
|
||||
|
||||
Because sorting is handled by the database, the field cannot be a [Virtual Field](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges). It must be stored in the database to be searchable.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ All Payload API routes are mounted and prefixed to your config's `routes.api` UR
|
||||
- [depth](../queries/depth) - automatically populates relationships and uploads
|
||||
- [locale](/docs/configuration/localization#retrieving-localized-docs) - retrieves document(s) in a specific locale
|
||||
- [fallback-locale](/docs/configuration/localization#retrieving-localized-docs) - specifies a fallback locale if no locale value exists
|
||||
- [select](../queries/select) - specifies which fields to include to the result
|
||||
|
||||
## Collections
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ _An asterisk denotes that an option is required._
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
|
||||
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
| **`filenameCompoundIndex`** | Field slugs to use for a compount index instead of the default filename index.
|
||||
| **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index.
|
||||
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
|
||||
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
|
||||
| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. |
|
||||
@@ -144,7 +144,7 @@ export default buildConfig({
|
||||
|
||||
If you specify an array of `imageSizes` to your `upload` config, Payload will automatically crop and resize your uploads to fit each of the sizes specified by your config.
|
||||
|
||||
The [Admin Panel](../admin/overview) will also automatically display all available files, including width, height, and filesize, for each of your uploaded files.
|
||||
The [Admin Panel](../admin/overview) will also automatically display all available files, including width, height, and file size, for each of your uploaded files.
|
||||
|
||||
Behind the scenes, Payload relies on [`sharp`](https://sharp.pixelplumbing.com/api-resize#resize) to perform its image resizing. You can specify additional options for `sharp` to use while resizing your images.
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ pnpm add @payloadcms/storage-s3@beta
|
||||
|
||||
### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
|
||||
@@ -124,7 +124,7 @@ pnpm add @payloadcms/storage-azure@beta
|
||||
|
||||
### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
|
||||
```ts
|
||||
@@ -173,7 +173,7 @@ pnpm add @payloadcms/storage-gcs@beta
|
||||
|
||||
### Usage
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
|
||||
```ts
|
||||
@@ -310,7 +310,7 @@ This plugin is configurable to work across many different Payload collections. A
|
||||
| Option | Type | Description |
|
||||
| ---------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `collections` \* | `Record<string, CollectionOptions>` | Object with keys set to the slug of collections you want to enable the plugin for, and values set to collection-specific options. |
|
||||
| `enabled` | | `boolean` to conditionally enable/disable plugin. Default: true. |
|
||||
| `enabled` | `boolean` | To conditionally enable/disable plugin. Default: `true`. |
|
||||
|
||||
## Collection-specific options
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -2,12 +2,13 @@ import type { DeleteOne, Document, PayloadRequest } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, req = {} as PayloadRequest, where },
|
||||
{ collection, req = {} as PayloadRequest, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options = await withSession(this, req)
|
||||
@@ -17,7 +18,14 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, options).lean()
|
||||
const doc = await Model.findOneAndDelete(query, {
|
||||
...options,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
select,
|
||||
}),
|
||||
}).lean()
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
@@ -21,6 +22,7 @@ export const find: Find = async function find(
|
||||
pagination,
|
||||
projection,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
sort: sortArg,
|
||||
where,
|
||||
},
|
||||
@@ -67,6 +69,14 @@ export const find: Find = async function find(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (select) {
|
||||
paginationOptions.projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: collectionConfig.fields,
|
||||
select,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
|
||||
@@ -4,17 +4,23 @@ import { combineQueries } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const findGlobal: FindGlobal = async function findGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, locale, req = {} as PayloadRequest, where },
|
||||
{ slug, locale, req = {} as PayloadRequest, select, where },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
select: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.globals.config.find((each) => each.slug === slug).fields,
|
||||
select,
|
||||
}),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
@@ -18,6 +19,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where,
|
||||
@@ -69,6 +71,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }),
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { MongooseQueryOptions } from 'mongoose'
|
||||
import type { MongooseQueryOptions, QueryOptions } from 'mongoose'
|
||||
import type { Document, FindOne, PayloadRequest } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const findOne: FindOne = async function findOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, joins, locale, req = {} as PayloadRequest, where },
|
||||
{ collection, joins, locale, req = {} as PayloadRequest, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
@@ -24,6 +25,12 @@ export const findOne: FindOne = async function findOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: collectionConfig.fields,
|
||||
select,
|
||||
})
|
||||
|
||||
const aggregate = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
@@ -31,6 +38,7 @@ export const findOne: FindOne = async function findOne(
|
||||
joins,
|
||||
limit: 1,
|
||||
locale,
|
||||
projection,
|
||||
query,
|
||||
})
|
||||
|
||||
@@ -38,6 +46,7 @@ export const findOne: FindOne = async function findOne(
|
||||
if (aggregate) {
|
||||
;[doc] = await Model.aggregate(aggregate, options)
|
||||
} else {
|
||||
;(options as Record<string, unknown>).projection = projection
|
||||
doc = await Model.findOne(query, {}, options)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { PaginateOptions } from 'mongoose'
|
||||
import type { FindVersions, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
@@ -18,6 +19,7 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where,
|
||||
@@ -65,6 +67,11 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
|
||||
select,
|
||||
}),
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { PaginateOptions } from 'mongoose'
|
||||
import type { PayloadRequest, QueryDrafts } from 'payload'
|
||||
|
||||
import { combineQueries, flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
@@ -20,6 +21,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
sort: sortArg,
|
||||
where,
|
||||
},
|
||||
@@ -54,6 +56,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
where: combinedWhere,
|
||||
})
|
||||
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
|
||||
select,
|
||||
})
|
||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
||||
const useEstimatedCount =
|
||||
hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0
|
||||
@@ -64,6 +71,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
@@ -109,6 +117,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
projection,
|
||||
query: versionQuery,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
@@ -2,19 +2,23 @@ import type { PayloadRequest, UpdateGlobal } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, data, req = {} as PayloadRequest },
|
||||
{ slug, data, req = {} as PayloadRequest, select },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
|
||||
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
|
||||
}
|
||||
|
||||
let result
|
||||
@@ -22,7 +26,7 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields: this.payload.config.globals.find((global) => global.slug === slug).fields,
|
||||
fields,
|
||||
})
|
||||
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
@@ -17,16 +18,23 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
global: globalSlug,
|
||||
locale,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
versionData,
|
||||
where,
|
||||
}: UpdateGlobalVersionArgs<T>,
|
||||
) {
|
||||
const VersionModel = this.versions[globalSlug]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
const fields = buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
)
|
||||
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
|
||||
}
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
@@ -38,10 +46,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: versionData,
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
),
|
||||
fields,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest, UpdateOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
@@ -17,16 +18,19 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
where: whereArg,
|
||||
},
|
||||
) {
|
||||
const where = id ? { id: { equals: id } } : whereArg
|
||||
const Model = this.collections[collection]
|
||||
const fields = this.payload.collections[collection].config.fields
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
@@ -40,7 +44,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
fields,
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,19 +2,26 @@ import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion }
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
this: MongooseAdapter,
|
||||
{ id, collection, locale, req = {} as PayloadRequest, versionData, where },
|
||||
{ id, collection, locale, req = {} as PayloadRequest, select, versionData, where },
|
||||
) {
|
||||
const VersionModel = this.versions[collection]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
const fields = buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
)
|
||||
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
|
||||
}
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
@@ -26,10 +33,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: versionData,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
),
|
||||
fields,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { MongooseAdapter } from './index.js'
|
||||
|
||||
export const upsert: Upsert = async function upsert(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, locale, req = {} as PayloadRequest, where },
|
||||
{ collection, data, locale, req = {} as PayloadRequest, select, where },
|
||||
) {
|
||||
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, where })
|
||||
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, select, where })
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type BuildJoinAggregationArgs = {
|
||||
// the number of docs to get at the top collection level
|
||||
limit?: number
|
||||
locale: string
|
||||
projection?: Record<string, true>
|
||||
// the where clause for the top collection
|
||||
query?: Where
|
||||
/** whether the query is from drafts */
|
||||
@@ -26,6 +27,7 @@ export const buildJoinAggregation = async ({
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
projection,
|
||||
query,
|
||||
versions,
|
||||
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
|
||||
@@ -56,6 +58,10 @@ export const buildJoinAggregation = async ({
|
||||
for (const join of joinConfig[slug]) {
|
||||
const joinModel = adapter.collections[join.field.collection]
|
||||
|
||||
if (projection && !projection[join.schemaPath]) {
|
||||
continue
|
||||
}
|
||||
|
||||
const {
|
||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||
@@ -100,7 +106,7 @@ export const buildJoinAggregation = async ({
|
||||
$lookup: {
|
||||
as: `${as}.docs`,
|
||||
foreignField: `${join.field.on}${code}`,
|
||||
from: slug,
|
||||
from: adapter.collections[slug].collection.name,
|
||||
localField: versions ? 'parent' : '_id',
|
||||
pipeline,
|
||||
},
|
||||
@@ -141,7 +147,7 @@ export const buildJoinAggregation = async ({
|
||||
$lookup: {
|
||||
as: `${as}.docs`,
|
||||
foreignField: `${join.field.on}${localeSuffix}`,
|
||||
from: slug,
|
||||
from: adapter.collections[slug].collection.name,
|
||||
localField: versions ? 'parent' : '_id',
|
||||
pipeline,
|
||||
},
|
||||
@@ -174,5 +180,9 @@ export const buildJoinAggregation = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (projection) {
|
||||
aggregate.push({ $project: projection })
|
||||
}
|
||||
|
||||
return aggregate
|
||||
}
|
||||
|
||||
234
packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts
Normal file
234
packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import {
|
||||
deepCopyObjectSimple,
|
||||
type Field,
|
||||
type FieldAffectingData,
|
||||
type SelectMode,
|
||||
type SelectType,
|
||||
type TabAsField,
|
||||
} from 'payload'
|
||||
import { fieldAffectsData, getSelectMode } from 'payload/shared'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
const addFieldToProjection = ({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
field,
|
||||
projection,
|
||||
withinLocalizedField,
|
||||
}: {
|
||||
adapter: MongooseAdapter
|
||||
databaseSchemaPath: string
|
||||
field: FieldAffectingData
|
||||
projection: Record<string, true>
|
||||
withinLocalizedField: boolean
|
||||
}) => {
|
||||
const { config } = adapter.payload
|
||||
|
||||
if (withinLocalizedField && config.localization) {
|
||||
for (const locale of config.localization.localeCodes) {
|
||||
const localeDatabaseSchemaPath = databaseSchemaPath.replace('<locale>', locale)
|
||||
projection[`${localeDatabaseSchemaPath}${field.name}`] = true
|
||||
}
|
||||
} else {
|
||||
projection[`${databaseSchemaPath}${field.name}`] = true
|
||||
}
|
||||
}
|
||||
|
||||
const traverseFields = ({
|
||||
adapter,
|
||||
databaseSchemaPath = '',
|
||||
fields,
|
||||
projection,
|
||||
select,
|
||||
selectAllOnCurrentLevel = false,
|
||||
selectMode,
|
||||
withinLocalizedField = false,
|
||||
}: {
|
||||
adapter: MongooseAdapter
|
||||
databaseSchemaPath?: string
|
||||
fields: (Field | TabAsField)[]
|
||||
projection: Record<string, true>
|
||||
select: SelectType
|
||||
selectAllOnCurrentLevel?: boolean
|
||||
selectMode: SelectMode
|
||||
withinLocalizedField?: boolean
|
||||
}) => {
|
||||
for (const field of fields) {
|
||||
if (fieldAffectsData(field)) {
|
||||
if (selectMode === 'include') {
|
||||
if (select[field.name] === true || selectAllOnCurrentLevel) {
|
||||
addFieldToProjection({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
field,
|
||||
projection,
|
||||
withinLocalizedField,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (!select[field.name]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (selectMode === 'exclude') {
|
||||
if (typeof select[field.name] === 'undefined') {
|
||||
addFieldToProjection({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
field,
|
||||
projection,
|
||||
withinLocalizedField,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (select[field.name] === false) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fieldDatabaseSchemaPath = databaseSchemaPath
|
||||
let fieldWithinLocalizedField = withinLocalizedField
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
fieldDatabaseSchemaPath = `${databaseSchemaPath}${field.name}.`
|
||||
|
||||
if (field.localized) {
|
||||
fieldDatabaseSchemaPath = `${fieldDatabaseSchemaPath}<locale>.`
|
||||
fieldWithinLocalizedField = true
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'collapsible':
|
||||
case 'row':
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
fields: field.fields,
|
||||
projection,
|
||||
select,
|
||||
selectMode,
|
||||
withinLocalizedField,
|
||||
})
|
||||
break
|
||||
|
||||
case 'tabs':
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
projection,
|
||||
select,
|
||||
selectMode,
|
||||
withinLocalizedField,
|
||||
})
|
||||
break
|
||||
|
||||
case 'group':
|
||||
case 'tab':
|
||||
case 'array':
|
||||
if (field.type === 'array' && selectMode === 'include') {
|
||||
select[field.name]['id'] = true
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath: fieldDatabaseSchemaPath,
|
||||
fields: field.fields,
|
||||
projection,
|
||||
select: select[field.name] as SelectType,
|
||||
selectMode,
|
||||
withinLocalizedField: fieldWithinLocalizedField,
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'blocks': {
|
||||
const blocksSelect = select[field.name] as SelectType
|
||||
|
||||
for (const block of field.blocks) {
|
||||
if (
|
||||
(selectMode === 'include' && blocksSelect[block.slug] === true) ||
|
||||
(selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined')
|
||||
) {
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath: fieldDatabaseSchemaPath,
|
||||
fields: block.fields,
|
||||
projection,
|
||||
select: {},
|
||||
selectAllOnCurrentLevel: true,
|
||||
selectMode: 'include',
|
||||
withinLocalizedField: fieldWithinLocalizedField,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
let blockSelectMode = selectMode
|
||||
|
||||
if (selectMode === 'exclude' && blocksSelect[block.slug] === false) {
|
||||
blockSelectMode = 'include'
|
||||
}
|
||||
|
||||
if (typeof blocksSelect[block.slug] !== 'object') {
|
||||
blocksSelect[block.slug] = {}
|
||||
}
|
||||
|
||||
if (blockSelectMode === 'include') {
|
||||
blocksSelect[block.slug]['id'] = true
|
||||
blocksSelect[block.slug]['blockType'] = true
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath: fieldDatabaseSchemaPath,
|
||||
fields: block.fields,
|
||||
projection,
|
||||
select: blocksSelect[block.slug] as SelectType,
|
||||
selectMode: blockSelectMode,
|
||||
withinLocalizedField: fieldWithinLocalizedField,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const buildProjectionFromSelect = ({
|
||||
adapter,
|
||||
fields,
|
||||
select,
|
||||
}: {
|
||||
adapter: MongooseAdapter
|
||||
fields: Field[]
|
||||
select?: SelectType
|
||||
}): Record<string, true> | undefined => {
|
||||
if (!select) {
|
||||
return
|
||||
}
|
||||
|
||||
const projection: Record<string, true> = {
|
||||
_id: true,
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
fields,
|
||||
projection,
|
||||
// Clone to safely mutate it later
|
||||
select: deepCopyObjectSimple(select),
|
||||
selectMode: getSelectMode(select),
|
||||
})
|
||||
|
||||
return projection
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"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.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { upsertRow } from './upsertRow/index.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, data, req },
|
||||
{ collection: collectionSlug, data, req, select },
|
||||
) {
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -22,6 +22,7 @@ export const create: Create = async function create(
|
||||
fields: collection.fields,
|
||||
operation: 'create',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
globalSlug,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
@@ -41,6 +42,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
fields: buildVersionGlobalFields(this.payload.config, global),
|
||||
operation: 'create',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
parent,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
@@ -51,6 +52,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
fields: buildVersionCollectionFields(this.payload.config, collection),
|
||||
operation: 'create',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ import { transform } from './transform/read/index.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, joins: joinQuery, req = {} as PayloadRequest, where: whereArg },
|
||||
{
|
||||
collection: collectionSlug,
|
||||
joins: joinQuery,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
where: whereArg,
|
||||
},
|
||||
) {
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -49,6 +55,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
depth: 0,
|
||||
fields: collection.fields,
|
||||
joinQuery,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export const find: Find = async function find(
|
||||
page = 1,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
sort: sortArg,
|
||||
where,
|
||||
},
|
||||
@@ -34,6 +35,7 @@ export const find: Find = async function find(
|
||||
page,
|
||||
pagination,
|
||||
req,
|
||||
select,
|
||||
sort,
|
||||
tableName,
|
||||
where,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { DBQueryConfig } from 'drizzle-orm'
|
||||
import type { Field, JoinQuery } from 'payload'
|
||||
import type { Field, JoinQuery, SelectType } from 'payload'
|
||||
|
||||
import { getSelectMode } from 'payload/shared'
|
||||
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
|
||||
|
||||
@@ -15,6 +17,7 @@ type BuildFindQueryArgs = {
|
||||
*/
|
||||
joins?: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
select?: SelectType
|
||||
tableName: string
|
||||
versions?: boolean
|
||||
}
|
||||
@@ -34,6 +37,7 @@ export const buildFindManyArgs = ({
|
||||
joinQuery,
|
||||
joins = [],
|
||||
locale,
|
||||
select,
|
||||
tableName,
|
||||
versions,
|
||||
}: BuildFindQueryArgs): Record<string, unknown> => {
|
||||
@@ -42,48 +46,30 @@ export const buildFindManyArgs = ({
|
||||
with: {},
|
||||
}
|
||||
|
||||
if (select) {
|
||||
result.columns = {
|
||||
id: true,
|
||||
}
|
||||
}
|
||||
|
||||
const _locales: Result = {
|
||||
columns: {
|
||||
id: false,
|
||||
_parentID: false,
|
||||
},
|
||||
columns: select
|
||||
? { _locale: true }
|
||||
: {
|
||||
id: false,
|
||||
_parentID: false,
|
||||
},
|
||||
extras: {},
|
||||
with: {},
|
||||
}
|
||||
|
||||
if (adapter.tables[`${tableName}_texts`]) {
|
||||
result.with._texts = {
|
||||
columns: {
|
||||
id: false,
|
||||
parent: false,
|
||||
},
|
||||
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.tables[`${tableName}_numbers`]) {
|
||||
result.with._numbers = {
|
||||
columns: {
|
||||
id: false,
|
||||
parent: false,
|
||||
},
|
||||
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.tables[`${tableName}${adapter.relationshipsSuffix}`]) {
|
||||
result.with._rels = {
|
||||
columns: {
|
||||
id: false,
|
||||
parent: false,
|
||||
},
|
||||
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
|
||||
result.with._locales = _locales
|
||||
}
|
||||
const withTabledFields = select
|
||||
? {}
|
||||
: {
|
||||
numbers: true,
|
||||
rels: true,
|
||||
texts: true,
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
_locales,
|
||||
@@ -96,11 +82,51 @@ export const buildFindManyArgs = ({
|
||||
joins,
|
||||
locale,
|
||||
path: '',
|
||||
select,
|
||||
selectMode: select ? getSelectMode(select) : undefined,
|
||||
tablePath: '',
|
||||
topLevelArgs: result,
|
||||
topLevelTableName: tableName,
|
||||
versions,
|
||||
withTabledFields,
|
||||
})
|
||||
|
||||
if (adapter.tables[`${tableName}_texts`] && withTabledFields.texts) {
|
||||
result.with._texts = {
|
||||
columns: {
|
||||
id: false,
|
||||
parent: false,
|
||||
},
|
||||
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.tables[`${tableName}_numbers`] && withTabledFields.numbers) {
|
||||
result.with._numbers = {
|
||||
columns: {
|
||||
id: false,
|
||||
parent: false,
|
||||
},
|
||||
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.tables[`${tableName}${adapter.relationshipsSuffix}`] && withTabledFields.rels) {
|
||||
result.with._rels = {
|
||||
columns: {
|
||||
id: false,
|
||||
parent: false,
|
||||
},
|
||||
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
adapter.tables[`${tableName}${adapter.localesSuffix}`] &&
|
||||
(!select || Object.keys(_locales.columns).length > 1)
|
||||
) {
|
||||
result.with._locales = _locales
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export const findMany = async function find({
|
||||
page = 1,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
skip,
|
||||
sort,
|
||||
tableName,
|
||||
@@ -72,6 +73,7 @@ export const findMany = async function find({
|
||||
fields,
|
||||
joinQuery,
|
||||
joins,
|
||||
select,
|
||||
tableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { Field, JoinQuery } from 'payload'
|
||||
import type { Field, JoinQuery, SelectMode, SelectType } from 'payload'
|
||||
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared'
|
||||
@@ -22,10 +22,19 @@ type TraverseFieldArgs = {
|
||||
joins?: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
path: string
|
||||
select?: SelectType
|
||||
selectAllOnCurrentLevel?: boolean
|
||||
selectMode?: SelectMode
|
||||
tablePath: string
|
||||
topLevelArgs: Record<string, unknown>
|
||||
topLevelTableName: string
|
||||
versions?: boolean
|
||||
withinLocalizedField?: boolean
|
||||
withTabledFields: {
|
||||
numbers?: boolean
|
||||
rels?: boolean
|
||||
texts?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
@@ -39,10 +48,15 @@ export const traverseFields = ({
|
||||
joins,
|
||||
locale,
|
||||
path,
|
||||
select,
|
||||
selectAllOnCurrentLevel = false,
|
||||
selectMode,
|
||||
tablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
versions,
|
||||
withinLocalizedField = false,
|
||||
withTabledFields,
|
||||
}: TraverseFieldArgs) => {
|
||||
fields.forEach((field) => {
|
||||
if (fieldIsVirtual(field)) {
|
||||
@@ -74,9 +88,12 @@ export const traverseFields = ({
|
||||
joinQuery,
|
||||
joins,
|
||||
path,
|
||||
select,
|
||||
selectMode,
|
||||
tablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
withTabledFields,
|
||||
})
|
||||
|
||||
return
|
||||
@@ -87,6 +104,20 @@ export const traverseFields = ({
|
||||
const tabPath = tabHasName(tab) ? `${path}${tab.name}_` : path
|
||||
const tabTablePath = tabHasName(tab) ? `${tablePath}${toSnakeCase(tab.name)}_` : tablePath
|
||||
|
||||
const tabSelect = tabHasName(tab) ? select?.[tab.name] : select
|
||||
|
||||
if (tabSelect === false) {
|
||||
return
|
||||
}
|
||||
|
||||
let tabSelectAllOnCurrentLevel = selectAllOnCurrentLevel
|
||||
|
||||
if (tabHasName(tab) && select && !tabSelectAllOnCurrentLevel) {
|
||||
tabSelectAllOnCurrentLevel =
|
||||
select[tab.name] === true ||
|
||||
(selectMode === 'exclude' && typeof select[tab.name] === 'undefined')
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
_locales,
|
||||
adapter,
|
||||
@@ -97,10 +128,14 @@ export const traverseFields = ({
|
||||
joinQuery,
|
||||
joins,
|
||||
path: tabPath,
|
||||
select: typeof tabSelect === 'object' ? tabSelect : undefined,
|
||||
selectAllOnCurrentLevel: tabSelectAllOnCurrentLevel,
|
||||
selectMode,
|
||||
tablePath: tabTablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
versions,
|
||||
withTabledFields,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -110,10 +145,27 @@ export const traverseFields = ({
|
||||
if (fieldAffectsData(field)) {
|
||||
switch (field.type) {
|
||||
case 'array': {
|
||||
const arraySelect = selectAllOnCurrentLevel ? true : select?.[field.name]
|
||||
|
||||
if (select) {
|
||||
if (
|
||||
(selectMode === 'include' && typeof arraySelect === 'undefined') ||
|
||||
(selectMode === 'exclude' && arraySelect === false)
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const withArray: Result = {
|
||||
columns: {
|
||||
_parentID: false,
|
||||
},
|
||||
columns:
|
||||
typeof arraySelect === 'object'
|
||||
? {
|
||||
id: true,
|
||||
_order: true,
|
||||
}
|
||||
: {
|
||||
_parentID: false,
|
||||
},
|
||||
orderBy: ({ _order }, { asc }) => [asc(_order)],
|
||||
with: {},
|
||||
}
|
||||
@@ -122,17 +174,33 @@ export const traverseFields = ({
|
||||
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
if (typeof arraySelect === 'object') {
|
||||
if (adapter.tables[arrayTableName]._locale) {
|
||||
withArray.columns._locale = true
|
||||
}
|
||||
|
||||
if (adapter.tables[arrayTableName]._uuid) {
|
||||
withArray.columns._uuid = true
|
||||
}
|
||||
}
|
||||
|
||||
const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}`
|
||||
|
||||
if (adapter.tables[arrayTableNameWithLocales]) {
|
||||
withArray.with._locales = {
|
||||
columns: {
|
||||
id: false,
|
||||
_parentID: false,
|
||||
},
|
||||
columns:
|
||||
typeof arraySelect === 'object'
|
||||
? {
|
||||
_locale: true,
|
||||
}
|
||||
: {
|
||||
id: false,
|
||||
_parentID: false,
|
||||
},
|
||||
with: {},
|
||||
}
|
||||
}
|
||||
|
||||
currentArgs.with[`${path}${field.name}`] = withArray
|
||||
|
||||
traverseFields({
|
||||
@@ -144,16 +212,37 @@ export const traverseFields = ({
|
||||
fields: field.fields,
|
||||
joinQuery,
|
||||
path: '',
|
||||
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
||||
selectMode,
|
||||
tablePath: '',
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
withinLocalizedField: withinLocalizedField || field.localized,
|
||||
withTabledFields,
|
||||
})
|
||||
|
||||
if (
|
||||
typeof arraySelect === 'object' &&
|
||||
withArray.with._locales &&
|
||||
Object.keys(withArray.with._locales).length === 1
|
||||
) {
|
||||
delete withArray.with._locales
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'select': {
|
||||
if (field.hasMany) {
|
||||
if (select) {
|
||||
if (
|
||||
(selectMode === 'include' && !select[field.name]) ||
|
||||
(selectMode === 'exclude' && select[field.name] === false)
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const withSelect: Result = {
|
||||
columns: {
|
||||
id: false,
|
||||
@@ -169,15 +258,55 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks':
|
||||
case 'blocks': {
|
||||
const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name]
|
||||
|
||||
if (select) {
|
||||
if (
|
||||
(selectMode === 'include' && !blocksSelect) ||
|
||||
(selectMode === 'exclude' && blocksSelect === false)
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
const blockKey = `_blocks_${block.slug}`
|
||||
|
||||
let blockSelect: boolean | SelectType | undefined
|
||||
|
||||
let blockSelectMode = selectMode
|
||||
|
||||
if (selectMode === 'include' && blocksSelect === true) {
|
||||
blockSelect = true
|
||||
}
|
||||
|
||||
if (typeof blocksSelect === 'object') {
|
||||
if (typeof blocksSelect[block.slug] === 'object') {
|
||||
blockSelect = blocksSelect[block.slug]
|
||||
} else if (
|
||||
(selectMode === 'include' && typeof blocksSelect[block.slug] === 'undefined') ||
|
||||
(selectMode === 'exclude' && blocksSelect[block.slug] === false)
|
||||
) {
|
||||
blockSelect = {}
|
||||
blockSelectMode = 'include'
|
||||
} else if (selectMode === 'include' && blocksSelect[block.slug] === true) {
|
||||
blockSelect = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!topLevelArgs[blockKey]) {
|
||||
const withBlock: Result = {
|
||||
columns: {
|
||||
_parentID: false,
|
||||
},
|
||||
columns:
|
||||
typeof blockSelect === 'object'
|
||||
? {
|
||||
id: true,
|
||||
_order: true,
|
||||
_path: true,
|
||||
}
|
||||
: {
|
||||
_parentID: false,
|
||||
},
|
||||
orderBy: ({ _order }, { asc }) => [asc(_order)],
|
||||
with: {},
|
||||
}
|
||||
@@ -186,10 +315,26 @@ export const traverseFields = ({
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
|
||||
if (typeof blockSelect === 'object') {
|
||||
if (adapter.tables[tableName]._locale) {
|
||||
withBlock.columns._locale = true
|
||||
}
|
||||
|
||||
if (adapter.tables[tableName]._uuid) {
|
||||
withBlock.columns._uuid = true
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
|
||||
withBlock.with._locales = {
|
||||
with: {},
|
||||
}
|
||||
|
||||
if (typeof blockSelect === 'object') {
|
||||
withBlock.with._locales.columns = {
|
||||
_locale: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
topLevelArgs.with[blockKey] = withBlock
|
||||
|
||||
@@ -202,16 +347,35 @@ export const traverseFields = ({
|
||||
fields: block.fields,
|
||||
joinQuery,
|
||||
path: '',
|
||||
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
||||
selectMode: blockSelectMode,
|
||||
tablePath: '',
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
withinLocalizedField: withinLocalizedField || field.localized,
|
||||
withTabledFields,
|
||||
})
|
||||
|
||||
if (
|
||||
typeof blockSelect === 'object' &&
|
||||
withBlock.with._locales &&
|
||||
Object.keys(withBlock.with._locales.columns).length === 1
|
||||
) {
|
||||
delete withBlock.with._locales
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
if (groupSelect === false) {
|
||||
break
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
_locales,
|
||||
adapter,
|
||||
@@ -222,10 +386,18 @@ export const traverseFields = ({
|
||||
joinQuery,
|
||||
joins,
|
||||
path: `${path}${field.name}_`,
|
||||
select: typeof groupSelect === 'object' ? groupSelect : undefined,
|
||||
selectAllOnCurrentLevel:
|
||||
selectAllOnCurrentLevel ||
|
||||
groupSelect === true ||
|
||||
(selectMode === 'exclude' && typeof groupSelect === 'undefined'),
|
||||
selectMode,
|
||||
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
versions,
|
||||
withinLocalizedField: withinLocalizedField || field.localized,
|
||||
withTabledFields,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -237,6 +409,13 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
(select && selectMode === 'include' && !select[field.name]) ||
|
||||
(selectMode === 'exclude' && select[field.name] === false)
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
const {
|
||||
limit: limitArg = field.defaultLimit ?? 10,
|
||||
sort = field.defaultSort,
|
||||
@@ -410,6 +589,40 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
default: {
|
||||
if (!select && !selectAllOnCurrentLevel) {
|
||||
break
|
||||
}
|
||||
|
||||
if (
|
||||
selectAllOnCurrentLevel ||
|
||||
(selectMode === 'include' && select[field.name] === true) ||
|
||||
(selectMode === 'exclude' && typeof select[field.name] === 'undefined')
|
||||
) {
|
||||
const fieldPath = `${path}${field.name}`
|
||||
|
||||
if ((field.localized || withinLocalizedField) && _locales) {
|
||||
_locales.columns[fieldPath] = true
|
||||
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
|
||||
currentArgs.columns[fieldPath] = true
|
||||
}
|
||||
|
||||
if (
|
||||
!withTabledFields.rels &&
|
||||
field.type === 'relationship' &&
|
||||
(field.hasMany || Array.isArray(field.relationTo))
|
||||
) {
|
||||
withTabledFields.rels = true
|
||||
}
|
||||
|
||||
if (!withTabledFields.numbers && field.type === 'number' && field.hasMany) {
|
||||
withTabledFields.numbers = true
|
||||
}
|
||||
|
||||
if (!withTabledFields.texts && field.type === 'text' && field.hasMany) {
|
||||
withTabledFields.texts = true
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { findMany } from './find/findMany.js'
|
||||
|
||||
export const findGlobal: FindGlobal = async function findGlobal(
|
||||
this: DrizzleAdapter,
|
||||
{ slug, locale, req, where },
|
||||
{ slug, locale, req, select, where },
|
||||
) {
|
||||
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
|
||||
|
||||
@@ -23,6 +23,7 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
||||
locale,
|
||||
pagination: false,
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where,
|
||||
@@ -40,6 +41,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
page,
|
||||
pagination,
|
||||
req,
|
||||
select,
|
||||
skip,
|
||||
sort,
|
||||
tableName,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { findMany } from './find/findMany.js'
|
||||
|
||||
export async function findOne<T extends TypeWithID>(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, joins, locale, req = {} as PayloadRequest, where }: FindOneArgs,
|
||||
{ collection, joins, locale, req = {} as PayloadRequest, select, where }: FindOneArgs,
|
||||
): Promise<T> {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
@@ -23,6 +23,7 @@ export async function findOne<T extends TypeWithID>(
|
||||
page: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
select,
|
||||
sort: undefined,
|
||||
tableName,
|
||||
where,
|
||||
|
||||
@@ -16,6 +16,7 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where,
|
||||
@@ -38,6 +39,7 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
page,
|
||||
pagination,
|
||||
req,
|
||||
select,
|
||||
skip,
|
||||
sort,
|
||||
tableName,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { JoinQuery, PayloadRequest, QueryDrafts, SanitizedCollectionConfig } from 'payload'
|
||||
import type { PayloadRequest, QueryDrafts, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { buildVersionCollectionFields, combineQueries } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
@@ -17,6 +17,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
page = 1,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
sort,
|
||||
where,
|
||||
},
|
||||
@@ -38,6 +39,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
page,
|
||||
pagination,
|
||||
req,
|
||||
select,
|
||||
sort,
|
||||
tableName,
|
||||
versions: true,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { upsertRow } from './upsertRow/index.js'
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
this: DrizzleAdapter,
|
||||
{ id, collection: collectionSlug, data, draft, joins: joinQuery, locale, req, where: whereArg },
|
||||
{ id, collection: collectionSlug, data, joins: joinQuery, locale, req, select, where: whereArg },
|
||||
) {
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -49,6 +49,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
joinQuery,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { upsertRow } from './upsertRow/index.js'
|
||||
|
||||
export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
this: DrizzleAdapter,
|
||||
{ slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs,
|
||||
{ slug, data, req = {} as PayloadRequest, select }: UpdateGlobalArgs,
|
||||
): Promise<T> {
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
|
||||
@@ -23,6 +23,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
db,
|
||||
fields: globalConfig.fields,
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
global,
|
||||
locale,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
versionData,
|
||||
where: whereArg,
|
||||
}: UpdateGlobalVersionArgs<T>,
|
||||
@@ -53,6 +54,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
fields,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function updateVersion<T extends TypeWithID>(
|
||||
collection,
|
||||
locale,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
versionData,
|
||||
where: whereArg,
|
||||
}: UpdateVersionArgs<T>,
|
||||
@@ -50,6 +51,7 @@ export async function updateVersion<T extends TypeWithID>(
|
||||
fields,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
operation,
|
||||
path = '',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
upsertTarget,
|
||||
where,
|
||||
@@ -415,6 +416,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
depth: 0,
|
||||
fields,
|
||||
joinQuery,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { Field, JoinQuery, PayloadRequest } from 'payload'
|
||||
import type { Field, JoinQuery, PayloadRequest, SelectType } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter, DrizzleTransaction, GenericColumn } from '../types.js'
|
||||
|
||||
@@ -23,6 +23,7 @@ type CreateArgs = {
|
||||
id?: never
|
||||
joinQuery?: never
|
||||
operation: 'create'
|
||||
select?: SelectType
|
||||
upsertTarget?: never
|
||||
where?: never
|
||||
} & BaseArgs
|
||||
@@ -31,6 +32,7 @@ type UpdateArgs = {
|
||||
id?: number | string
|
||||
joinQuery?: JoinQuery
|
||||
operation: 'update'
|
||||
select?: SelectType
|
||||
upsertTarget?: GenericColumn
|
||||
where?: SQL<unknown>
|
||||
} & BaseArgs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { DataFromGlobalSlug, GlobalSlug, PayloadRequest, SanitizedGlobalConfig } from 'payload'
|
||||
import type {
|
||||
DataFromGlobalSlug,
|
||||
GlobalSlug,
|
||||
PayloadRequest,
|
||||
SanitizedGlobalConfig,
|
||||
SelectType,
|
||||
} from 'payload'
|
||||
import type { DeepPartial } from 'ts-essentials'
|
||||
|
||||
import { isolateObjectProperty, updateOperationGlobal } from 'payload'
|
||||
@@ -40,7 +46,7 @@ export function update<TSlug extends GlobalSlug>(
|
||||
req: isolateObjectProperty(context.req, 'transactionID'),
|
||||
}
|
||||
|
||||
const result = await updateOperationGlobal<TSlug>(options)
|
||||
const result = await updateOperationGlobal<TSlug, SelectType>(options)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +124,10 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
parentName: singularName,
|
||||
})
|
||||
|
||||
const mutationInputFields = [...fields]
|
||||
|
||||
if (collectionConfig.auth && !collectionConfig.auth.disableLocalStrategy) {
|
||||
fields.push({
|
||||
mutationInputFields.push({
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
label: 'Password',
|
||||
@@ -136,7 +138,7 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
const createMutationInputType = buildMutationInputType({
|
||||
name: singularName,
|
||||
config,
|
||||
fields,
|
||||
fields: mutationInputFields,
|
||||
graphqlResult,
|
||||
parentName: singularName,
|
||||
})
|
||||
@@ -147,7 +149,9 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
const updateMutationInputType = buildMutationInputType({
|
||||
name: `${singularName}Update`,
|
||||
config,
|
||||
fields: fields.filter((field) => !(fieldAffectsData(field) && field.name === 'id')),
|
||||
fields: mutationInputFields.filter(
|
||||
(field) => !(fieldAffectsData(field) && field.name === 'id'),
|
||||
),
|
||||
forceNullable: true,
|
||||
graphqlResult,
|
||||
parentName: `${singularName}Update`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const create: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { searchParams } = req
|
||||
@@ -20,6 +21,7 @@ export const create: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
depth: isNumber(depth) ? depth : undefined,
|
||||
draft,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
return Response.json(
|
||||
|
||||
@@ -8,11 +8,13 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, overrideLock, where } = req.query as {
|
||||
const { depth, overrideLock, select, where } = req.query as {
|
||||
depth?: string
|
||||
overrideLock?: string
|
||||
select?: Record<string, unknown>
|
||||
where?: Where
|
||||
}
|
||||
|
||||
@@ -21,6 +23,7 @@ export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) =>
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const deleteByID: CollectionRouteHandlerWithID = async ({
|
||||
id: incomingID,
|
||||
@@ -28,6 +29,7 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({
|
||||
depth: isNumber(depth) ? depth : undefined,
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
const headers = headersWithCors({
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const duplicate: CollectionRouteHandlerWithID = async ({
|
||||
id: incomingID,
|
||||
@@ -30,6 +31,7 @@ export const duplicate: CollectionRouteHandlerWithID = async ({
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
const message = req.t('general:successfullyDuplicated', {
|
||||
|
||||
@@ -8,14 +8,16 @@ import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, draft, joins, limit, page, sort, where } = req.query as {
|
||||
const { depth, draft, joins, limit, page, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
draft?: string
|
||||
joins?: JoinQuery
|
||||
limit?: string
|
||||
page?: string
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
}
|
||||
@@ -28,6 +30,7 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findByID: CollectionRouteHandlerWithID = async ({
|
||||
id: incomingID,
|
||||
@@ -31,6 +32,7 @@ export const findByID: CollectionRouteHandlerWithID = async ({
|
||||
draft: searchParams.get('draft') === 'true',
|
||||
joins: sanitizeJoinParams(req.query.joins as JoinQuery),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
return Response.json(result, {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersionByID: CollectionRouteHandlerWithID = async ({
|
||||
id: incomingID,
|
||||
@@ -26,6 +27,7 @@ export const findVersionByID: CollectionRouteHandlerWithID = async ({
|
||||
collection,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
return Response.json(result, {
|
||||
|
||||
@@ -7,12 +7,14 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersions: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, limit, page, sort, where } = req.query as {
|
||||
const { depth, limit, page, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
limit?: string
|
||||
page?: string
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
}
|
||||
@@ -23,6 +25,7 @@ export const findVersions: CollectionRouteHandler = async ({ collection, req })
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -8,13 +8,15 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const update: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, draft, limit, overrideLock, where } = req.query as {
|
||||
const { depth, draft, limit, overrideLock, select, where } = req.query as {
|
||||
depth?: string
|
||||
draft?: string
|
||||
limit?: string
|
||||
overrideLock?: string
|
||||
select?: Record<string, unknown>
|
||||
where?: Where
|
||||
}
|
||||
|
||||
@@ -26,6 +28,7 @@ export const update: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
where,
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const updateByID: CollectionRouteHandlerWithID = async ({
|
||||
id: incomingID,
|
||||
@@ -35,6 +36,7 @@ export const updateByID: CollectionRouteHandlerWithID = async ({
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
publishSpecificLocale,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
let message = req.t('general:updatedSuccessfully')
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
const { searchParams } = req
|
||||
@@ -16,6 +17,7 @@ export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
draft: searchParams.get('draft') === 'true',
|
||||
globalConfig,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
return Response.json(result, {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConfig, req }) => {
|
||||
const { searchParams } = req
|
||||
@@ -15,6 +16,7 @@ export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConf
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
globalConfig,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
return Response.json(result, {
|
||||
|
||||
@@ -7,12 +7,14 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
const { depth, limit, page, sort, where } = req.query as {
|
||||
const { depth, limit, page, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
limit?: string
|
||||
page?: string
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
}
|
||||
@@ -23,6 +25,7 @@ export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) =>
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const update: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
const { searchParams } = req
|
||||
@@ -22,6 +23,7 @@ export const update: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
globalConfig,
|
||||
publishSpecificLocale,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
let message = req.t('general:updatedSuccessfully')
|
||||
|
||||
20
packages/next/src/routes/rest/utilities/sanitizeSelect.ts
Normal file
20
packages/next/src/routes/rest/utilities/sanitizeSelect.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { SelectType } from 'payload'
|
||||
|
||||
/**
|
||||
* Sanitizes REST select query to SelectType
|
||||
*/
|
||||
export const sanitizeSelect = (unsanitizedSelect: unknown): SelectType | undefined => {
|
||||
if (unsanitizedSelect && typeof unsanitizedSelect === 'object') {
|
||||
for (const k in unsanitizedSelect) {
|
||||
if (unsanitizedSelect[k] === 'true') {
|
||||
unsanitizedSelect[k] = true
|
||||
} else if (unsanitizedSelect[k] === 'false') {
|
||||
unsanitizedSelect[k] = false
|
||||
} else if (typeof unsanitizedSelect[k] === 'object') {
|
||||
sanitizeSelect(unsanitizedSelect[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unsanitizedSelect as SelectType
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { getPayloadHMR } from '../getPayloadHMR.js'
|
||||
import { initReq } from '../initReq.js'
|
||||
import { getRouteInfo } from './handleAdminPage.js'
|
||||
import { handleAuthRedirect } from './handleAuthRedirect.js'
|
||||
import { isCustomAdminView } from './isCustomAdminView.js'
|
||||
import { isPublicAdminRoute } from './shared.js'
|
||||
|
||||
export const initPage = async ({
|
||||
@@ -133,7 +134,8 @@ export const initPage = async ({
|
||||
|
||||
if (
|
||||
!permissions.canAccessAdmin &&
|
||||
!isPublicAdminRoute({ adminRoute, config: payload.config, route })
|
||||
!isPublicAdminRoute({ adminRoute, config: payload.config, route }) &&
|
||||
!isCustomAdminView({ adminRoute, config: payload.config, route })
|
||||
) {
|
||||
redirectTo = handleAuthRedirect({
|
||||
config: payload.config,
|
||||
|
||||
35
packages/next/src/utilities/initPage/isCustomAdminView.ts
Normal file
35
packages/next/src/utilities/initPage/isCustomAdminView.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { AdminViewConfig, PayloadRequest, SanitizedConfig } from 'payload'
|
||||
|
||||
import { getRouteWithoutAdmin } from './shared.js'
|
||||
|
||||
/**
|
||||
* Returns an array of views marked with 'public: true' in the config
|
||||
*/
|
||||
export const isCustomAdminView = ({
|
||||
adminRoute,
|
||||
config,
|
||||
route,
|
||||
}: {
|
||||
adminRoute: string
|
||||
config: SanitizedConfig
|
||||
route: string
|
||||
}): boolean => {
|
||||
if (config.admin?.components?.views) {
|
||||
const isPublicAdminRoute = Object.entries(config.admin.components.views).some(([_, view]) => {
|
||||
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
|
||||
|
||||
if (view.exact) {
|
||||
if (routeWithoutAdmin === view.path) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if (routeWithoutAdmin.startsWith(view.path)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
return isPublicAdminRoute
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -35,9 +35,10 @@ export const isPublicAdminRoute = ({
|
||||
config: SanitizedConfig
|
||||
route: string
|
||||
}): boolean => {
|
||||
return publicAdminRoutes.some((routeSegment) => {
|
||||
const isPublicAdminRoute = publicAdminRoutes.some((routeSegment) => {
|
||||
const segment = config.admin?.routes?.[routeSegment] || routeSegment
|
||||
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
|
||||
|
||||
if (routeWithoutAdmin.startsWith(segment)) {
|
||||
return true
|
||||
} else if (routeWithoutAdmin.includes('/verify/')) {
|
||||
@@ -46,6 +47,8 @@ export const isPublicAdminRoute = ({
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return isPublicAdminRoute
|
||||
}
|
||||
|
||||
export const getRouteWithoutAdmin = ({
|
||||
|
||||
@@ -170,6 +170,9 @@ export const getViewsFromConfig = ({
|
||||
DefaultView = {
|
||||
Component: DefaultLivePreviewView,
|
||||
}
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -314,6 +317,9 @@ export const getViewsFromConfig = ({
|
||||
DefaultView = {
|
||||
Component: DefaultLivePreviewView,
|
||||
}
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const APIKey: React.FC<{ readonly enabled: boolean; readonly readOnly?: b
|
||||
const [highlightedField, setHighlightedField] = useState(false)
|
||||
const { i18n, t } = useTranslation()
|
||||
const { config } = useConfig()
|
||||
const { collectionSlug, docPermissions } = useDocumentInfo()
|
||||
const { collectionSlug } = useDocumentInfo()
|
||||
|
||||
const apiKey = useFormFields(([fields]) => (fields && fields[path]) || null)
|
||||
|
||||
@@ -77,12 +77,6 @@ export const APIKey: React.FC<{ readonly enabled: boolean; readonly readOnly?: b
|
||||
[apiKeyLabel, apiKeyValue],
|
||||
)
|
||||
|
||||
const canUpdateAPIKey = useMemo(() => {
|
||||
if (docPermissions && docPermissions?.fields?.apiKey) {
|
||||
return docPermissions.fields.apiKey.update.permission
|
||||
}
|
||||
}, [docPermissions])
|
||||
|
||||
const fieldType = useField({
|
||||
path: 'apiKey',
|
||||
validate,
|
||||
@@ -142,7 +136,7 @@ export const APIKey: React.FC<{ readonly enabled: boolean; readonly readOnly?: b
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
</div>
|
||||
{!readOnly && canUpdateAPIKey && (
|
||||
{!readOnly && (
|
||||
<GenerateConfirmation highlightField={highlightField} setKey={() => setValue(uuidv4())} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -73,6 +73,12 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
return false
|
||||
}, [permissions, collectionSlug])
|
||||
|
||||
const apiKeyReadOnly = readOnly || !docPermissions?.fields?.apiKey?.update?.permission
|
||||
const enableAPIKeyReadOnly = readOnly || !docPermissions?.fields?.enableAPIKey?.update?.permission
|
||||
|
||||
const canReadApiKey = docPermissions?.fields?.apiKey?.read?.permission
|
||||
const canReadEnableAPIKey = docPermissions?.fields?.enableAPIKey?.read?.permission
|
||||
|
||||
const handleChangePassword = useCallback(
|
||||
(showPasswordFields: boolean) => {
|
||||
if (showPasswordFields) {
|
||||
@@ -200,14 +206,16 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{useAPIKey && (
|
||||
<div className={`${baseClass}__api-key`}>
|
||||
<CheckboxField
|
||||
field={{
|
||||
name: 'enableAPIKey',
|
||||
admin: { disabled, readOnly },
|
||||
label: t('authentication:enableAPIKey'),
|
||||
}}
|
||||
/>
|
||||
<APIKey enabled={!!enableAPIKey?.value} readOnly={readOnly} />
|
||||
{canReadEnableAPIKey && (
|
||||
<CheckboxField
|
||||
field={{
|
||||
name: 'enableAPIKey',
|
||||
admin: { disabled, readOnly: enableAPIKeyReadOnly },
|
||||
label: t('authentication:enableAPIKey'),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{canReadApiKey && <APIKey enabled={!!enableAPIKey?.value} readOnly={apiKeyReadOnly} />}
|
||||
</div>
|
||||
)}
|
||||
{verify && (
|
||||
|
||||
@@ -67,6 +67,10 @@ export const NotFoundPage = async ({
|
||||
|
||||
const params = await paramsPromise
|
||||
|
||||
if (!initPageResult.req.user || !initPageResult.permissions.canAccessAdmin) {
|
||||
return <NotFoundClient />
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaultTemplate
|
||||
i18n={initPageResult.req.i18n}
|
||||
|
||||
@@ -66,24 +66,29 @@ export const RootPage = async ({
|
||||
|
||||
let dbHasUser = false
|
||||
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const initPageResult = await initPage(initPageOptions)
|
||||
|
||||
dbHasUser = await initPageResult?.req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult?.req,
|
||||
})
|
||||
?.then((doc) => !!doc)
|
||||
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
if (initPageResult?.req?.user) {
|
||||
notFound()
|
||||
}
|
||||
if (dbHasUser) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof initPageResult?.redirectTo === 'string') {
|
||||
redirect(initPageResult.redirectTo)
|
||||
}
|
||||
|
||||
if (initPageResult) {
|
||||
dbHasUser = await initPageResult?.req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult?.req,
|
||||
})
|
||||
?.then((doc) => !!doc)
|
||||
|
||||
const createFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute })
|
||||
|
||||
const collectionConfig = config.collections.find(({ slug }) => slug === userSlug)
|
||||
@@ -102,6 +107,10 @@ export const RootPage = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent && !dbHasUser) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
const createMappedView = getCreateMappedComponent({
|
||||
importMap,
|
||||
serverProps: {
|
||||
|
||||
@@ -21,11 +21,11 @@ Add the plugin to your Payload config
|
||||
`yarn add @payloadcms/payload-cloud`
|
||||
|
||||
```ts
|
||||
import { payloadCloud } from '@payloadcms/payload-cloud'
|
||||
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
plugins: [payloadCloud()],
|
||||
plugins: [payloadCloudPlugin()],
|
||||
// rest of config
|
||||
})
|
||||
```
|
||||
@@ -41,7 +41,7 @@ After configuring, ensure that the `from` email address is from a domain you hav
|
||||
If you wish to opt-out of any Payload cloud features, the plugin also accepts options to do so.
|
||||
|
||||
```ts
|
||||
payloadCloud({
|
||||
payloadCloudPlugin({
|
||||
storage: false, // Disable file storage
|
||||
email: false, // Disable email delivery
|
||||
uploadCaching: false, // Disable upload caching
|
||||
@@ -53,7 +53,7 @@ payloadCloud({
|
||||
If you wish to configure upload caching on a per-collection basis, you can do so by passing in a keyed object of collection names. By default, all collections will be cached for 24 hours (86400 seconds). The cache is invalidated when an item is updated or deleted.
|
||||
|
||||
```ts
|
||||
payloadCloud({
|
||||
payloadCloudPlugin({
|
||||
uploadCaching: {
|
||||
maxAge: 604800, // Override default maxAge for all collections
|
||||
collection1Slug: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.120",
|
||||
"version": "3.0.0-beta.122",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
@@ -92,6 +92,7 @@
|
||||
"bson-objectid": "2.0.4",
|
||||
"ci-info": "^4.0.0",
|
||||
"console-table-printer": "2.11.2",
|
||||
"croner": "8.1.2",
|
||||
"dataloader": "2.2.2",
|
||||
"deepmerge": "4.3.1",
|
||||
"file-type": "19.3.0",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user