Compare commits

..

30 Commits

Author SHA1 Message Date
Elliot DeNolf
d192f1414d chore(release): v3.0.0-beta.122 [skip ci] 2024-10-30 21:02:15 -04:00
Said Akhrarov
755355ea68 fix(ui): description undefined error on empty tabs array (#8830)
Fixes an error that occurs when `tabs` array is empty or active tab
config is undefined due to missing optional chaining operator.
2024-10-30 20:57:54 -04:00
Manuel Leitold
58441c2bcc fix(graphql): avoid adding extra password fields when running mutations (#8032) (#8845)
<!--

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

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

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

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

### What?

### Why?

### How?

Fixes #

-->

### What?
`auth` enabled collections show "Password" fields whenever a GraphQL
query is performed or the GraphQL playground is opened (see #8032)

You can reproduce this behavior by spinning up the `admin` test with
PostgreSQL:
```bash
pnpm dev:postgres admin
```

Open the admin UI and navigate to the `dev@payloadcms.com` document in
the `Users` collection (see screenshot below)
<img width="915" alt="image"
src="https://github.com/user-attachments/assets/40624a8f-80b7-412b-b851-5e3643ffcae1">

Open the [GraphQL
playground](http://localhost:3000/api/graphql-playground)
Open the admin UI and select the user again. The password field appears
multiple times.
Subsequent GraphQL playground page refreshes lead to even more password
fields in the admin UI.

<img width="1086" alt="image"
src="https://github.com/user-attachments/assets/009264bd-b153-4bf7-8fc9-8e465fc27247">

The current behavior has an impact during development and even on
production. Since the password field is added to the collection, payload
tries to add this field to the database as well (at least I could
observe at in my own project)

### Why?
In the `packages/graphql/src/schema/initCollections.ts` file, the
`initCollections` function mutates the config object by adding the
password field for the GraphQL schema (line 128). This mutation adds the
field multiple times, depending how often you open the playground. In
addition, this added field is also shown in the UI since the config
object is shared (see screenshot above).

### How?
By creating a deep copy of the object, the mutation of the configuration
does not leak additional fields to the UI or other parts of the code.
2024-10-31 00:45:07 +00:00
Gregor Gabrič
3918c09013 feat(translations): added sl to exported date locales (#8817)
added sl to exported date locales
2024-10-30 18:26:12 -06:00
Konsequanzheng
bf989e6041 docs: fix copy paste oversights in storage-adapters.mdx (#8919)
### What?
S3, Azure Blob, and Google Cloud storage sections were referring to
Vercel Blob Storage (presumably because of copy pasting)
2024-10-30 18:15:25 -06:00
Paul
57fba36257 fix(ui): overly large width on stay logged in modal content (#8952) 2024-10-31 00:01:58 +00:00
Sasha
df4661a388 docs: fix defaultPopulate docs formatting (#8951)
### What?
Fixes this
[here](https://payloadcms.com/docs/beta/queries/select#rest-api)
<img width="535" alt="image"
src="https://github.com/user-attachments/assets/a9fec4a7-c1c2-43f3-ba36-a07505deb012">
2024-10-31 00:37:45 +02:00
James Mikrut
03e5ae8095 fix: bulk upload mimetype wildcard file selection (#8954)
Fixes an issue where using wildcards in upload-enabled collection
mimeType restrictions would prevent files from being selected in the
bulk upload file selector.
2024-10-30 16:26:36 -06:00
Elliot DeNolf
d89db00295 chore(release): v3.0.0-beta.121 [skip ci] 2024-10-30 14:25:34 -04:00
James Mikrut
8970c6b3a6 feat: adds jobs queue (#8228)
Adds a jobs queue to Payload.

- [x] Docs, w/ examples for Vercel Cron, additional services
- [x] Type the `job` using GeneratedTypes in `JobRunnerArgs`
(@AlessioGr)
- [x] Write the `runJobs` function 
- [x] Allow for some type of `payload.runTask` 
- [x] Open up a new bin script for running jobs
- [x] Determine strategy for runner endpoint to either await jobs
successfully or return early and stay open until job work completes
(serverless ramifications here)
- [x] Allow for job runner to accept how many jobs to run in one
invocation
- [x] Make a Payload local API method for creating a new job easily
(payload.createJob) or similar which is strongly typed (@AlessioGr)
- [x] Make `payload.runJobs` or similar  (@AlessioGr)
- [x] Write tests for retrying up to max retries for a given step
- [x] Write tests for dynamic import of a runner

The shape of the config should permit the definition of steps separate
from the job workflows themselves.

```js
const config = {
  // Not sure if we need this property anymore
  queues: {
  },
  // A job is an instance of a workflow, stored in DB
  // and triggered by something at some point
  jobs: {
    // Be able to override the jobs collection
    collectionOverrides: () => {},

    // Workflows are groups of tasks that handle
    // the flow from task to task.
    // When defined on the config, they are considered as predefined workflows
    // BUT - in the future, we'll allow for UI-based workflow definition as well.
    workflows: [
      {
        slug: 'job-name',
        // Temporary name for this
        // should be able to pass function 
        // or path to it for Node to dynamically import
        controlFlowInJS: '/my-runner.js',

        // Temporary name as well
        // should be able to eventually define workflows
        // in UI (meaning they need to be serialized in JSON)
        // Should not be able to define both control flows
        controlFlowInJSON: [
          {
            task: 'myTask',
            next: {
              // etc
            }
          }
        ],

        // Workflows take input
        // which are a group of fields
        input: [
          {
            name: 'post',
            type: 'relationship',
            relationTo: 'posts',
            maxDepth: 0,
            required: true,
          },
          {
            name: 'message',
            type: 'text',
            required: true,
          },
        ],
      },
    ],

    // Tasks are defined separately as isolated functions
    // that can be retried on fail
    tasks: [
      {
        slug: 'myTask',
        retries: 2,
        // Each task takes input
        // Used to auto-type the task func args
        input: [
          {
            name: 'post',
            type: 'relationship',
            relationTo: 'posts',
            maxDepth: 0,
            required: true,
          },
          {
            name: 'message',
            type: 'text',
            required: true,
          },
        ],
        // Each task takes output
        // Used to auto-type the function signature
        output: [
          {
            name: 'success',
            type: 'checkbox',
          }
        ],
        onSuccess: () => {},
        onFail: () => {},
        run: myRunner,
      },
    ]
  }
}
```

### `payload.createJob`

This function should allow for the creation of jobs based on either a
workflow (group of tasks) or an individual task.

To create a job using a workflow:

```js
const job = await payload.createJob({
  // Accept the `name` of a workflow so we can match to either a 
  // code-based workflow OR a workflow defined in the DB
  // Should auto-type the input
  workflowName: 'myWorkflow',
  input: {
    // typed to the args of the workflow by name
  }
})
```

To create a job using a task:

```js
const job = await payload.createJob({
  // Accept the `name` of a task
  task: 'myTask',
  input: {
    // typed to the args of the task by name
  }
})
```

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-10-30 17:56:50 +00:00
Said Akhrarov
0574155e59 docs: fix docs-wide spelling errors and formatting issues (#8942)
### What?
I noticed a spelling error in the banner of the beta docs and decided I
could save everyone some time by *running the entirety of the beta docs*
through a spellchecker.

### Why?
To fix many spelling and formatting mistakes at once.

### How?
By enabling `edit mode` in my browser and letting the built-in
spellchecker perform its magic (and changing _only_ where it made
sense).

~~Ironically, the original spelling mistake that inspired me to do this
remains unchanged as that is a part of the website repo. [PR for that is
here](https://github.com/payloadcms/website/pull/388).~~
2024-10-30 11:54:44 -06:00
Paul
03331de2ac fix(ui): perf improvements in bulk upload (#8944) 2024-10-30 13:44:09 -04:00
Sasha
d64946c2e2 fix(db-mongodb): ensure relationships are stored in ObjectID (#8932)
### What?
Since the join field, we do store relationship fields values in
`ObjectID`. This wasn't true if the field is nested to an array /
blocks.

### Why?
All relationship fields values should be stored in `ObjectID`.

### How?
Fixes arrays / blocks handling in the `traverseFields.ts` function.
Before it didn't run for them.
2024-10-30 13:42:07 -04:00
Sasha
c41ef65a2b feat: add defaultPopulate property to collection config (#8934)
### What?
Adds `defaultPopulate` property to collection config that allows to
specify which fields to select when the collection is populated from
another document.
```ts
import type { CollectionConfig } from 'payload'

// 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',
  // I need only slug, NOT the WHOLE CONTENT!
  defaultPopulate: {
    slug: true,
  },
  fields: [
    {
      name: 'slug',
      type: 'text',
      required: true,
    },
  ],
}
```

### Why?
This is essential for example in case of links. You don't need the whole
document, which can contain large data but only the `slug`.

### How?
Implements `defaultPopulate` when populating relationships, including
inside of lexical / slate rich text fields.
2024-10-30 13:41:34 -04:00
Paul
d38d7b8932 fix(ui): broken buttons in the bulk upload drawer (#8926)
Fixes the mobile bottom interface and the arrow buttons in the actions
at the top.

Before:

![image](https://github.com/user-attachments/assets/26902eb0-5d1a-480d-b6f5-c36a800a6bff)


After:

![image](https://github.com/user-attachments/assets/7837684c-37a7-4b2e-a875-47972cf1671f)
2024-10-30 11:29:58 -06:00
Paul
01ccbd48b0 feat!: custom views are now public by default and fixed some issues with notFound page (#8820)
This PR aims to fix a few issues with the notFound page and custom views
so it matches v2 behaviour:
- Non authorised users should always be redirected to the login page
regardless if not found or valid URL
- Previously notFound would render for non users too potentially
exposing valid but protected routes and creating a confusing workflow as
the UI was being rendered as well
- Custom views are now public by default
- in our `admin` test suite, the `/admin/public-custom-view` is
accessible to non users but
`/admin/public-custom-view/protected-nested-view` is not unless the
checkbox is true in the Settings global, there's e2e coverage for this
- Fixes https://github.com/payloadcms/payload/issues/8716
2024-10-30 11:29:29 -06:00
Kendell Joseph
61b4f2efd7 chore: updates payload cloud plugin docs (#8943)
Documentation updated to match current implementation.

Original Doc:
```ts
import { payloadCloud } from '@payloadcms/payload-cloud'
```

Current:
```ts
import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
```

---

References in docs have been updated.
2024-10-30 12:52:56 -04:00
Sasha
f4041ce6e2 fix(db-mongodb): joins with singular collection name (#8933)
### What?
Properly specifies `$lookup.from` when the collection name is singular.

### Why?
MongoDB can pluralize the collection name and so can be different for
singular ones.

### How?
Uses the collection name from the driver directly
`adapter.collections[slug].collection.name` instead of just `slug`.
2024-10-30 12:06:03 -04:00
James Mikrut
123125185c fix!: plugin-search with localization enabled (#8938)
The search plugin was incorrectly retrieving all locales, when it should
just be retrieving the locale of the parent document that was actively
being updated.

## BREAKING CHANGES:

If you have a localized Payload config, and you are using the `plugin-search`, we will now automatically localize the `title` field that is injected by the search plugin and this may lead to data loss. To opt out of this new behavior, you can pass `localize: false` to the plugin options.
2024-10-30 11:49:54 -04:00
Kendell Joseph
04bd502d37 chore: uses custom live preview component if one is provided (#8930)
Issue: https://github.com/payloadcms/payload/issues/8273
2024-10-30 11:37:01 -04:00
Sasha
dae832c288 feat: select fields (#8550)
Adds `select` which is used to specify the field projection for local
and rest API calls. This is available as an optimization to reduce the
payload's of requests and make the database queries more efficient.

Includes:
- [x] generate types for the `select` property
- [x] infer the return type by `select` with 2 modes - include (`field:
true`) and exclude (`field: false`)
- [x] lots of integration tests, including deep fields / localization
etc
- [x] implement the property in db adapters
- [x] implement the property in the local api for most operations
- [x] implement the property in the rest api 
- [x] docs

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-10-29 21:47:18 +00:00
Dan Ribbens
6cdf141380 feat: prevent create new for joins (#8929)
### What?

Adds a way to prevent creating new documents from the admin UI in a join
field.

### Why?

There are two reasons: 
1. You want to disable this any time as a feature of your admin user
experience
2. When creating a new document it is not yet possible to create the
relationship, preventing create is necessary for the workflow to make
sense.

### How?

join field has a new admin property called `allowCreate`, can be set to
false. By default the UI will never allow create when the current
document being edited does not yet have an `id`.

Fixes #

#8892

### Before

Even though the document doesn't have an ID yet, the create buttons are
shown which doesn't actually work.

![image](https://github.com/user-attachments/assets/152abed4-a174-498b-835c-aa4779c46834)

### After

Initial document creation: 
![Screenshot 2024-10-29
125132](https://github.com/user-attachments/assets/f33b1532-5b72-4c94-967d-bda618dadd34)

Prevented using `allowCreate: false`
![Screenshot 2024-10-29
130409](https://github.com/user-attachments/assets/69c3f601-fab3-4f5a-9df5-93fd133682ca)
2024-10-29 16:49:27 -04:00
Kendell Joseph
29704428bd chore: corrects package import paths for live preview test (#8925)
Corrects package import paths for live preview test.

- This would cause a import glitch when trying to run the live-preview
test due to incorrect file paths.
2024-10-29 16:12:45 -04:00
Sasha
6c341b5661 fix(ui): sanitize limit for preferences (#8913)
### What?
Fixes the issue with passing a string `limit` value from user
preferences to the mongodb `.aggregate` function.

To reproduce:

- click the list view for a collection that has a join field
- set "show per page" to 100
- reload, see this:

<img width="1001" alt="image"
src="https://github.com/user-attachments/assets/86c644d1-d183-48e6-bf34-0ccac23cb114">

### Why?
When using `.aggregate`, MongoDB doesn't cast a value for the `$limit`
stage to a number automatically as it's not handled by Mongoose. It's
also more convenient to store this value as a number.

### How?
Stores `limit` inside of preferences in number.
2024-10-29 16:03:31 -04:00
Kendell Joseph
9c530e47bb chore: changes admin API key field visuals based on read and update permissions (#8923)
Issue: https://github.com/payloadcms/payload/issues/8785
2024-10-29 18:56:29 +00:00
Elliot DeNolf
7ba19e03d6 ci: add payload-cloud as valid pr scope 2024-10-29 13:47:37 -04:00
Paul
c0aa96f59a fix(ui): missing localization label on text area fields (#8927) 2024-10-29 17:19:38 +00:00
Patrik
c7bde52aba chore: adds additional locked documents e2e tests (#8921)
Additional tests for global locked documents
2024-10-29 10:03:09 -04:00
Paul
915a3ce3f5 docs: fix dead link for local API (#8917) 2024-10-29 05:56:05 +00:00
Said Akhrarov
b6867f222b docs: form builder number field table unformatted (#8915)
### What?
This PR aims to fix an issue in the form-builder plugin page - in the
`number` field table, where an issue with one of the columns makes the
whole table unformatted. [See issue
here](https://payloadcms.com/docs/beta/plugins/form-builder#number).

### Why?
As it stands, the whole table is being rendered without any formatting,
making understanding it very difficult.

### How?
Changes to `docs/plugins/form-builder.mdx`
2024-10-28 23:29:21 -06:00
271 changed files with 12955 additions and 653 deletions

View File

@@ -46,6 +46,7 @@ jobs:
live-preview
live-preview-react
next
payload-cloud
plugin-cloud
plugin-cloud-storage
plugin-form-builder

View File

@@ -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:

View File

@@ -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.

View File

@@ -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'

View File

@@ -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.

View File

@@ -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). |

View File

@@ -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

View File

@@ -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._

View File

@@ -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
{

View File

@@ -126,6 +126,6 @@ await payload.update({
where: {
slug: { equals: 'my-slug' }
},
req: { disableTransaction: true },
disableTransaction: true,
})
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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. |

View File

@@ -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.

View 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.

View File

@@ -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:

View File

@@ -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._

View File

@@ -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:

View File

@@ -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
View 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,
},
],
}
```

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.120",
"version": "3.0.0-beta.122",
"private": true,
"type": "module",
"scripts": {

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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))

View File

@@ -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 = {

View File

@@ -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({

View File

@@ -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,
}

View File

@@ -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)
}

View File

@@ -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,
}

View File

@@ -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,
})

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 })
}

View File

@@ -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
}

View 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
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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,
})

View File

@@ -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
}
}

View File

@@ -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,
})

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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
}
}

View File

@@ -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`,

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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(

View File

@@ -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,
})

View File

@@ -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({

View File

@@ -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', {

View File

@@ -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,
})

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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')

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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,
})

View File

@@ -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')

View 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
}

View File

@@ -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,

View 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
}

View File

@@ -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 = ({

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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}

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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": {

View File

@@ -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