Compare commits
58 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7faa6253fc | ||
|
|
7d2022f28b | ||
|
|
493b121ae8 | ||
|
|
55afbe589c | ||
|
|
e916512fa7 | ||
|
|
81a972d966 | ||
|
|
1f7d47a361 | ||
|
|
afb8805a9b | ||
|
|
0789f4d0d4 | ||
|
|
cb831362c7 | ||
|
|
1afcaa30ed | ||
|
|
d3b982f38d | ||
|
|
265d7fa0e2 | ||
|
|
2596b85027 | ||
|
|
6ef2bdea15 | ||
|
|
b51d2bcb39 | ||
|
|
0cdd5b628c | ||
|
|
63b446c82b | ||
|
|
dbdc7d9308 | ||
|
|
79c117c664 | ||
|
|
faaa1188f4 | ||
|
|
c6a7ad2817 | ||
|
|
9c3f863ad2 | ||
|
|
879f690161 | ||
|
|
405a6c3447 | ||
|
|
a94e40f762 | ||
|
|
2d29b7e254 | ||
|
|
7b907a8701 | ||
|
|
72995fccf5 | ||
|
|
a095a6f891 | ||
|
|
37e1adfa5c | ||
|
|
9821aeb67a | ||
|
|
dd96f9a058 | ||
|
|
023c650e03 | ||
|
|
4d54cc6e02 | ||
|
|
f011e4fa26 | ||
|
|
c0aad3cccb | ||
|
|
110fda7533 | ||
|
|
f98d032617 | ||
|
|
05a3cc47a6 | ||
|
|
89601f18f5 | ||
|
|
31ffc57366 | ||
|
|
9035467998 | ||
|
|
029eba57b2 | ||
|
|
67cd3b3cf8 | ||
|
|
bf48af411d | ||
|
|
a7e8828e5e | ||
|
|
a06458d70d | ||
|
|
149b7cb26c | ||
|
|
ccd0b1ef48 | ||
|
|
d2eafdfaf8 | ||
|
|
a68f0cec4a | ||
|
|
8520fd9570 | ||
|
|
b7a0b15786 | ||
|
|
aee76cb793 | ||
|
|
b7db53cef4 | ||
|
|
f72fd8543b | ||
|
|
d046e0d18f |
48
.github/actions/setup/action.yml
vendored
Normal file
48
.github/actions/setup/action.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Setup node and pnpm
|
||||
description: Configure the Node.js and pnpm versions
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'The Node.js version to use'
|
||||
required: true
|
||||
default: 22.6.2
|
||||
pnpm-version:
|
||||
description: 'The pnpm version to use'
|
||||
required: true
|
||||
default: 9.7.1
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
shell: bash
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- shell: bash
|
||||
run: pnpm install
|
||||
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -310,6 +310,7 @@ jobs:
|
||||
- fields__collections__Upload
|
||||
- live-preview
|
||||
- localization
|
||||
- locked-documents
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
|
||||
1
.github/workflows/pr-title.yml
vendored
1
.github/workflows/pr-title.yml
vendored
@@ -52,7 +52,6 @@ jobs:
|
||||
plugin-form-builder
|
||||
plugin-nested-docs
|
||||
plugin-redirects
|
||||
plugin-relationship-object-ids
|
||||
plugin-search
|
||||
plugin-sentry
|
||||
plugin-seo
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -313,3 +313,4 @@ test/admin-root/app/(payload)/admin/importMap.js
|
||||
test/app/(payload)/admin/importMap.js
|
||||
/test/app/(payload)/admin/importMap.js
|
||||
test/pnpm-lock.yaml
|
||||
test/databaseAdapter.js
|
||||
|
||||
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -118,6 +118,13 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js locked-documents",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Locked Documents",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js uploads",
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
||||
@@ -357,7 +357,7 @@ Each Custom Component receives the following props by default:
|
||||
| Prop | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `payload` | The [Payload](../local-api/overview) class. |
|
||||
| `i18n` | The [i18n](../i18n) object. |
|
||||
| `i18n` | The [i18n](../configuration/i18n) object. |
|
||||
|
||||
Custom Components also receive various other props that are specific to the context in which the Custom Component is being rendered. For example, [Custom Views](./views) receive the `user` prop. For a full list of available props, consult the documentation related to the specific component you are working with.
|
||||
|
||||
|
||||
79
docs/admin/locked-documents.mdx
Normal file
79
docs/admin/locked-documents.mdx
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Document Locking
|
||||
label: Document Locking
|
||||
order: 90
|
||||
desc: Ensure your documents are locked while being edited, preventing concurrent edits from multiple users and preserving data integrity.
|
||||
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
|
||||
---
|
||||
|
||||
Document locking in Payload ensures that only one user at a time can edit a document, preventing data conflicts and accidental overwrites. When a document is locked, other users are prevented from making changes until the lock is released, ensuring data integrity in collaborative environments.
|
||||
|
||||
The lock is automatically triggered when a user begins editing a document within the Admin Panel and remains in place until the user exits the editing view or the lock expires due to inactivity.
|
||||
|
||||
## How it works
|
||||
|
||||
When a user starts editing a document, Payload locks the document for that user. If another user tries to access the same document, they will be notified that it is currently being edited and can choose one of the following options:
|
||||
|
||||
- View in Read-Only Mode: View the document without making any changes.
|
||||
- Take Over Editing: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
|
||||
- Return to Dashboard: Navigate away from the locked document and continue with other tasks.
|
||||
|
||||
The lock will automatically expire after a set period of inactivity, configurable using the duration property in the lockDocuments configuration, after which others can resume editing.
|
||||
|
||||
<Banner type="info"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
|
||||
|
||||
### Config Options
|
||||
|
||||
The lockDocuments property exists on both the Collection Config and the Global Config. By default, document locking is enabled for all collections and globals, but you can customize the lock duration or disable the feature entirely.
|
||||
|
||||
Here’s an example configuration for document locking:
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
// other fields...
|
||||
],
|
||||
lockDocuments: {
|
||||
duration: 600, // Duration in seconds
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Locking Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`lockDocuments`** | Enables or disables document locking for the collection or global. By default, document locking is enabled. Set to an object to configure, or set to false to disable locking. |
|
||||
| **`duration`** | Specifies the duration (in seconds) for how long a document remains locked without user interaction. The default is 300 seconds (5 minutes). |
|
||||
|
||||
### Impact on APIs
|
||||
|
||||
Document locking affects both the Local API and the REST API, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
|
||||
|
||||
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
|
||||
|
||||
#### Overriding Locks
|
||||
|
||||
For operations like update and delete, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
|
||||
|
||||
By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`.
|
||||
|
||||
```ts
|
||||
const result = await payload.update({
|
||||
collection: 'posts',
|
||||
id: '123',
|
||||
data: {
|
||||
title: 'New title',
|
||||
},
|
||||
overrideLock: false, // Enforces the document lock, preventing updates if the document is locked
|
||||
})
|
||||
```
|
||||
|
||||
This option is particularly useful in scenarios where administrative privileges or specific workflows require you to override the lock and ensure the operation is completed.
|
||||
@@ -85,3 +85,47 @@ const config = buildConfig({
|
||||
|
||||
export default config
|
||||
```
|
||||
|
||||
#### Cross domain authentication
|
||||
|
||||
If your frontend is on a different domain than your Payload API then you will not be able to use HTTP-only cookies for authentication by default as they will be considered third-party cookies by the browser.
|
||||
There are a few strategies to get around this:
|
||||
|
||||
##### 1. Use subdomains
|
||||
|
||||
Cookies can cross subdomains without being considered third party cookies, for example if your API is at api.example.com then you can authenticate from example.com.
|
||||
|
||||
##### 2. Configure cookies
|
||||
|
||||
If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](https://payloadcms.com/docs/beta/authentication/overview#config-options) on your authentication collection to achieve the following setup:
|
||||
|
||||
```
|
||||
SameSite: None // allows the cookie to cross domains
|
||||
Secure: true // ensures its sent over HTTPS only
|
||||
HttpOnly: true // ensures its not accessible via client side JavaScript
|
||||
```
|
||||
|
||||
Configuration example:
|
||||
|
||||
```ts
|
||||
{
|
||||
slug: 'users',
|
||||
auth: {
|
||||
cookies: {
|
||||
sameSite: 'None',
|
||||
secure: true,
|
||||
}
|
||||
},
|
||||
fields: [
|
||||
// your auth fields here
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
If you're configuring [cors](https://payloadcms.com/docs/beta/production/preventing-abuse#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains.
|
||||
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Good to know:</strong>
|
||||
Setting up <code>secure: true</code> will not work if you're developing on <code>http://localhost</code> or any non-https domain. For local development you should conditionally set this to <code>false</code> based on the environment.
|
||||
</Banner>
|
||||
|
||||
@@ -57,25 +57,26 @@ 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). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| **`defaultSort`** | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
||||
| **`dbName`** | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
|
||||
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| 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). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| **`defaultSort`** | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
||||
| **`dbName`** | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
|
||||
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -65,21 +65,22 @@ export const Nav: GlobalConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
|
||||
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/globals). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`dbName`** | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| **`description`** | Text or React component to display below the Global header to give editors more information. |
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
|
||||
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
|
||||
| **`label`** | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
|
||||
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#globals-config). |
|
||||
| Option | Description |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
|
||||
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/globals). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`dbName`** | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| **`description`** | Text or React component to display below the Global header to give editors more information. |
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
|
||||
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
|
||||
| **`label`** | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
|
||||
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#globals-config). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -42,24 +42,25 @@ export const MyArrayField: Field = {
|
||||
| Option | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) or an object with keys for each language. Auto-generated from name if not defined. |
|
||||
| **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) or an object with keys for each language. Auto-generated from name if not defined. |
|
||||
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
|
||||
| **`dbName`** | Custom table name for the field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -47,17 +47,18 @@ export const MyBlocksField: Field = {
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -34,16 +34,17 @@ export const MyCheckboxField: Field = {
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -38,16 +38,17 @@ export const MyBlocksField: Field = {
|
||||
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
|
||||
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -34,16 +34,17 @@ export const MyDateField: Field = {
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -35,16 +35,17 @@ export const MyEmailField: Field = {
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -39,16 +39,17 @@ export const MyGroupField: Field = {
|
||||
| **`fields`** \* | Array of field types to nest within this Group. |
|
||||
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
235
docs/fields/join.mdx
Normal file
235
docs/fields/join.mdx
Normal file
@@ -0,0 +1,235 @@
|
||||
---
|
||||
title: Join Field
|
||||
label: Join
|
||||
order: 140
|
||||
desc: The Join field provides the ability to work on related documents. Learn how to use Join field, see examples and options.
|
||||
keywords: join, relationship, junction, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Join Field is used to make Relationship fields in the opposite direction. It is used to show the relationship from
|
||||
the other side. The field itself acts as a virtual field, in that no new data is stored on the collection with a Join
|
||||
field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's
|
||||
APIs.
|
||||
|
||||
The Join field is useful in scenarios including:
|
||||
|
||||
- To surface `Order`s for a given `Product`
|
||||
- To view and edit `Posts` belonging to a `Category`
|
||||
- To work with any bi-directional relationship data
|
||||
|
||||
For the Join field to work, you must have an existing [relationship](./relationship) field in the collection you are
|
||||
joining. This will reference the collection and path of the field of the related documents.
|
||||
To add a Relationship Field, set the `type` to `join` in your [Field Config](./overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
export const MyJoinField: Field = {
|
||||
// highlight-start
|
||||
name: 'relatedPosts',
|
||||
type: 'join',
|
||||
collection: 'posts',
|
||||
on: 'category',
|
||||
// highlight-end
|
||||
}
|
||||
|
||||
// relationship field in another collection:
|
||||
export const MyRelationshipField: Field = {
|
||||
name: 'category',
|
||||
type: 'relationship',
|
||||
relationTo: 'categories',
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the field is defined to show the related `posts` when added to a `category` collection. The `on`
|
||||
property is used to
|
||||
specify the relationship field name of the field that relates to the collection document.
|
||||
|
||||
With this example, if you navigate to a Category in the Admin UI or an API response, you'll now see that the Posts which
|
||||
are related to the Category are populated for you. This is extremely powerful and can be used to define a wide variety
|
||||
of relationship types in an easy manner.
|
||||
|
||||
<Banner type="success">
|
||||
The Join field is extremely performant and does not add additional query overhead to your API responses until you add depth of 1 or above. It works in all database adapters. In MongoDB, we use <strong>aggregations</strong> to automatically join in related documents, and in relational databases, we use joins.
|
||||
</Banner>
|
||||
|
||||
### Schema advice
|
||||
|
||||
When modeling your database, you might come across many places where you'd like to feature bi-directional relationships.
|
||||
But here's an important consideration—you generally only want to store information about a given relationship in _one_
|
||||
place.
|
||||
|
||||
Let's take the Posts and Categories example. It makes sense to define which category a post belongs to while editing the
|
||||
post.
|
||||
|
||||
It would generally not be necessary to have a list of post IDs stored directly on the category as well, for a few
|
||||
reasons:
|
||||
|
||||
- You want to have a "single source of truth" for relationships, and not worry about keeping two sources in sync with
|
||||
one another
|
||||
- If you have hundreds, thousands, or even millions of posts, you would not want to store all of those post IDs on a
|
||||
given category
|
||||
- Etc.
|
||||
|
||||
This is where the `join` field is especially powerful. With it, you only need to store the `category_id` on the `post`,
|
||||
and Payload will automatically join in related posts for you when you query for categories. The related category is only
|
||||
stored on the post itself - and is not duplicated on both sides. However, the `join` field is what enables
|
||||
bi-directional APIs and UI for you.
|
||||
|
||||
### Using the Join field to have full control of your database schema
|
||||
|
||||
For typical polymorphic / many relationships, if you're using Postgres or SQLite, Payload will automatically create
|
||||
a `posts_rels` table, which acts as a junction table to store all of a given document's relationships.
|
||||
|
||||
However, this might not be appropriate for your use case if you'd like to have more control over your database
|
||||
architecture. You might not want to have that `_rels` table, and would prefer to maintain / control your own junction
|
||||
table design.
|
||||
|
||||
<Banner type="success">
|
||||
With the Join field, you can control your own junction table design, and avoid Payload's automatic _rels table creation.
|
||||
</Banner>
|
||||
|
||||
The `join` field can be used in conjunction with _any_ collection - and if you wanted to define your own "junction"
|
||||
collection, which, say, is called `categories_posts` and has a `post_id` and a `category_id` column, you can achieve
|
||||
complete control over the shape of that junction table.
|
||||
|
||||
You could go a step further and leverage the `admin.hidden` property of the `categories_posts` collection to hide the
|
||||
collection from appearing in the Admin UI navigation.
|
||||
|
||||
#### Specifying additional fields on relationships
|
||||
|
||||
Another very powerful use case of the `join` field is to be able to define "context" fields on your relationships. Let's
|
||||
say that you have Posts and Categories, and use join fields on both your Posts and Categories collection to join in
|
||||
related docs from a new pseudo-junction collection called `categories_posts`. Now, the relations are stored in this
|
||||
third junction collection, and can be surfaced on both Posts and Categories. But, importantly, you could add
|
||||
additional "context" fields to this shared junction collection.
|
||||
|
||||
For example, on this `categories_posts` collection, in addition to having the `category` and
|
||||
post` fields, we could add custom "context" fields like `featured` or `
|
||||
spotlight`, which would allow you to store additional information directly on relationships. The `join` field gives you
|
||||
complete control over any type of relational architecture in Payload, all wrapped up in a 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 relationship field name of the field that relates to 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). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`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 |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
## Join Field Data
|
||||
|
||||
When a document is returned that for a Join field is populated with related documents. The structure returned is an
|
||||
object with:
|
||||
|
||||
- `docs` an array of related documents or only IDs if the depth is reached
|
||||
- `hasNextPage` a boolean indicating if there are additional documents
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "66e3431a3f23e684075aae9c",
|
||||
"relatedPosts": {
|
||||
"docs": [
|
||||
{
|
||||
"id": "66e3431a3f23e684075aaeb9",
|
||||
// other fields...
|
||||
"category": "66e3431a3f23e684075aae9c",
|
||||
},
|
||||
// { ... }
|
||||
],
|
||||
"hasNextPage": false
|
||||
},
|
||||
// other fields...
|
||||
}
|
||||
```
|
||||
|
||||
## Query Options
|
||||
|
||||
The Join Field supports custom queries to filter, sort, and limit the related documents that will be returned. In
|
||||
addition to the specific query options for each Join Field, you can pass `joins: false` to disable all Join Field from
|
||||
returning. This is useful for performance reasons when you don't need the related documents.
|
||||
|
||||
The following query options are supported:
|
||||
|
||||
| Property | Description |
|
||||
|-------------|--------------------------------------------------------------|
|
||||
| **`limit`** | The maximum related documents to be returned, default is 10. |
|
||||
| **`where`** | An optional `Where` query to filter joined documents. |
|
||||
| **`sort`** | A string used to order related results |
|
||||
|
||||
These can be applied to the local API, GraphQL, and REST API.
|
||||
|
||||
### Local API
|
||||
|
||||
By adding `joins` to the local API you can customize the request for each join field by the `name` of the field.
|
||||
|
||||
```js
|
||||
const result = await db.findOne('categories', {
|
||||
where: {
|
||||
title: {
|
||||
equals: 'My Category'
|
||||
}
|
||||
},
|
||||
joins: {
|
||||
relatedPosts: {
|
||||
limit: 5,
|
||||
where: {
|
||||
title: {
|
||||
equals: 'My Post'
|
||||
}
|
||||
},
|
||||
sort: 'title'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Rest API
|
||||
|
||||
The rest API supports the same query options as the local API. You can use the `joins` query parameter to customize the
|
||||
request for each join field by the `name` of the field. For example, an API call to get a document with the related
|
||||
posts limited to 5 and sorted by title:
|
||||
|
||||
`/api/categories/${id}?joins[relatedPosts][limit]=5&joins[relatedPosts][sort]=title`
|
||||
|
||||
You can specify as many `joins` parameters as needed for the same or different join fields for a single request.
|
||||
|
||||
### GraphQL
|
||||
|
||||
The GraphQL API supports the same query options as the local and REST APIs. You can specify the query options for each join field in your query.
|
||||
|
||||
Example:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
Categories {
|
||||
docs {
|
||||
relatedPosts(
|
||||
sort: "createdAt"
|
||||
limit: 5
|
||||
where: {
|
||||
author: {
|
||||
equals: "66e3431a3f23e684075aaeb9"
|
||||
}
|
||||
}
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -37,16 +37,17 @@ export const MyJSONField: Field = {
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -40,16 +40,17 @@ export const MyNumberField: Field = {
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -40,16 +40,17 @@ export const MyPointField: Field = {
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -40,17 +40,18 @@ export const MyRadioField: Field = {
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -50,16 +50,17 @@ export const MyRelationshipField: Field = {
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -196,6 +197,12 @@ You can learn more about writing queries [here](/docs/queries/overview).
|
||||
<strong>payload/shared</strong> in your validate function.
|
||||
</Banner>
|
||||
|
||||
## Bi-directional relationships
|
||||
|
||||
The `relationship` field on its own is used to define relationships for the document that contains the relationship field, and this can be considered as a "one-way" relationship. For example, if you have a Post that has a `category` relationship field on it, the related `category` itself will not surface any information about the posts that have the category set.
|
||||
|
||||
However, the `relationship` field can be used in conjunction with the `Join` field to produce powerful bi-directional relationship authoring capabilities. If you're interested in bi-directional relationships, check out the [documentation for the Join field](./join).
|
||||
|
||||
## How the data is saved
|
||||
|
||||
Given the variety of options possible within the `relationship` field type, the shape of the data needed for creating
|
||||
|
||||
@@ -38,22 +38,23 @@ Right now, Payload is officially supporting two rich text editors:
|
||||
|
||||
## Config Options
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`editor`** | Override the rich text editor specified in your base configuration for this field. |
|
||||
| **`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 stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`editor`** | Override the rich text editor specified in your base configuration for this field. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -33,27 +33,28 @@ export const MySelectField: Field = {
|
||||
|
||||
## Config Options
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
|
||||
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-options) for more details. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| Option | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
|
||||
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-options) for more details. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ Each tab must have either a `name` or `label` and the required `fields` array. Y
|
||||
| **`fields`** \* | The fields to render within this tab. |
|
||||
| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. |
|
||||
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). (`name` must be present) |
|
||||
| **`virtual`** | Provide `true` to disable field in the database (`name` must be present). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -37,19 +37,20 @@ export const MyTextField: Field = {
|
||||
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`hasMany`** | Makes this field an ordered array of text instead of just a single text. |
|
||||
| **`minRows`** | Minimum number of texts in the array, if `hasMany` is set to true. |
|
||||
| **`maxRows`** | Maximum number of texts in the array, if `hasMany` is set to true. |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -37,16 +37,17 @@ export const MyTextareaField: Field = {
|
||||
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -47,22 +47,23 @@ export const MyUploadField: Field = {
|
||||
| **`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> |
|
||||
| **`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) |
|
||||
| **`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. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`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). |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [Admin Options](../admin/fields#admin-options). |
|
||||
| **`admin`** | Admin-specific configuration. [Admin Options](../admin/fields#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -85,10 +85,12 @@ You can specify more options within the Local API vs. REST or GraphQL due to the
|
||||
| `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. |
|
||||
|
||||
_There are more options available on an operation by operation basis outlined below._
|
||||
|
||||
@@ -205,6 +207,7 @@ const result = await payload.update({
|
||||
fallbackLocale: false,
|
||||
user: dummyUser,
|
||||
overrideAccess: false,
|
||||
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
|
||||
showHiddenFields: true,
|
||||
|
||||
// If your collection supports uploads, you can upload
|
||||
@@ -243,6 +246,7 @@ const result = await payload.update({
|
||||
fallbackLocale: false,
|
||||
user: dummyUser,
|
||||
overrideAccess: false,
|
||||
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
|
||||
showHiddenFields: true,
|
||||
|
||||
// If your collection supports uploads, you can upload
|
||||
@@ -269,6 +273,7 @@ const result = await payload.delete({
|
||||
fallbackLocale: false,
|
||||
user: dummyUser,
|
||||
overrideAccess: false,
|
||||
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
|
||||
showHiddenFields: true,
|
||||
})
|
||||
```
|
||||
@@ -292,6 +297,7 @@ const result = await payload.delete({
|
||||
fallbackLocale: false,
|
||||
user: dummyUser,
|
||||
overrideAccess: false,
|
||||
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
|
||||
showHiddenFields: true,
|
||||
})
|
||||
```
|
||||
@@ -428,6 +434,7 @@ const result = await payload.updateGlobal({
|
||||
fallbackLocale: false,
|
||||
user: dummyUser,
|
||||
overrideAccess: false,
|
||||
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
|
||||
showHiddenFields: true,
|
||||
})
|
||||
```
|
||||
|
||||
@@ -136,12 +136,28 @@ const beforeEmail: BeforeEmail<FormSubmission> = (emailsToSend, beforeChangePara
|
||||
}
|
||||
```
|
||||
|
||||
### `defaultToEmail`
|
||||
|
||||
Provide a fallback for the email address to send form submissions to. If the email in form configuration does not have a to email set, this email address will be used. If this is not provided then it falls back to the `defaultFromAddress` in your [email configuration](https://payloadcms.com/docs/beta/email/overview).
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
formBuilder({
|
||||
// ...
|
||||
defaultToEmail: 'test@example.com',
|
||||
})
|
||||
```
|
||||
|
||||
### `formOverrides`
|
||||
|
||||
Override anything on the `forms` collection by sending a [Payload Collection Config](https://payloadcms.com/docs/configuration/collections) to the `formOverrides` property.
|
||||
|
||||
Note that the `fields` property is a function that receives the default fields and returns an array of fields. This is because the `fields` property is a special case that is merged with the default fields, rather than replacing them. This allows you to map over default fields and modify them as needed.
|
||||
|
||||
<Banner type="warning">
|
||||
Good to know: The form collection is publicly available to read by default. The emails field is locked for authenticated users only. If you have any frontend users you should override the access permissions for both the collection and the emails field to make sure you don't leak out any private emails.
|
||||
</Banner>
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
formBuilder({
|
||||
@@ -149,7 +165,7 @@ formBuilder({
|
||||
formOverrides: {
|
||||
slug: 'contact-forms',
|
||||
access: {
|
||||
read: () => true,
|
||||
read: ({ req: { user } }) => !!user, // authenticated users only
|
||||
update: () => false,
|
||||
},
|
||||
fields: ({ defaultFields }) => {
|
||||
@@ -174,7 +190,7 @@ Override anything on the `form-submissions` collection by sending a [Payload Col
|
||||
control](https://payloadcms.com/docs/access-control/collections) to restrict the `update` and
|
||||
`read` operations on the `form-submissions` collection. This is because _anyone_ should be able to
|
||||
create a form submission, even from a public-facing website, but _no one_ should be able to update
|
||||
a submission one it has been created, or read a submission unless they have permission. You can
|
||||
a submission once it has been created, or read a submission unless they have permission. You can
|
||||
override this behavior or any other property as needed.
|
||||
</Banner>
|
||||
|
||||
@@ -396,7 +412,19 @@ formBuilder({
|
||||
|
||||
## Email
|
||||
|
||||
This plugin relies on the [email configuration](https://payloadcms.com/docs/email/overview) defined in your `payload.init()`. It will read from your config and attempt to send your emails using the credentials provided.
|
||||
This plugin relies on the [email configuration](https://payloadcms.com/docs/beta/email/overview) defined in your payload configuration. It will read from your config and attempt to send your emails using the credentials provided.
|
||||
|
||||
### 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 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:
|
||||
|
||||
```html
|
||||
Thank you for your submission, {{name}}!
|
||||
```
|
||||
|
||||
You can also use `{{*}}` as a wildcard to output all the data in a key:value format and `{{*:table}}` to output all the data in a table format.
|
||||
|
||||
## TypeScript
|
||||
|
||||
|
||||
@@ -243,5 +243,5 @@ official [Nested Docs Plugin Example](https://github.com/payloadcms/payload/tree
|
||||
demonstrates exactly how to configure this plugin in Payload and implement it on your front-end.
|
||||
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an
|
||||
official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website)
|
||||
and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere), both of which use this
|
||||
and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce), both of which use this
|
||||
plugin.
|
||||
|
||||
@@ -119,11 +119,33 @@ A function that allows you to return any meta title, including from document's c
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateTitle: ({ ...docInfo, doc, locale, req }) => `Website.com — ${doc?.title}`,
|
||||
generateTitle: ({ doc }) => `Website.com — ${doc?.title}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
All "generate" functions receive the following arguments:
|
||||
|
||||
| Argument | Description |
|
||||
| --- | --- |
|
||||
| **`collectionConfig`** | The configuration of the collection. |
|
||||
| **`collectionSlug`** | The slug of the collection. |
|
||||
| **`doc`** | The data of the current document. |
|
||||
| **`docPermissions`** | The permissions of the document. |
|
||||
| **`globalConfig`** | The configuration of the global. |
|
||||
| **`globalSlug`** | The slug of the global. |
|
||||
| **`hasPublishPermission`** | Whether the user has permission to publish the document. |
|
||||
| **`hasSavePermission`** | Whether the user has permission to save the document. |
|
||||
| **`id`** | The ID of the document. |
|
||||
| **`initialData`** | The initial data of the document. |
|
||||
| **`initialState`** | The initial state of the document. |
|
||||
| **`locale`** | The locale of the document. |
|
||||
| **`preferencesKey`** | The preferences key of the document. |
|
||||
| **`publishedDoc`** | The published document. |
|
||||
| **`req`** | The Payload request object containing `user`, `payload`, `i18n`, etc. |
|
||||
| **`title`** | The title of the document. |
|
||||
| **`versionsCount`** | The number of versions of the document. |
|
||||
|
||||
##### `generateDescription`
|
||||
|
||||
A function that allows you to return any meta description, including from document's content.
|
||||
@@ -133,11 +155,13 @@ A function that allows you to return any meta description, including from docume
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateDescription: ({ ...docInfo, doc, locale, req }) => doc?.excerpt,
|
||||
generateDescription: ({ doc }) => doc?.excerpt,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
|
||||
|
||||
##### `generateImage`
|
||||
|
||||
A function that allows you to return any meta image, including from document's content.
|
||||
@@ -147,11 +171,13 @@ A function that allows you to return any meta image, including from document's c
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateImage: ({ ...docInfo, doc, locale, req }) => doc?.featuredImage,
|
||||
generateImage: ({ doc }) => doc?.featuredImage,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
|
||||
|
||||
##### `generateURL`
|
||||
|
||||
A function called by the search preview component to display the actual URL of your page.
|
||||
@@ -161,12 +187,14 @@ A function called by the search preview component to display the actual URL of y
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateURL: ({ ...docInfo, doc, locale, req }) =>
|
||||
`https://yoursite.com/${collection?.slug}/${doc?.slug}`,
|
||||
generateURL: ({ doc, collectionSlug }) =>
|
||||
`https://yoursite.com/${collectionSlug}/${doc?.slug}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
|
||||
|
||||
#### `interfaceName`
|
||||
|
||||
Rename the meta group interface name that is generated for TypeScript and GraphQL.
|
||||
|
||||
@@ -573,6 +573,11 @@ In addition to the dynamically generated endpoints above Payload also has REST e
|
||||
|
||||
Additional REST API endpoints can be added to your application by providing an array of `endpoints` in various places within a Payload Config. Custom endpoints are useful for adding additional middleware on existing routes or for building custom functionality into Payload apps and plugins. Endpoints can be added at the top of the Payload Config, `collections`, and `globals` and accessed respective of the api and slugs you have configured.
|
||||
|
||||
<Banner type="warning">
|
||||
Custom endpoints are not authenticated by default. You are responsible for securing your own endpoints.
|
||||
</Banner>
|
||||
|
||||
|
||||
Each endpoint object needs to have:
|
||||
|
||||
| Property | Description |
|
||||
@@ -625,6 +630,22 @@ export const Orders: CollectionConfig = {
|
||||
// data to update the document with
|
||||
}
|
||||
})
|
||||
return Response.json({
|
||||
message: 'successfully updated tracking info'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:id/forbidden',
|
||||
method: 'post',
|
||||
handler: async (req) => {
|
||||
// this is an example of an authenticated endpoint
|
||||
if (!req.user) {
|
||||
return Response.json({ error: 'forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
// do something
|
||||
|
||||
return Response.json({
|
||||
message: 'successfully updated tracking info'
|
||||
})
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"graphql": "^16.9.0",
|
||||
"next": "15.0.0-canary.104",
|
||||
"next": "15.0.0-canary.160",
|
||||
"payload": "3.0.0-beta.106",
|
||||
"react": "19.0.0-rc-06d0b89e-20240801",
|
||||
"react-dom": "19.0.0-rc-06d0b89e-20240801"
|
||||
"react": "19.0.0-rc-5dcb0097-20240918",
|
||||
"react-dom": "19.0.0-rc-5dcb0097-20240918"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/graphql": "3.0.0-beta.106",
|
||||
|
||||
@@ -55,59 +55,108 @@ import { CustomDefaultRootView as CustomDefaultRootView_53 } from '@/components/
|
||||
import { CustomMinimalRootView as CustomMinimalRootView_54 } from '@/components/views/CustomMinimalRootView'
|
||||
|
||||
export const importMap = {
|
||||
"@/collections/Fields/array/components/server/Label#CustomArrayFieldLabelServer": CustomArrayFieldLabelServer_0,
|
||||
"@/collections/Fields/array/components/server/Field#CustomArrayFieldServer": CustomArrayFieldServer_1,
|
||||
"@/collections/Fields/array/components/client/Label#CustomArrayFieldLabelClient": CustomArrayFieldLabelClient_2,
|
||||
"@/collections/Fields/array/components/client/Field#CustomArrayFieldClient": CustomArrayFieldClient_3,
|
||||
"@/collections/Fields/blocks/components/server/Field#CustomBlocksFieldServer": CustomBlocksFieldServer_4,
|
||||
"@/collections/Fields/blocks/components/client/Field#CustomBlocksFieldClient": CustomBlocksFieldClient_5,
|
||||
"@/collections/Fields/checkbox/components/server/Label#CustomCheckboxFieldLabelServer": CustomCheckboxFieldLabelServer_6,
|
||||
"@/collections/Fields/checkbox/components/server/Field#CustomCheckboxFieldServer": CustomCheckboxFieldServer_7,
|
||||
"@/collections/Fields/checkbox/components/client/Label#CustomCheckboxFieldLabelClient": CustomCheckboxFieldLabelClient_8,
|
||||
"@/collections/Fields/checkbox/components/client/Field#CustomCheckboxFieldClient": CustomCheckboxFieldClient_9,
|
||||
"@/collections/Fields/date/components/server/Label#CustomDateFieldLabelServer": CustomDateFieldLabelServer_10,
|
||||
"@/collections/Fields/date/components/server/Field#CustomDateFieldServer": CustomDateFieldServer_11,
|
||||
"@/collections/Fields/date/components/client/Label#CustomDateFieldLabelClient": CustomDateFieldLabelClient_12,
|
||||
"@/collections/Fields/date/components/client/Field#CustomDateFieldClient": CustomDateFieldClient_13,
|
||||
"@/collections/Fields/email/components/server/Label#CustomEmailFieldLabelServer": CustomEmailFieldLabelServer_14,
|
||||
"@/collections/Fields/email/components/server/Field#CustomEmailFieldServer": CustomEmailFieldServer_15,
|
||||
"@/collections/Fields/email/components/client/Label#CustomEmailFieldLabelClient": CustomEmailFieldLabelClient_16,
|
||||
"@/collections/Fields/email/components/client/Field#CustomEmailFieldClient": CustomEmailFieldClient_17,
|
||||
"@/collections/Fields/number/components/server/Label#CustomNumberFieldLabelServer": CustomNumberFieldLabelServer_18,
|
||||
"@/collections/Fields/number/components/server/Field#CustomNumberFieldServer": CustomNumberFieldServer_19,
|
||||
"@/collections/Fields/number/components/client/Label#CustomNumberFieldLabelClient": CustomNumberFieldLabelClient_20,
|
||||
"@/collections/Fields/number/components/client/Field#CustomNumberFieldClient": CustomNumberFieldClient_21,
|
||||
"@/collections/Fields/point/components/server/Label#CustomPointFieldLabelServer": CustomPointFieldLabelServer_22,
|
||||
"@/collections/Fields/point/components/server/Field#CustomPointFieldServer": CustomPointFieldServer_23,
|
||||
"@/collections/Fields/point/components/client/Label#CustomPointFieldLabelClient": CustomPointFieldLabelClient_24,
|
||||
"@/collections/Fields/point/components/client/Field#CustomPointFieldClient": CustomPointFieldClient_25,
|
||||
"@/collections/Fields/radio/components/server/Label#CustomRadioFieldLabelServer": CustomRadioFieldLabelServer_26,
|
||||
"@/collections/Fields/radio/components/server/Field#CustomRadioFieldServer": CustomRadioFieldServer_27,
|
||||
"@/collections/Fields/radio/components/client/Label#CustomRadioFieldLabelClient": CustomRadioFieldLabelClient_28,
|
||||
"@/collections/Fields/radio/components/client/Field#CustomRadioFieldClient": CustomRadioFieldClient_29,
|
||||
"@/collections/Fields/relationship/components/server/Label#CustomRelationshipFieldLabelServer": CustomRelationshipFieldLabelServer_30,
|
||||
"@/collections/Fields/relationship/components/server/Field#CustomRelationshipFieldServer": CustomRelationshipFieldServer_31,
|
||||
"@/collections/Fields/relationship/components/client/Label#CustomRelationshipFieldLabelClient": CustomRelationshipFieldLabelClient_32,
|
||||
"@/collections/Fields/relationship/components/client/Field#CustomRelationshipFieldClient": CustomRelationshipFieldClient_33,
|
||||
"@/collections/Fields/select/components/server/Label#CustomSelectFieldLabelServer": CustomSelectFieldLabelServer_34,
|
||||
"@/collections/Fields/select/components/server/Field#CustomSelectFieldServer": CustomSelectFieldServer_35,
|
||||
"@/collections/Fields/select/components/client/Label#CustomSelectFieldLabelClient": CustomSelectFieldLabelClient_36,
|
||||
"@/collections/Fields/select/components/client/Field#CustomSelectFieldClient": CustomSelectFieldClient_37,
|
||||
"@/collections/Fields/text/components/server/Label#CustomTextFieldLabelServer": CustomTextFieldLabelServer_38,
|
||||
"@/collections/Fields/text/components/server/Field#CustomTextFieldServer": CustomTextFieldServer_39,
|
||||
"@/collections/Fields/text/components/client/Label#CustomTextFieldLabelClient": CustomTextFieldLabelClient_40,
|
||||
"@/collections/Fields/text/components/client/Field#CustomTextFieldClient": CustomTextFieldClient_41,
|
||||
"@/collections/Fields/textarea/components/server/Label#CustomTextareaFieldLabelServer": CustomTextareaFieldLabelServer_42,
|
||||
"@/collections/Fields/textarea/components/server/Field#CustomTextareaFieldServer": CustomTextareaFieldServer_43,
|
||||
"@/collections/Fields/textarea/components/client/Label#CustomTextareaFieldLabelClient": CustomTextareaFieldLabelClient_44,
|
||||
"@/collections/Fields/textarea/components/client/Field#CustomTextareaFieldClient": CustomTextareaFieldClient_45,
|
||||
"@/collections/Views/components/CustomTabEditView#CustomTabEditView": CustomTabEditView_46,
|
||||
"@/collections/Views/components/CustomDefaultEditView#CustomDefaultEditView": CustomDefaultEditView_47,
|
||||
"@/collections/RootViews/components/CustomRootEditView#CustomRootEditView": CustomRootEditView_48,
|
||||
"@/components/afterNavLinks/LinkToCustomView#LinkToCustomView": LinkToCustomView_49,
|
||||
"@/components/afterNavLinks/LinkToCustomMinimalView#LinkToCustomMinimalView": LinkToCustomMinimalView_50,
|
||||
"@/components/afterNavLinks/LinkToCustomDefaultView#LinkToCustomDefaultView": LinkToCustomDefaultView_51,
|
||||
"@/components/views/CustomRootView#CustomRootView": CustomRootView_52,
|
||||
"@/components/views/CustomDefaultRootView#CustomDefaultRootView": CustomDefaultRootView_53,
|
||||
"@/components/views/CustomMinimalRootView#CustomMinimalRootView": CustomMinimalRootView_54
|
||||
'@/collections/Fields/array/components/server/Label#CustomArrayFieldLabelServer':
|
||||
CustomArrayFieldLabelServer_0,
|
||||
'@/collections/Fields/array/components/server/Field#CustomArrayFieldServer':
|
||||
CustomArrayFieldServer_1,
|
||||
'@/collections/Fields/array/components/client/Label#CustomArrayFieldLabelClient':
|
||||
CustomArrayFieldLabelClient_2,
|
||||
'@/collections/Fields/array/components/client/Field#CustomArrayFieldClient':
|
||||
CustomArrayFieldClient_3,
|
||||
'@/collections/Fields/blocks/components/server/Field#CustomBlocksFieldServer':
|
||||
CustomBlocksFieldServer_4,
|
||||
'@/collections/Fields/blocks/components/client/Field#CustomBlocksFieldClient':
|
||||
CustomBlocksFieldClient_5,
|
||||
'@/collections/Fields/checkbox/components/server/Label#CustomCheckboxFieldLabelServer':
|
||||
CustomCheckboxFieldLabelServer_6,
|
||||
'@/collections/Fields/checkbox/components/server/Field#CustomCheckboxFieldServer':
|
||||
CustomCheckboxFieldServer_7,
|
||||
'@/collections/Fields/checkbox/components/client/Label#CustomCheckboxFieldLabelClient':
|
||||
CustomCheckboxFieldLabelClient_8,
|
||||
'@/collections/Fields/checkbox/components/client/Field#CustomCheckboxFieldClient':
|
||||
CustomCheckboxFieldClient_9,
|
||||
'@/collections/Fields/date/components/server/Label#CustomDateFieldLabelServer':
|
||||
CustomDateFieldLabelServer_10,
|
||||
'@/collections/Fields/date/components/server/Field#CustomDateFieldServer':
|
||||
CustomDateFieldServer_11,
|
||||
'@/collections/Fields/date/components/client/Label#CustomDateFieldLabelClient':
|
||||
CustomDateFieldLabelClient_12,
|
||||
'@/collections/Fields/date/components/client/Field#CustomDateFieldClient':
|
||||
CustomDateFieldClient_13,
|
||||
'@/collections/Fields/email/components/server/Label#CustomEmailFieldLabelServer':
|
||||
CustomEmailFieldLabelServer_14,
|
||||
'@/collections/Fields/email/components/server/Field#CustomEmailFieldServer':
|
||||
CustomEmailFieldServer_15,
|
||||
'@/collections/Fields/email/components/client/Label#CustomEmailFieldLabelClient':
|
||||
CustomEmailFieldLabelClient_16,
|
||||
'@/collections/Fields/email/components/client/Field#CustomEmailFieldClient':
|
||||
CustomEmailFieldClient_17,
|
||||
'@/collections/Fields/number/components/server/Label#CustomNumberFieldLabelServer':
|
||||
CustomNumberFieldLabelServer_18,
|
||||
'@/collections/Fields/number/components/server/Field#CustomNumberFieldServer':
|
||||
CustomNumberFieldServer_19,
|
||||
'@/collections/Fields/number/components/client/Label#CustomNumberFieldLabelClient':
|
||||
CustomNumberFieldLabelClient_20,
|
||||
'@/collections/Fields/number/components/client/Field#CustomNumberFieldClient':
|
||||
CustomNumberFieldClient_21,
|
||||
'@/collections/Fields/point/components/server/Label#CustomPointFieldLabelServer':
|
||||
CustomPointFieldLabelServer_22,
|
||||
'@/collections/Fields/point/components/server/Field#CustomPointFieldServer':
|
||||
CustomPointFieldServer_23,
|
||||
'@/collections/Fields/point/components/client/Label#CustomPointFieldLabelClient':
|
||||
CustomPointFieldLabelClient_24,
|
||||
'@/collections/Fields/point/components/client/Field#CustomPointFieldClient':
|
||||
CustomPointFieldClient_25,
|
||||
'@/collections/Fields/radio/components/server/Label#CustomRadioFieldLabelServer':
|
||||
CustomRadioFieldLabelServer_26,
|
||||
'@/collections/Fields/radio/components/server/Field#CustomRadioFieldServer':
|
||||
CustomRadioFieldServer_27,
|
||||
'@/collections/Fields/radio/components/client/Label#CustomRadioFieldLabelClient':
|
||||
CustomRadioFieldLabelClient_28,
|
||||
'@/collections/Fields/radio/components/client/Field#CustomRadioFieldClient':
|
||||
CustomRadioFieldClient_29,
|
||||
'@/collections/Fields/relationship/components/server/Label#CustomRelationshipFieldLabelServer':
|
||||
CustomRelationshipFieldLabelServer_30,
|
||||
'@/collections/Fields/relationship/components/server/Field#CustomRelationshipFieldServer':
|
||||
CustomRelationshipFieldServer_31,
|
||||
'@/collections/Fields/relationship/components/client/Label#CustomRelationshipFieldLabelClient':
|
||||
CustomRelationshipFieldLabelClient_32,
|
||||
'@/collections/Fields/relationship/components/client/Field#CustomRelationshipFieldClient':
|
||||
CustomRelationshipFieldClient_33,
|
||||
'@/collections/Fields/select/components/server/Label#CustomSelectFieldLabelServer':
|
||||
CustomSelectFieldLabelServer_34,
|
||||
'@/collections/Fields/select/components/server/Field#CustomSelectFieldServer':
|
||||
CustomSelectFieldServer_35,
|
||||
'@/collections/Fields/select/components/client/Label#CustomSelectFieldLabelClient':
|
||||
CustomSelectFieldLabelClient_36,
|
||||
'@/collections/Fields/select/components/client/Field#CustomSelectFieldClient':
|
||||
CustomSelectFieldClient_37,
|
||||
'@/collections/Fields/text/components/server/Label#CustomTextFieldLabelServer':
|
||||
CustomTextFieldLabelServer_38,
|
||||
'@/collections/Fields/text/components/server/Field#CustomTextFieldServer':
|
||||
CustomTextFieldServer_39,
|
||||
'@/collections/Fields/text/components/client/Label#CustomTextFieldLabelClient':
|
||||
CustomTextFieldLabelClient_40,
|
||||
'@/collections/Fields/text/components/client/Field#CustomTextFieldClient':
|
||||
CustomTextFieldClient_41,
|
||||
'@/collections/Fields/textarea/components/server/Label#CustomTextareaFieldLabelServer':
|
||||
CustomTextareaFieldLabelServer_42,
|
||||
'@/collections/Fields/textarea/components/server/Field#CustomTextareaFieldServer':
|
||||
CustomTextareaFieldServer_43,
|
||||
'@/collections/Fields/textarea/components/client/Label#CustomTextareaFieldLabelClient':
|
||||
CustomTextareaFieldLabelClient_44,
|
||||
'@/collections/Fields/textarea/components/client/Field#CustomTextareaFieldClient':
|
||||
CustomTextareaFieldClient_45,
|
||||
'@/collections/Views/components/CustomTabEditView#CustomTabEditView': CustomTabEditView_46,
|
||||
'@/collections/Views/components/CustomDefaultEditView#CustomDefaultEditView':
|
||||
CustomDefaultEditView_47,
|
||||
'@/collections/RootViews/components/CustomRootEditView#CustomRootEditView': CustomRootEditView_48,
|
||||
'@/components/afterNavLinks/LinkToCustomView#LinkToCustomView': LinkToCustomView_49,
|
||||
'@/components/afterNavLinks/LinkToCustomMinimalView#LinkToCustomMinimalView':
|
||||
LinkToCustomMinimalView_50,
|
||||
'@/components/afterNavLinks/LinkToCustomDefaultView#LinkToCustomDefaultView':
|
||||
LinkToCustomDefaultView_51,
|
||||
'@/components/views/CustomRootView#CustomRootView': CustomRootView_52,
|
||||
'@/components/views/CustomDefaultRootView#CustomDefaultRootView': CustomDefaultRootView_53,
|
||||
'@/components/views/CustomMinimalRootView#CustomMinimalRootView': CustomMinimalRootView_54,
|
||||
}
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
||||
35
package.json
35
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.107",
|
||||
"version": "3.0.0-beta.108",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -34,7 +34,6 @@
|
||||
"build:plugin-form-builder": "turbo build --filter \"@payloadcms/plugin-form-builder\"",
|
||||
"build:plugin-nested-docs": "turbo build --filter \"@payloadcms/plugin-nested-docs\"",
|
||||
"build:plugin-redirects": "turbo build --filter \"@payloadcms/plugin-redirects\"",
|
||||
"build:plugin-relationship-object-ids": "turbo build --filter \"@payloadcms/plugin-relationship-object-ids\"",
|
||||
"build:plugin-search": "turbo build --filter \"@payloadcms/plugin-search\"",
|
||||
"build:plugin-sentry": "turbo build --filter \"@payloadcms/plugin-sentry\"",
|
||||
"build:plugin-seo": "turbo build --filter \"@payloadcms/plugin-seo\"",
|
||||
@@ -70,6 +69,8 @@
|
||||
"lint:fix": "turbo run lint:fix --concurrency 1 --continue",
|
||||
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
"prepare": "husky",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
||||
@@ -83,8 +84,8 @@
|
||||
"test:e2e": "pnpm runts ./test/runE2E.ts",
|
||||
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd .. && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:e2e:prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd .. && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:e2e:prod": "pnpm prepare-run-test-against-prod && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:e2e:prod:ci": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
@@ -104,7 +105,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.6.2",
|
||||
"@next/bundle-analyzer": "15.0.0-canary.104",
|
||||
"@next/bundle-analyzer": "15.0.0-canary.160",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
@@ -118,8 +119,8 @@
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/prompts": "^2.4.5",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.0",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@types/semver": "^7.5.3",
|
||||
"@types/shelljs": "0.8.15",
|
||||
"chalk": "^4.1.2",
|
||||
@@ -142,15 +143,15 @@
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^9.0",
|
||||
"next": "15.0.0-canary.104",
|
||||
"next": "15.0.0-canary.160",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"playwright": "1.46.0",
|
||||
"playwright-core": "1.46.0",
|
||||
"prettier": "3.3.3",
|
||||
"prompts": "2.4.2",
|
||||
"react": "19.0.0-rc-06d0b89e-20240801",
|
||||
"react-dom": "19.0.0-rc-06d0b89e-20240801",
|
||||
"react": "19.0.0-rc-5dcb0097-20240918",
|
||||
"react-dom": "19.0.0-rc-5dcb0097-20240918",
|
||||
"rimraf": "3.0.2",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "0.32.6",
|
||||
@@ -160,12 +161,12 @@
|
||||
"swc-plugin-transform-remove-imports": "1.15.0",
|
||||
"tempy": "1.0.1",
|
||||
"tsx": "4.19.1",
|
||||
"turbo": "^2.1.1",
|
||||
"turbo": "^2.1.2",
|
||||
"typescript": "5.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
|
||||
"react": "^19.0.0 || ^19.0.0-rc-5dcb0097-20240918",
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-5dcb0097-20240918"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"engines": {
|
||||
@@ -178,8 +179,8 @@
|
||||
"domexception": "4"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.0",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"copyfiles": "$copyfiles",
|
||||
"cross-env": "$cross-env",
|
||||
"dotenv": "$dotenv",
|
||||
@@ -192,8 +193,8 @@
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.0",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0"
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
},
|
||||
"workspaces:": [
|
||||
"packages/*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.107",
|
||||
"version": "3.0.0-beta.108",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import execa from 'execa'
|
||||
import fse from 'fs-extra'
|
||||
|
||||
import type { CliArgs, PackageManager } from '../types.js'
|
||||
|
||||
export function getPackageManager(args: { cliArgs?: CliArgs; projectDir: string }): PackageManager {
|
||||
export async function getPackageManager(args: {
|
||||
cliArgs?: CliArgs
|
||||
projectDir: string
|
||||
}): Promise<PackageManager> {
|
||||
const { cliArgs, projectDir } = args
|
||||
|
||||
try {
|
||||
@@ -16,6 +20,9 @@ export function getPackageManager(args: { cliArgs?: CliArgs; projectDir: string
|
||||
detected = 'npm'
|
||||
} else if (cliArgs?.['--use-bun'] || fse.existsSync(`${projectDir}/bun.lockb`)) {
|
||||
detected = 'bun'
|
||||
} else if (await commandExists('pnpm')) {
|
||||
// Prefer pnpm if it's installed
|
||||
detected = 'pnpm'
|
||||
} else {
|
||||
// Otherwise check the execution environment
|
||||
detected = getEnvironmentPackageManager()
|
||||
@@ -44,3 +51,12 @@ function getEnvironmentPackageManager(): PackageManager {
|
||||
|
||||
return 'npm'
|
||||
}
|
||||
|
||||
async function commandExists(command: string): Promise<boolean> {
|
||||
try {
|
||||
await execa.command(`command -v ${command}`)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ export class Main {
|
||||
? path.dirname(nextConfigPath)
|
||||
: path.resolve(process.cwd(), slugify(projectName))
|
||||
|
||||
const packageManager = getPackageManager({ cliArgs: this.args, projectDir })
|
||||
const packageManager = await getPackageManager({ cliArgs: this.args, projectDir })
|
||||
|
||||
if (nextConfigPath) {
|
||||
p.log.step(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.107",
|
||||
"version": "3.0.0-beta.108",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -38,13 +38,14 @@
|
||||
"bson-objectid": "2.0.4",
|
||||
"http-status": "1.6.2",
|
||||
"mongoose": "6.12.3",
|
||||
"mongoose-aggregate-paginate-v2": "1.0.6",
|
||||
"mongoose-paginate-v2": "1.7.22",
|
||||
"prompts": "2.4.2",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.6",
|
||||
"mongodb": "4.17.1",
|
||||
"mongodb-memory-server": "^9",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Create, Document, PayloadRequest } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
@@ -12,8 +13,15 @@ export const create: Create = async function create(
|
||||
const Model = this.collections[collection]
|
||||
const options = await withSession(this, req)
|
||||
let doc
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
})
|
||||
|
||||
try {
|
||||
;[doc] = await Model.create([data], options)
|
||||
;[doc] = await Model.create([sanitizedData], options)
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CreateGlobal, PayloadRequest } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
@@ -10,10 +11,16 @@ export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
{ slug, data, req = {} as PayloadRequest },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const global = {
|
||||
globalType: slug,
|
||||
...data,
|
||||
}
|
||||
|
||||
const global = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
globalType: slug,
|
||||
...data,
|
||||
},
|
||||
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
|
||||
})
|
||||
|
||||
const options = await withSession(this, req)
|
||||
|
||||
let [result] = (await Model.create([global], options)) as any
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
import type { CreateGlobalVersion, Document, PayloadRequest } from 'payload'
|
||||
import {
|
||||
buildVersionGlobalFields,
|
||||
type CreateGlobalVersion,
|
||||
type Document,
|
||||
type PayloadRequest,
|
||||
} from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
|
||||
this: MongooseAdapter,
|
||||
{ autosave, createdAt, globalSlug, parent, req = {} as PayloadRequest, updatedAt, versionData },
|
||||
{
|
||||
autosave,
|
||||
createdAt,
|
||||
globalSlug,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
},
|
||||
) {
|
||||
const VersionModel = this.versions[globalSlug]
|
||||
const options = await withSession(this, req)
|
||||
|
||||
const [doc] = await VersionModel.create(
|
||||
[
|
||||
{
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
],
|
||||
options,
|
||||
req,
|
||||
)
|
||||
const data = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
),
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
|
||||
await VersionModel.updateMany(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { CreateVersion, Document, PayloadRequest } from 'payload'
|
||||
import {
|
||||
buildVersionCollectionFields,
|
||||
type CreateVersion,
|
||||
type Document,
|
||||
type PayloadRequest,
|
||||
} from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const createVersion: CreateVersion = async function createVersion(
|
||||
@@ -11,7 +17,9 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
collectionSlug,
|
||||
createdAt,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
},
|
||||
@@ -19,20 +27,25 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
const VersionModel = this.versions[collectionSlug]
|
||||
const options = await withSession(this, req)
|
||||
|
||||
const [doc] = await VersionModel.create(
|
||||
[
|
||||
{
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
],
|
||||
options,
|
||||
req,
|
||||
)
|
||||
const data = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collectionSlug].config,
|
||||
),
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
|
||||
await VersionModel.updateMany(
|
||||
{
|
||||
@@ -44,7 +57,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
$eq: parent,
|
||||
$eq: data.parent,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,12 +6,24 @@ import { flattenWhereToOperators } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const find: Find = async function find(
|
||||
this: MongooseAdapter,
|
||||
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where },
|
||||
{
|
||||
collection,
|
||||
joins = {},
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
req = {} as PayloadRequest,
|
||||
sort: sortArg,
|
||||
where,
|
||||
},
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
@@ -50,6 +62,7 @@ export const find: Find = async function find(
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
@@ -88,7 +101,24 @@ export const find: Find = async function find(
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
let result
|
||||
|
||||
const aggregate = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
query,
|
||||
})
|
||||
// build join aggregation
|
||||
if (aggregate) {
|
||||
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
|
||||
} else {
|
||||
result = await Model.paginate(query, paginationOptions)
|
||||
}
|
||||
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
) {
|
||||
const Model = this.versions[global]
|
||||
const versionFields = buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.globals.config.find(({ slug }) => slug === global),
|
||||
)
|
||||
const options = {
|
||||
|
||||
@@ -3,14 +3,16 @@ import type { Document, FindOne, PayloadRequest } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const findOne: FindOne = async function findOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, where },
|
||||
{ collection, joins, locale, req = {} as PayloadRequest, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const options: MongooseQueryOptions = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
@@ -22,7 +24,22 @@ export const findOne: FindOne = async function findOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOne(query, {}, options)
|
||||
const aggregate = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit: 1,
|
||||
locale,
|
||||
query,
|
||||
})
|
||||
|
||||
let doc
|
||||
if (aggregate) {
|
||||
;[doc] = await Model.aggregate(aggregate, options)
|
||||
} else {
|
||||
doc = await Model.findOne(query, {}, options)
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PaginateOptions } from 'mongoose'
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||
import paginate from 'mongoose-paginate-v2'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
|
||||
@@ -21,7 +22,7 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
if (collection.versions) {
|
||||
const versionModelName = getDBName({ config: collection, versions: true })
|
||||
|
||||
const versionCollectionFields = buildVersionCollectionFields(collection)
|
||||
const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection)
|
||||
|
||||
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
|
||||
disableUnique: true,
|
||||
@@ -40,12 +41,16 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
}),
|
||||
)
|
||||
|
||||
if (Object.keys(collection.joins).length > 0) {
|
||||
versionSchema.plugin(mongooseAggregatePaginate)
|
||||
}
|
||||
|
||||
const model = mongoose.model(
|
||||
versionModelName,
|
||||
versionSchema,
|
||||
this.autoPluralization === true ? undefined : versionModelName,
|
||||
) as CollectionModel
|
||||
// this.payload.versions[collection.slug] = model;
|
||||
|
||||
this.versions[collection.slug] = model
|
||||
}
|
||||
|
||||
@@ -64,7 +69,7 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
if (global.versions) {
|
||||
const versionModelName = getDBName({ config: global, versions: true })
|
||||
|
||||
const versionGlobalFields = buildVersionGlobalFields(global)
|
||||
const versionGlobalFields = buildVersionGlobalFields(this.payload.config, global)
|
||||
|
||||
const versionSchema = buildSchema(this.payload.config, versionGlobalFields, {
|
||||
disableUnique: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PaginateOptions, Schema } from 'mongoose'
|
||||
import type { SanitizedCollectionConfig, SanitizedConfig } from 'payload'
|
||||
|
||||
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||
import paginate from 'mongoose-paginate-v2'
|
||||
|
||||
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
|
||||
@@ -42,5 +43,9 @@ export const buildCollectionSchema = (
|
||||
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
|
||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
|
||||
|
||||
if (Object.keys(collection.joins).length > 0) {
|
||||
schema.plugin(mongooseAggregatePaginate)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
fieldAffectsData,
|
||||
fieldIsLocalized,
|
||||
fieldIsPresentationalOnly,
|
||||
fieldIsVirtual,
|
||||
tabHasName,
|
||||
} from 'payload/shared'
|
||||
|
||||
@@ -136,6 +137,10 @@ export const buildSchema = (
|
||||
const schema = new mongoose.Schema(fields, options)
|
||||
|
||||
schemaFields.forEach((field) => {
|
||||
if (fieldIsVirtual(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!fieldIsPresentationalOnly(field)) {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ export async function buildSearchParam({
|
||||
const subQuery = priorQueryResult.value
|
||||
const result = await SubModel.find(subQuery, subQueryOptions)
|
||||
|
||||
const $in = result.map((doc) => doc._id.toString())
|
||||
const $in = result.map((doc) => doc._id)
|
||||
|
||||
// If it is the last recursion
|
||||
// then pass through the search param
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Field, TabAsField } from 'payload'
|
||||
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
import mongoose from 'mongoose'
|
||||
import { createArrayFromCommaDelineated } from 'payload'
|
||||
|
||||
@@ -11,6 +12,8 @@ type SanitizeQueryValueArgs = {
|
||||
val: any
|
||||
}
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
export const sanitizeQueryValue = ({
|
||||
field,
|
||||
hasCustomID,
|
||||
@@ -26,21 +29,49 @@ export const sanitizeQueryValue = ({
|
||||
let formattedOperator = operator
|
||||
|
||||
// Disregard invalid _ids
|
||||
if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) {
|
||||
if (!hasCustomID) {
|
||||
const isValid = mongoose.Types.ObjectId.isValid(val)
|
||||
if (path === '_id') {
|
||||
if (typeof val === 'string' && val.split(',').length === 1) {
|
||||
if (!hasCustomID) {
|
||||
const isValid = mongoose.Types.ObjectId.isValid(val)
|
||||
|
||||
if (!isValid) {
|
||||
return { operator: formattedOperator, val: undefined }
|
||||
if (!isValid) {
|
||||
return { operator: formattedOperator, val: undefined }
|
||||
} else {
|
||||
if (['in', 'not_in'].includes(operator)) {
|
||||
formattedValue = createArrayFromCommaDelineated(formattedValue).map((id) =>
|
||||
ObjectId(id),
|
||||
)
|
||||
} else {
|
||||
formattedValue = ObjectId(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'number') {
|
||||
const parsedNumber = parseFloat(val)
|
||||
if (field.type === 'number') {
|
||||
const parsedNumber = parseFloat(val)
|
||||
|
||||
if (Number.isNaN(parsedNumber)) {
|
||||
return { operator: formattedOperator, val: undefined }
|
||||
if (Number.isNaN(parsedNumber)) {
|
||||
return { operator: formattedOperator, val: undefined }
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(val)) {
|
||||
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
|
||||
const newValues = [inVal]
|
||||
if (!hasCustomID) {
|
||||
if (mongoose.Types.ObjectId.isValid(inVal)) {
|
||||
newValues.push(ObjectId(inVal))
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'number') {
|
||||
const parsedNumber = parseFloat(inVal)
|
||||
if (!Number.isNaN(parsedNumber)) {
|
||||
newValues.push(parsedNumber)
|
||||
}
|
||||
}
|
||||
|
||||
return [...formattedValues, ...newValues]
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +117,13 @@ export const sanitizeQueryValue = ({
|
||||
formattedValue.value &&
|
||||
formattedValue.relationTo
|
||||
) {
|
||||
const { value } = formattedValue
|
||||
const isValid = mongoose.Types.ObjectId.isValid(value)
|
||||
|
||||
if (isValid) {
|
||||
formattedValue.value = ObjectId(value)
|
||||
}
|
||||
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
@@ -96,11 +134,11 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'in' && Array.isArray(formattedValue)) {
|
||||
if (['in', 'not_in'].includes(operator) && Array.isArray(formattedValue)) {
|
||||
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
|
||||
const newValues = [inVal]
|
||||
if (mongoose.Types.ObjectId.isValid(inVal)) {
|
||||
newValues.push(new mongoose.Types.ObjectId(inVal))
|
||||
newValues.push(ObjectId(inVal))
|
||||
}
|
||||
|
||||
const parsedNumber = parseFloat(inVal)
|
||||
@@ -111,6 +149,12 @@ export const sanitizeQueryValue = ({
|
||||
return [...formattedValues, ...newValues]
|
||||
}, [])
|
||||
}
|
||||
|
||||
if (operator === 'contains' && typeof formattedValue === 'string') {
|
||||
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
|
||||
formattedValue = ObjectId(formattedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up specific formatting necessary by operators
|
||||
@@ -152,12 +196,38 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
|
||||
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
|
||||
if (operator === 'contains') {
|
||||
if (operator === 'contains' && !mongoose.Types.ObjectId.isValid(formattedValue)) {
|
||||
formattedValue = {
|
||||
$options: 'i',
|
||||
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === 'exists') {
|
||||
formattedValue = formattedValue === 'true' || formattedValue === true
|
||||
|
||||
if (formattedValue) {
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [path]: { $exists: true } },
|
||||
{ [path]: { $ne: null } },
|
||||
{ [path]: { $ne: '' } },
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rawQuery: {
|
||||
$or: [
|
||||
{ [path]: { $exists: false } },
|
||||
{ [path]: { $eq: null } },
|
||||
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { IndexDefinition, IndexOptions, Model, PaginateModel, SchemaOptions } from 'mongoose'
|
||||
import type {
|
||||
AggregatePaginateModel,
|
||||
IndexDefinition,
|
||||
IndexOptions,
|
||||
Model,
|
||||
PaginateModel,
|
||||
SchemaOptions,
|
||||
} from 'mongoose'
|
||||
import type {
|
||||
ArrayField,
|
||||
BlocksField,
|
||||
@@ -9,6 +16,7 @@ import type {
|
||||
EmailField,
|
||||
Field,
|
||||
GroupField,
|
||||
JoinField,
|
||||
JSONField,
|
||||
NumberField,
|
||||
Payload,
|
||||
@@ -27,7 +35,10 @@ import type {
|
||||
|
||||
import type { BuildQueryArgs } from './queries/buildQuery.js'
|
||||
|
||||
export interface CollectionModel extends Model<any>, PaginateModel<any> {
|
||||
export interface CollectionModel
|
||||
extends Model<any>,
|
||||
PaginateModel<any>,
|
||||
AggregatePaginateModel<any> {
|
||||
/** buildQuery is used to transform payload's where operator into what can be used by mongoose (e.g. id => _id) */
|
||||
buildQuery: (args: BuildQueryArgs) => Promise<Record<string, unknown>> // TODO: Delete this
|
||||
}
|
||||
@@ -83,6 +94,7 @@ export type FieldToSchemaMap<TSchema> = {
|
||||
date: FieldGeneratorFunction<TSchema, DateField>
|
||||
email: FieldGeneratorFunction<TSchema, EmailField>
|
||||
group: FieldGeneratorFunction<TSchema, GroupField>
|
||||
join: FieldGeneratorFunction<TSchema, JoinField>
|
||||
json: FieldGeneratorFunction<TSchema, JSONField>
|
||||
number: FieldGeneratorFunction<TSchema, NumberField>
|
||||
point: FieldGeneratorFunction<TSchema, PointField>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest, UpdateGlobal } from 'payload'
|
||||
import type { MongooseAdapter } from './index.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(
|
||||
@@ -17,7 +18,14 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
}
|
||||
|
||||
let result
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, data, options)
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields: this.payload.config.globals.find((global) => global.slug === slug).fields,
|
||||
})
|
||||
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import type { PayloadRequest, TypeWithID, UpdateGlobalVersionArgs } from 'payload'
|
||||
import {
|
||||
buildVersionGlobalFields,
|
||||
type PayloadRequest,
|
||||
type TypeWithID,
|
||||
type UpdateGlobalVersionArgs,
|
||||
} from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
this: MongooseAdapter,
|
||||
{
|
||||
id,
|
||||
global,
|
||||
global: globalSlug,
|
||||
locale,
|
||||
req = {} as PayloadRequest,
|
||||
versionData,
|
||||
where,
|
||||
}: UpdateGlobalVersionArgs<T>,
|
||||
) {
|
||||
const VersionModel = this.versions[global]
|
||||
const VersionModel = this.versions[globalSlug]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
@@ -29,7 +35,16 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: versionData,
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
),
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
@@ -26,8 +27,14 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
|
||||
let result
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
})
|
||||
|
||||
try {
|
||||
result = await Model.findOneAndUpdate(query, data, options)
|
||||
result = await Model.findOneAndUpdate(query, sanitizedData, options)
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { PayloadRequest, UpdateVersion } from 'payload'
|
||||
import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
@@ -22,7 +23,16 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: versionData,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
),
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
|
||||
|
||||
174
packages/db-mongodb/src/utilities/buildJoinAggregation.ts
Normal file
174
packages/db-mongodb/src/utilities/buildJoinAggregation.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import type { PipelineStage } from 'mongoose'
|
||||
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { buildSortParam } from '../queries/buildSortParam.js'
|
||||
|
||||
type BuildJoinAggregationArgs = {
|
||||
adapter: MongooseAdapter
|
||||
collection: CollectionSlug
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
joins: JoinQuery
|
||||
// the number of docs to get at the top collection level
|
||||
limit?: number
|
||||
locale: string
|
||||
// the where clause for the top collection
|
||||
query?: Where
|
||||
}
|
||||
|
||||
export const buildJoinAggregation = async ({
|
||||
adapter,
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
query,
|
||||
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
|
||||
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const joinConfig = adapter.payload.collections[collection].config.joins
|
||||
const aggregate: PipelineStage[] = [
|
||||
{
|
||||
$sort: { createdAt: -1 },
|
||||
},
|
||||
]
|
||||
|
||||
if (query) {
|
||||
aggregate.push({
|
||||
$match: query,
|
||||
})
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
aggregate.push({
|
||||
$limit: limit,
|
||||
})
|
||||
}
|
||||
|
||||
for (const slug of Object.keys(joinConfig)) {
|
||||
for (const join of joinConfig[slug]) {
|
||||
const joinModel = adapter.collections[join.field.collection]
|
||||
|
||||
const {
|
||||
limit: limitJoin = 10,
|
||||
sort: sortJoin,
|
||||
where: whereJoin,
|
||||
} = joins?.[join.schemaPath] || {}
|
||||
|
||||
const sort = buildSortParam({
|
||||
config: adapter.payload.config,
|
||||
fields: adapter.payload.collections[slug].config.fields,
|
||||
locale,
|
||||
sort: sortJoin || collectionConfig.defaultSort,
|
||||
timestamps: true,
|
||||
})
|
||||
const sortProperty = Object.keys(sort)[0]
|
||||
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
|
||||
|
||||
const $match = await joinModel.buildQuery({
|
||||
locale,
|
||||
payload: adapter.payload,
|
||||
where: whereJoin,
|
||||
})
|
||||
|
||||
const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [
|
||||
{ $match },
|
||||
{
|
||||
$sort: { [sortProperty]: sortDirection },
|
||||
},
|
||||
]
|
||||
|
||||
if (limitJoin > 0) {
|
||||
pipeline.push({
|
||||
$limit: limitJoin + 1,
|
||||
})
|
||||
}
|
||||
|
||||
if (adapter.payload.config.localization && locale === 'all') {
|
||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
||||
const as = `${join.schemaPath}${code}`
|
||||
|
||||
aggregate.push(
|
||||
{
|
||||
$lookup: {
|
||||
as: `${as}.docs`,
|
||||
foreignField: `${join.field.on}${code}`,
|
||||
from: slug,
|
||||
localField: '_id',
|
||||
pipeline,
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
[`${as}.docs`]: {
|
||||
$map: {
|
||||
as: 'doc',
|
||||
in: '$$doc._id',
|
||||
input: `$${as}.docs`,
|
||||
},
|
||||
}, // Slicing the docs to match the limit
|
||||
[`${as}.hasNextPage`]: {
|
||||
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
|
||||
}, // Boolean indicating if more docs than limit
|
||||
},
|
||||
},
|
||||
)
|
||||
if (limitJoin > 0) {
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
[`${as}.docs`]: {
|
||||
$slice: [`$${as}.docs`, limitJoin],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const localeSuffix =
|
||||
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
|
||||
const as = `${join.schemaPath}${localeSuffix}`
|
||||
|
||||
aggregate.push(
|
||||
{
|
||||
$lookup: {
|
||||
as: `${as}.docs`,
|
||||
foreignField: `${join.field.on}${localeSuffix}`,
|
||||
from: slug,
|
||||
localField: '_id',
|
||||
pipeline,
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
[`${as}.docs`]: {
|
||||
$map: {
|
||||
as: 'doc',
|
||||
in: '$$doc._id',
|
||||
input: `$${as}.docs`,
|
||||
},
|
||||
}, // Slicing the docs to match the limit
|
||||
[`${as}.hasNextPage`]: {
|
||||
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
|
||||
}, // Boolean indicating if more docs than limit
|
||||
},
|
||||
},
|
||||
)
|
||||
if (limitJoin > 0) {
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
[`${as}.docs`]: {
|
||||
$slice: [`$${as}.docs`, limitJoin],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aggregate
|
||||
}
|
||||
140
packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts
Normal file
140
packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
import { traverseFields } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
data: Record<string, unknown>
|
||||
fields: Field[]
|
||||
}
|
||||
|
||||
interface RelationObject {
|
||||
relationTo: string
|
||||
value: number | string
|
||||
}
|
||||
|
||||
function isValidRelationObject(value: unknown): value is RelationObject {
|
||||
return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value
|
||||
}
|
||||
|
||||
const convertValue = ({
|
||||
relatedCollection,
|
||||
value,
|
||||
}: {
|
||||
relatedCollection: CollectionConfig
|
||||
value: number | string
|
||||
}): mongoose.Types.ObjectId | number | string => {
|
||||
const customIDField = relatedCollection.fields.find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
)
|
||||
|
||||
if (!customIDField) {
|
||||
return new mongoose.Types.ObjectId(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const sanitizeRelationship = ({ config, field, locale, ref, value }) => {
|
||||
let relatedCollection: CollectionConfig | undefined
|
||||
let result = value
|
||||
|
||||
const hasManyRelations = typeof field.relationTo !== 'string'
|
||||
|
||||
if (!hasManyRelations) {
|
||||
relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo)
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
result = value.map((val) => {
|
||||
// Handle has many
|
||||
if (relatedCollection && val && (typeof val === 'string' || typeof val === 'number')) {
|
||||
return convertValue({
|
||||
relatedCollection,
|
||||
value: val,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle has many - polymorphic
|
||||
if (isValidRelationObject(val)) {
|
||||
const relatedCollectionForSingleValue = config.collections?.find(
|
||||
({ slug }) => slug === val.relationTo,
|
||||
)
|
||||
|
||||
if (relatedCollectionForSingleValue) {
|
||||
return {
|
||||
relationTo: val.relationTo,
|
||||
value: convertValue({
|
||||
relatedCollection: relatedCollectionForSingleValue,
|
||||
value: val.value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return val
|
||||
})
|
||||
}
|
||||
|
||||
// Handle has one - polymorphic
|
||||
if (isValidRelationObject(value)) {
|
||||
relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo)
|
||||
|
||||
if (relatedCollection) {
|
||||
result = {
|
||||
relationTo: value.relationTo,
|
||||
value: convertValue({ relatedCollection, value: value.value }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle has one
|
||||
if (relatedCollection && value && (typeof value === 'string' || typeof value === 'number')) {
|
||||
result = convertValue({
|
||||
relatedCollection,
|
||||
value,
|
||||
})
|
||||
}
|
||||
if (locale) {
|
||||
ref[locale] = result
|
||||
} else {
|
||||
ref[field.name] = result
|
||||
}
|
||||
}
|
||||
|
||||
export const sanitizeRelationshipIDs = ({
|
||||
config,
|
||||
data,
|
||||
fields,
|
||||
}: Args): Record<string, unknown> => {
|
||||
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
// handle localized relationships
|
||||
if (config.localization && field.localized) {
|
||||
const locales = config.localization.locales
|
||||
const fieldRef = ref[field.name]
|
||||
for (const { code } of locales) {
|
||||
if (ref[field.name]?.[code]) {
|
||||
const value = ref[field.name][code]
|
||||
sanitizeRelationship({ config, field, locale: code, ref: fieldRef, value })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// handle non-localized relationships
|
||||
sanitizeRelationship({
|
||||
config,
|
||||
field,
|
||||
locale: undefined,
|
||||
ref,
|
||||
value: ref[field.name],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({ callback: sanitize, fields, ref: data })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type { MongooseAdapter } from './index.js'
|
||||
export async function withSession(
|
||||
db: MongooseAdapter,
|
||||
req: PayloadRequest,
|
||||
): Promise<{ session: ClientSession } | object> {
|
||||
): Promise<{ session: ClientSession } | Record<string, never>> {
|
||||
let transactionID = req.transactionID
|
||||
|
||||
if (transactionID instanceof Promise) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.107",
|
||||
"version": "3.0.0-beta.108",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -118,7 +118,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
const versionsTableName = adapter.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(collection)
|
||||
const versionFields = buildVersionCollectionFields(payload.config, collection)
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
@@ -191,7 +191,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
`_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const versionFields = buildVersionGlobalFields(global)
|
||||
const versionFields = buildVersionGlobalFields(payload.config, global)
|
||||
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.107",
|
||||
"version": "3.0.0-beta.108",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ChainedMethods } from '@payloadcms/drizzle/types'
|
||||
|
||||
import { chainMethods } from '@payloadcms/drizzle'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { CountDistinct, SQLiteAdapter } from './types.js'
|
||||
|
||||
@@ -22,8 +22,11 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql<number>`count
|
||||
(DISTINCT ${this.tables[tableName].id})`,
|
||||
count:
|
||||
joins.length > 0
|
||||
? sql`count
|
||||
(DISTINCT ${this.tables[tableName].id})`.mapWith(Number)
|
||||
: count(),
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
|
||||
@@ -37,6 +37,7 @@ export const init: Init = function init(this: SQLiteAdapter) {
|
||||
})
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const config = this.payload.config
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {}
|
||||
|
||||
@@ -51,6 +52,17 @@ export const init: Init = function init(this: SQLiteAdapter) {
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.upload.filenameCompoundIndex) {
|
||||
const indexName = `${tableName}_filename_compound_idx`
|
||||
|
||||
baseExtraConfig.filename_compound_index = (cols) => {
|
||||
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
|
||||
return cols[f]
|
||||
})
|
||||
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
@@ -66,7 +78,7 @@ export const init: Init = function init(this: SQLiteAdapter) {
|
||||
const versionsTableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(collection)
|
||||
const versionFields = buildVersionCollectionFields(config, collection)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
@@ -105,7 +117,8 @@ export const init: Init = function init(this: SQLiteAdapter) {
|
||||
versions: true,
|
||||
versionsCustomName: true,
|
||||
})
|
||||
const versionFields = buildVersionGlobalFields(global)
|
||||
const config = this.payload.config
|
||||
const versionFields = buildVersionGlobalFields(config, global)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
|
||||
@@ -35,7 +35,15 @@ export type BaseExtraConfig = Record<
|
||||
}) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
|
||||
>
|
||||
|
||||
export type RelationMap = Map<string, { localized: boolean; target: string; type: 'many' | 'one' }>
|
||||
export type RelationMap = Map<
|
||||
string,
|
||||
{
|
||||
localized: boolean
|
||||
relationName?: string
|
||||
target: string
|
||||
type: 'many' | 'one'
|
||||
}
|
||||
>
|
||||
|
||||
type Args = {
|
||||
adapter: SQLiteAdapter
|
||||
@@ -144,9 +152,9 @@ export const buildTable = ({
|
||||
const localizedRelations = new Map()
|
||||
const nonLocalizedRelations = new Map()
|
||||
|
||||
relationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
relationsToBuild.forEach(({ type, localized, relationName, target }, key) => {
|
||||
const map = localized ? localizedRelations : nonLocalizedRelations
|
||||
map.set(key, { type, target })
|
||||
map.set(key, { type, relationName, target })
|
||||
})
|
||||
|
||||
if (timestamps) {
|
||||
@@ -458,7 +466,7 @@ export const buildTable = ({
|
||||
adapter.relations[`relations_${tableName}`] = relations(table, ({ many, one }) => {
|
||||
const result: Record<string, Relation<string>> = {}
|
||||
|
||||
nonLocalizedRelations.forEach(({ type, target }, key) => {
|
||||
nonLocalizedRelations.forEach(({ type, relationName, target }, key) => {
|
||||
if (type === 'one') {
|
||||
result[key] = one(adapter.tables[target], {
|
||||
fields: [table[key]],
|
||||
@@ -467,7 +475,7 @@ export const buildTable = ({
|
||||
})
|
||||
}
|
||||
if (type === 'many') {
|
||||
result[key] = many(adapter.tables[target], { relationName: key })
|
||||
result[key] = many(adapter.tables[target], { relationName: relationName || key })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
text,
|
||||
} from 'drizzle-orm/sqlite-core'
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import { fieldAffectsData, optionIsObject } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { GenericColumns, IDType, SQLiteAdapter } from '../types.js'
|
||||
@@ -113,6 +113,11 @@ export const traverseFields = ({
|
||||
if ('name' in field && field.name === 'id') {
|
||||
return
|
||||
}
|
||||
|
||||
if (fieldIsVirtual(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
let columnName: string
|
||||
let fieldName: string
|
||||
|
||||
@@ -153,7 +158,7 @@ export const traverseFields = ({
|
||||
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
|
||||
}
|
||||
targetIndexes[`${newTableName}_${field.name}Idx`] = createIndex({
|
||||
name: fieldName,
|
||||
name: field.localized ? [fieldName, '_locale'] : fieldName,
|
||||
columnName,
|
||||
tableName: newTableName,
|
||||
unique,
|
||||
@@ -893,6 +898,21 @@ export const traverseFields = ({
|
||||
|
||||
break
|
||||
|
||||
case 'join': {
|
||||
// fieldName could be 'posts' or 'group_posts'
|
||||
// using on as the key for the relation
|
||||
const localized = adapter.payload.config.localization && field.localized
|
||||
const target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}`
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// joins are not localized on the parent table
|
||||
localized: false,
|
||||
relationName: toSnakeCase(field.on),
|
||||
target,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.0.0-beta.107",
|
||||
"version": "3.0.0-beta.108",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -118,7 +118,8 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
const versionsTableName = adapter.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(collection)
|
||||
|
||||
const versionFields = buildVersionCollectionFields(payload.config, collection)
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
@@ -191,7 +192,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
`_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const versionFields = buildVersionGlobalFields(global)
|
||||
const versionFields = buildVersionGlobalFields(payload.config, global)
|
||||
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.107",
|
||||
"version": "3.0.0-beta.108",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const count: Count = async function count(
|
||||
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
|
||||
const { joins, where } = await buildQuery({
|
||||
const { joins, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collectionConfig.fields,
|
||||
locale,
|
||||
|
||||
@@ -10,7 +10,14 @@ import { upsertRow } from './upsertRow/index.js'
|
||||
|
||||
export async function createGlobalVersion<T extends TypeWithID>(
|
||||
this: DrizzleAdapter,
|
||||
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
|
||||
{
|
||||
autosave,
|
||||
globalSlug,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
snapshot,
|
||||
versionData,
|
||||
}: CreateGlobalVersionArgs,
|
||||
) {
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
|
||||
@@ -22,10 +29,12 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
data: {
|
||||
autosave,
|
||||
latest: true,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
version: versionData,
|
||||
},
|
||||
db,
|
||||
fields: buildVersionGlobalFields(global),
|
||||
fields: buildVersionGlobalFields(this.payload.config, global),
|
||||
operation: 'create',
|
||||
req,
|
||||
tableName,
|
||||
|
||||
@@ -14,7 +14,9 @@ export async function createVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
collectionSlug,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
snapshot,
|
||||
versionData,
|
||||
}: CreateVersionArgs<T>,
|
||||
) {
|
||||
@@ -33,6 +35,8 @@ export async function createVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
version,
|
||||
}
|
||||
|
||||
@@ -44,7 +48,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: buildVersionCollectionFields(collection),
|
||||
fields: buildVersionCollectionFields(this.payload.config, collection),
|
||||
operation: 'create',
|
||||
req,
|
||||
tableName,
|
||||
@@ -56,11 +60,11 @@ export async function createVersion<T extends TypeWithID>(
|
||||
await this.execute({
|
||||
db,
|
||||
sql: sql`
|
||||
UPDATE ${table}
|
||||
SET latest = false
|
||||
WHERE ${table.id} != ${result.id}
|
||||
AND ${table.parent} = ${parent}
|
||||
`,
|
||||
UPDATE ${table}
|
||||
SET latest = false
|
||||
WHERE ${table.id} != ${result.id}
|
||||
AND ${table.parent} = ${parent}
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { transform } from './transform/read/index.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
|
||||
{ collection: collectionSlug, joins: joinQuery, req = {} as PayloadRequest, where: whereArg },
|
||||
) {
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -21,7 +21,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
|
||||
let docToDelete: Record<string, unknown>
|
||||
|
||||
const { joins, selectFields, where } = await buildQuery({
|
||||
const { joins, selectFields, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.fields,
|
||||
locale: req.locale,
|
||||
@@ -48,6 +48,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
adapter: this,
|
||||
depth: 0,
|
||||
fields: collection.fields,
|
||||
joinQuery,
|
||||
tableName,
|
||||
})
|
||||
|
||||
@@ -61,6 +62,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
config: this.payload.config,
|
||||
data: docToDelete,
|
||||
fields: collection.fields,
|
||||
joinQuery,
|
||||
})
|
||||
|
||||
await this.deleteWhere({
|
||||
|
||||
@@ -19,7 +19,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const fields = buildVersionCollectionFields(collectionConfig)
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
|
||||
|
||||
const { docs } = await findMany({
|
||||
adapter: this,
|
||||
|
||||
@@ -10,6 +10,7 @@ export const find: Find = async function find(
|
||||
this: DrizzleAdapter,
|
||||
{
|
||||
collection,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
page = 1,
|
||||
@@ -27,6 +28,7 @@ export const find: Find = async function find(
|
||||
return findMany({
|
||||
adapter: this,
|
||||
fields: collectionConfig.fields,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DBQueryConfig } from 'drizzle-orm'
|
||||
import type { Field } from 'payload'
|
||||
import type { Field, JoinQuery } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
@@ -9,6 +9,12 @@ type BuildFindQueryArgs = {
|
||||
adapter: DrizzleAdapter
|
||||
depth: number
|
||||
fields: Field[]
|
||||
joinQuery?: JoinQuery
|
||||
/**
|
||||
* The joins array will be mutated by pushing any joins needed for the where queries of join field joins
|
||||
*/
|
||||
joins?: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
tableName: string
|
||||
}
|
||||
|
||||
@@ -24,6 +30,9 @@ export const buildFindManyArgs = ({
|
||||
adapter,
|
||||
depth,
|
||||
fields,
|
||||
joinQuery,
|
||||
joins = [],
|
||||
locale,
|
||||
tableName,
|
||||
}: BuildFindQueryArgs): Record<string, unknown> => {
|
||||
const result: Result = {
|
||||
@@ -79,6 +88,9 @@ export const buildFindManyArgs = ({
|
||||
currentTableName: tableName,
|
||||
depth,
|
||||
fields,
|
||||
joinQuery,
|
||||
joins,
|
||||
locale,
|
||||
path: '',
|
||||
tablePath: '',
|
||||
topLevelArgs: result,
|
||||
|
||||
@@ -19,6 +19,7 @@ type Args = {
|
||||
export const findMany = async function find({
|
||||
adapter,
|
||||
fields,
|
||||
joins: joinQuery,
|
||||
limit: limitArg,
|
||||
locale,
|
||||
page = 1,
|
||||
@@ -42,7 +43,7 @@ export const findMany = async function find({
|
||||
limit = undefined
|
||||
}
|
||||
|
||||
const { joins, orderBy, selectFields, where } = await buildQuery({
|
||||
const { joins, orderBy, selectFields, where } = buildQuery({
|
||||
adapter,
|
||||
fields,
|
||||
locale,
|
||||
@@ -67,6 +68,8 @@ export const findMany = async function find({
|
||||
adapter,
|
||||
depth: 0,
|
||||
fields,
|
||||
joinQuery,
|
||||
joins,
|
||||
tableName,
|
||||
})
|
||||
|
||||
@@ -151,6 +154,7 @@ export const findMany = async function find({
|
||||
config: adapter.payload.config,
|
||||
data,
|
||||
fields,
|
||||
joinQuery,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { Field } from 'payload'
|
||||
import type { DBQueryConfig } from 'drizzle-orm'
|
||||
import type { Field, JoinQuery } from 'payload'
|
||||
|
||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
|
||||
import type { Result } from './buildFindManyArgs.js'
|
||||
|
||||
import { buildOrderBy } from '../queries/buildOrderBy.js'
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
|
||||
type TraverseFieldArgs = {
|
||||
_locales: Result
|
||||
adapter: DrizzleAdapter
|
||||
@@ -13,6 +17,9 @@ type TraverseFieldArgs = {
|
||||
currentTableName: string
|
||||
depth?: number
|
||||
fields: Field[]
|
||||
joinQuery: JoinQuery
|
||||
joins?: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
path: string
|
||||
tablePath: string
|
||||
topLevelArgs: Record<string, unknown>
|
||||
@@ -26,12 +33,19 @@ export const traverseFields = ({
|
||||
currentTableName,
|
||||
depth,
|
||||
fields,
|
||||
joinQuery = {},
|
||||
joins,
|
||||
locale,
|
||||
path,
|
||||
tablePath,
|
||||
topLevelArgs,
|
||||
topLevelTableName,
|
||||
}: TraverseFieldArgs) => {
|
||||
fields.forEach((field) => {
|
||||
if (fieldIsVirtual(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
// handle simple relationship
|
||||
if (
|
||||
depth > 0 &&
|
||||
@@ -54,6 +68,8 @@ export const traverseFields = ({
|
||||
currentTableName,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
joinQuery,
|
||||
joins,
|
||||
path,
|
||||
tablePath,
|
||||
topLevelArgs,
|
||||
@@ -75,6 +91,8 @@ export const traverseFields = ({
|
||||
currentTableName,
|
||||
depth,
|
||||
fields: tab.fields,
|
||||
joinQuery,
|
||||
joins,
|
||||
path: tabPath,
|
||||
tablePath: tabTablePath,
|
||||
topLevelArgs,
|
||||
@@ -120,6 +138,7 @@ export const traverseFields = ({
|
||||
currentTableName: arrayTableName,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
joinQuery,
|
||||
path: '',
|
||||
tablePath: '',
|
||||
topLevelArgs,
|
||||
@@ -177,6 +196,7 @@ export const traverseFields = ({
|
||||
currentTableName: tableName,
|
||||
depth,
|
||||
fields: block.fields,
|
||||
joinQuery,
|
||||
path: '',
|
||||
tablePath: '',
|
||||
topLevelArgs,
|
||||
@@ -195,6 +215,8 @@ export const traverseFields = ({
|
||||
currentTableName,
|
||||
depth,
|
||||
fields: field.fields,
|
||||
joinQuery,
|
||||
joins,
|
||||
path: `${path}${field.name}_`,
|
||||
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
|
||||
topLevelArgs,
|
||||
@@ -204,6 +226,67 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
case 'join': {
|
||||
// when `joinsQuery` is false, do not join
|
||||
if (joinQuery === false) {
|
||||
break
|
||||
}
|
||||
const {
|
||||
limit: limitArg = 10,
|
||||
sort,
|
||||
where,
|
||||
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
|
||||
let limit = limitArg
|
||||
if (limit !== 0) {
|
||||
// get an additional document and slice it later to determine if there is a next page
|
||||
limit += 1
|
||||
}
|
||||
const fields = adapter.payload.collections[field.collection].config.fields
|
||||
const joinTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${
|
||||
field.localized && adapter.payload.config.localization ? adapter.localesSuffix : ''
|
||||
}`
|
||||
const selectFields = {}
|
||||
|
||||
const orderBy = buildOrderBy({
|
||||
adapter,
|
||||
fields,
|
||||
joins: [],
|
||||
locale,
|
||||
selectFields,
|
||||
sort,
|
||||
tableName: joinTableName,
|
||||
})
|
||||
const withJoin: DBQueryConfig<'many', true, any, any> = {
|
||||
columns: selectFields,
|
||||
orderBy: () => [orderBy.order(orderBy.column)],
|
||||
}
|
||||
if (limit) {
|
||||
withJoin.limit = limit
|
||||
}
|
||||
|
||||
if (field.localized) {
|
||||
withJoin.columns._locale = true
|
||||
withJoin.columns._parentID = true
|
||||
} else {
|
||||
withJoin.columns.id = true
|
||||
}
|
||||
|
||||
if (where) {
|
||||
const { where: joinWhere } = buildQuery({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
sort,
|
||||
tableName: joinTableName,
|
||||
where,
|
||||
})
|
||||
withJoin.where = () => joinWhere
|
||||
}
|
||||
currentArgs.with[`${path.replaceAll('.', '_')}${field.name}`] = withJoin
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const fields = buildVersionGlobalFields(globalConfig)
|
||||
const fields = buildVersionGlobalFields(this.payload.config, globalConfig)
|
||||
|
||||
return findMany({
|
||||
adapter: this,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { findMany } from './find/findMany.js'
|
||||
|
||||
export async function findOne<T extends TypeWithID>(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
|
||||
{ collection, joins, locale, req = {} as PayloadRequest, where }: FindOneArgs,
|
||||
): Promise<T> {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
@@ -17,6 +17,7 @@ export async function findOne<T extends TypeWithID>(
|
||||
const { docs } = await findMany({
|
||||
adapter: this,
|
||||
fields: collectionConfig.fields,
|
||||
joins,
|
||||
limit: 1,
|
||||
locale,
|
||||
page: 1,
|
||||
|
||||
@@ -28,7 +28,7 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const fields = buildVersionCollectionFields(collectionConfig)
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
|
||||
|
||||
return findMany({
|
||||
adapter: this,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { ChainedMethods, TransactionPg } from '../types.js'
|
||||
import type { BasePostgresAdapter, CountDistinct } from './types.js'
|
||||
@@ -22,8 +22,11 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
methods: chainedMethods,
|
||||
query: (db as TransactionPg)
|
||||
.select({
|
||||
count: sql<string>`count
|
||||
(DISTINCT ${this.tables[tableName].id})`,
|
||||
count:
|
||||
joins.length > 0
|
||||
? sql`count
|
||||
(DISTINCT ${this.tables[tableName].id})`.mapWith(Number)
|
||||
: count(),
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
|
||||
@@ -63,7 +63,7 @@ export const init: Init = function init(this: BasePostgresAdapter) {
|
||||
const versionsTableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(collection)
|
||||
const versionFields = buildVersionCollectionFields(this.payload.config, collection)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
@@ -97,7 +97,7 @@ export const init: Init = function init(this: BasePostgresAdapter) {
|
||||
versions: true,
|
||||
versionsCustomName: true,
|
||||
})
|
||||
const versionFields = buildVersionGlobalFields(global)
|
||||
const versionFields = buildVersionGlobalFields(this.payload.config, global)
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
|
||||
@@ -138,9 +138,9 @@ export const buildTable = ({
|
||||
const localizedRelations = new Map()
|
||||
const nonLocalizedRelations = new Map()
|
||||
|
||||
relationsToBuild.forEach(({ type, localized, target }, key) => {
|
||||
relationsToBuild.forEach(({ type, localized, relationName, target }, key) => {
|
||||
const map = localized ? localizedRelations : nonLocalizedRelations
|
||||
map.set(key, { type, target })
|
||||
map.set(key, { type, relationName, target })
|
||||
})
|
||||
|
||||
if (timestamps) {
|
||||
@@ -444,7 +444,7 @@ export const buildTable = ({
|
||||
adapter.relations[`relations_${tableName}`] = relations(table, ({ many, one }) => {
|
||||
const result: Record<string, Relation<string>> = {}
|
||||
|
||||
nonLocalizedRelations.forEach(({ type, target }, key) => {
|
||||
nonLocalizedRelations.forEach(({ type, relationName, target }, key) => {
|
||||
if (type === 'one') {
|
||||
result[key] = one(adapter.tables[target], {
|
||||
fields: [table[key]],
|
||||
@@ -453,7 +453,7 @@ export const buildTable = ({
|
||||
})
|
||||
}
|
||||
if (type === 'many') {
|
||||
result[key] = many(adapter.tables[target], { relationName: key })
|
||||
result[key] = many(adapter.tables[target], { relationName: relationName || key })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import { fieldAffectsData, optionIsObject } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type {
|
||||
@@ -119,6 +119,10 @@ export const traverseFields = ({
|
||||
if ('name' in field && field.name === 'id') {
|
||||
return
|
||||
}
|
||||
if (fieldIsVirtual(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
let columnName: string
|
||||
let fieldName: string
|
||||
|
||||
@@ -159,7 +163,7 @@ export const traverseFields = ({
|
||||
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
|
||||
}
|
||||
targetIndexes[`${newTableName}_${field.name}Idx`] = createIndex({
|
||||
name: fieldName,
|
||||
name: field.localized ? [fieldName, '_locale'] : fieldName,
|
||||
columnName,
|
||||
tableName: newTableName,
|
||||
unique,
|
||||
@@ -902,6 +906,21 @@ export const traverseFields = ({
|
||||
|
||||
break
|
||||
|
||||
case 'join': {
|
||||
// fieldName could be 'posts' or 'group_posts'
|
||||
// using on as the key for the relation
|
||||
const localized = adapter.payload.config.localization && field.localized
|
||||
const target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}`
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// joins are not localized on the parent table
|
||||
localized: false,
|
||||
relationName: toSnakeCase(field.on),
|
||||
target,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -31,7 +31,15 @@ export type BaseExtraConfig = Record<
|
||||
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
|
||||
>
|
||||
|
||||
export type RelationMap = Map<string, { localized: boolean; target: string; type: 'many' | 'one' }>
|
||||
export type RelationMap = Map<
|
||||
string,
|
||||
{
|
||||
localized: boolean
|
||||
relationName?: string
|
||||
target: string
|
||||
type: 'many' | 'one'
|
||||
}
|
||||
>
|
||||
|
||||
export type GenericColumn = PgColumn<
|
||||
ColumnBaseConfig<ColumnDataType, string>,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
export async function buildAndOrConditions({
|
||||
export function buildAndOrConditions({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
@@ -24,7 +24,7 @@ export async function buildAndOrConditions({
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
where: Where[]
|
||||
}): Promise<SQL[]> {
|
||||
}): SQL[] {
|
||||
const completedConditions = []
|
||||
// Loop over all AND / OR operations and add them to the AND / OR query param
|
||||
// Operations should come through as an array
|
||||
@@ -32,7 +32,7 @@ export async function buildAndOrConditions({
|
||||
for (const condition of where) {
|
||||
// If the operation is properly formatted as an object
|
||||
if (typeof condition === 'object') {
|
||||
const result = await parseParams({
|
||||
const result = parseParams({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
|
||||
82
packages/drizzle/src/queries/buildOrderBy.ts
Normal file
82
packages/drizzle/src/queries/buildOrderBy.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { asc, desc } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, BuildQueryResult } from './buildQuery.js'
|
||||
|
||||
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
fields: Field[]
|
||||
joins: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
selectFields: Record<string, GenericColumn>
|
||||
sort?: string
|
||||
tableName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the order by column and direction constructed from the sort argument adds the column to the select fields and joins if necessary
|
||||
*/
|
||||
export const buildOrderBy = ({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
selectFields,
|
||||
sort,
|
||||
tableName,
|
||||
}: Args): BuildQueryResult['orderBy'] => {
|
||||
const orderBy: BuildQueryResult['orderBy'] = {
|
||||
column: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
let sortPath
|
||||
|
||||
if (sort[0] === '-') {
|
||||
sortPath = sort.substring(1)
|
||||
orderBy.order = desc
|
||||
} else {
|
||||
sortPath = sort
|
||||
orderBy.order = asc
|
||||
}
|
||||
|
||||
try {
|
||||
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
|
||||
adapter,
|
||||
collectionPath: sortPath,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: sortPath.replace(/__/g, '.').split('.'),
|
||||
selectFields,
|
||||
tableName,
|
||||
value: sortPath,
|
||||
})
|
||||
orderBy.column = sortTable?.[sortTableColumnName]
|
||||
} catch (err) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
if (!orderBy?.column) {
|
||||
orderBy.order = desc
|
||||
const createdAt = adapter.tables[tableName]?.createdAt
|
||||
|
||||
if (createdAt) {
|
||||
orderBy.column = createdAt
|
||||
} else {
|
||||
orderBy.column = adapter.tables[tableName].id
|
||||
}
|
||||
}
|
||||
|
||||
if (orderBy.column) {
|
||||
selectFields.sort = orderBy.column
|
||||
}
|
||||
|
||||
return orderBy
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { asc, desc, SQL } from 'drizzle-orm'
|
||||
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
import type { Field, Where } from 'payload'
|
||||
|
||||
import { asc, desc } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleAdapter, GenericColumn, GenericTable } from '../types.js'
|
||||
|
||||
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
|
||||
import { buildOrderBy } from './buildOrderBy.js'
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
export type BuildQueryJoinAliases = {
|
||||
@@ -17,13 +15,14 @@ export type BuildQueryJoinAliases = {
|
||||
type BuildQueryArgs = {
|
||||
adapter: DrizzleAdapter
|
||||
fields: Field[]
|
||||
joins?: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
sort?: string
|
||||
tableName: string
|
||||
where: Where
|
||||
}
|
||||
|
||||
type Result = {
|
||||
export type BuildQueryResult = {
|
||||
joins: BuildQueryJoinAliases
|
||||
orderBy: {
|
||||
column: GenericColumn
|
||||
@@ -32,72 +31,33 @@ type Result = {
|
||||
selectFields: Record<string, GenericColumn>
|
||||
where: SQL
|
||||
}
|
||||
const buildQuery = async function buildQuery({
|
||||
const buildQuery = function buildQuery({
|
||||
adapter,
|
||||
fields,
|
||||
joins = [],
|
||||
locale,
|
||||
sort,
|
||||
tableName,
|
||||
where: incomingWhere,
|
||||
}: BuildQueryArgs): Promise<Result> {
|
||||
}: BuildQueryArgs): BuildQueryResult {
|
||||
const selectFields: Record<string, GenericColumn> = {
|
||||
id: adapter.tables[tableName].id,
|
||||
}
|
||||
const joins: BuildQueryJoinAliases = []
|
||||
|
||||
const orderBy: Result['orderBy'] = {
|
||||
column: null,
|
||||
order: null,
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
let sortPath
|
||||
|
||||
if (sort[0] === '-') {
|
||||
sortPath = sort.substring(1)
|
||||
orderBy.order = desc
|
||||
} else {
|
||||
sortPath = sort
|
||||
orderBy.order = asc
|
||||
}
|
||||
|
||||
try {
|
||||
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
|
||||
adapter,
|
||||
collectionPath: sortPath,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: sortPath.replace(/__/g, '.').split('.'),
|
||||
selectFields,
|
||||
tableName,
|
||||
value: sortPath,
|
||||
})
|
||||
orderBy.column = sortTable?.[sortTableColumnName]
|
||||
} catch (err) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
if (!orderBy?.column) {
|
||||
orderBy.order = desc
|
||||
const createdAt = adapter.tables[tableName]?.createdAt
|
||||
|
||||
if (createdAt) {
|
||||
orderBy.column = createdAt
|
||||
} else {
|
||||
orderBy.column = adapter.tables[tableName].id
|
||||
}
|
||||
}
|
||||
|
||||
if (orderBy.column) {
|
||||
selectFields.sort = orderBy.column
|
||||
}
|
||||
const orderBy = buildOrderBy({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
locale,
|
||||
selectFields,
|
||||
sort,
|
||||
tableName,
|
||||
})
|
||||
|
||||
let where: SQL
|
||||
|
||||
if (incomingWhere && Object.keys(incomingWhere).length > 0) {
|
||||
where = await parseParams({
|
||||
where = parseParams({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { validate as uuidValidate } from 'uuid'
|
||||
import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
|
||||
import { getTableAlias } from './getTableAlias.js'
|
||||
|
||||
type Constraint = {
|
||||
@@ -23,7 +24,7 @@ type Constraint = {
|
||||
type TableColumn = {
|
||||
columnName?: string
|
||||
columns?: {
|
||||
idType: 'number' | 'text'
|
||||
idType: 'number' | 'text' | 'uuid'
|
||||
rawColumn: SQL<unknown>
|
||||
}[]
|
||||
constraints: Constraint[]
|
||||
@@ -521,7 +522,8 @@ export const getTableColumnFromPath = ({
|
||||
|
||||
const columns: TableColumn['columns'] = field.relationTo
|
||||
.map((relationTo) => {
|
||||
let idType: 'number' | 'text' = adapter.idType === 'uuid' ? 'text' : 'number'
|
||||
let idType: 'number' | 'text' | 'uuid' =
|
||||
adapter.idType === 'uuid' ? 'uuid' : 'number'
|
||||
|
||||
const { customIDType } = adapter.payload.collections[relationTo]
|
||||
|
||||
@@ -529,9 +531,19 @@ export const getTableColumnFromPath = ({
|
||||
idType = customIDType
|
||||
}
|
||||
|
||||
const idTypeTextOrUuid = idType === 'text' || idType === 'uuid'
|
||||
|
||||
// Do not add the column to OR if we know that it can't match by the type
|
||||
// We can't do the same with idType: 'number' because `value` can be from the REST search query params
|
||||
if (typeof value === 'number' && idType === 'text') {
|
||||
if (typeof value === 'number' && idTypeTextOrUuid) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.every((val) => typeof val === 'number') &&
|
||||
idTypeTextOrUuid
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -540,8 +552,8 @@ export const getTableColumnFromPath = ({
|
||||
// We need this because Postgres throws an error if querying by UUID column with a value that isn't a valid UUID.
|
||||
if (
|
||||
value &&
|
||||
!customIDType &&
|
||||
adapter.idType === 'uuid' &&
|
||||
!Array.isArray(value) &&
|
||||
idType === 'uuid' &&
|
||||
hasCustomCollectionWithCustomID
|
||||
) {
|
||||
if (!uuidValidate(value)) {
|
||||
@@ -549,6 +561,15 @@ export const getTableColumnFromPath = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
idType === 'uuid' &&
|
||||
hasCustomCollectionWithCustomID &&
|
||||
!value.some((val) => uuidValidate(val))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const relationTableName = adapter.tableNameMap.get(
|
||||
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
|
||||
)
|
||||
@@ -583,6 +604,19 @@ export const getTableColumnFromPath = ({
|
||||
},
|
||||
table: aliasRelationshipTable,
|
||||
}
|
||||
} else if (isPolymorphicRelationship(value)) {
|
||||
const { relationTo } = value
|
||||
|
||||
const relationTableName = adapter.tableNameMap.get(
|
||||
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
|
||||
)
|
||||
|
||||
return {
|
||||
constraints,
|
||||
field,
|
||||
rawColumn: sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`),
|
||||
table: aliasRelationshipTable,
|
||||
}
|
||||
} else {
|
||||
throw new APIError('Not supported')
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type Args = {
|
||||
where: Where
|
||||
}
|
||||
|
||||
export async function parseParams({
|
||||
export function parseParams({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
@@ -30,7 +30,7 @@ export async function parseParams({
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
}: Args): Promise<SQL> {
|
||||
}: Args): SQL {
|
||||
let result: SQL
|
||||
const constraints: SQL[] = []
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function parseParams({
|
||||
conditionOperator = or
|
||||
}
|
||||
if (Array.isArray(condition)) {
|
||||
const builtConditions = await buildAndOrConditions({
|
||||
const builtConditions = buildAndOrConditions({
|
||||
adapter,
|
||||
fields,
|
||||
joins,
|
||||
|
||||
@@ -2,13 +2,17 @@ import type { SQL } from 'drizzle-orm'
|
||||
|
||||
import { APIError, createArrayFromCommaDelineated, type Field, type TabAsField } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import { validate as uuidValidate } from 'uuid'
|
||||
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
|
||||
import { getCollectionIdType } from '../utilities/getCollectionIdType.js'
|
||||
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
|
||||
|
||||
type SanitizeQueryValueArgs = {
|
||||
adapter: DrizzleAdapter
|
||||
columns?: {
|
||||
idType: 'number' | 'text'
|
||||
idType: 'number' | 'text' | 'uuid'
|
||||
rawColumn: SQL<unknown>
|
||||
}[]
|
||||
field: Field | TabAsField
|
||||
@@ -106,28 +110,61 @@ export const sanitizeQueryValue = ({
|
||||
// convert the value to the idType of the relationship
|
||||
let idType: 'number' | 'text'
|
||||
if (typeof field.relationTo === 'string') {
|
||||
const collection = adapter.payload.collections[field.relationTo]
|
||||
const mixedType: 'number' | 'serial' | 'text' | 'uuid' =
|
||||
collection.customIDType || adapter.idType
|
||||
const typeMap: Record<string, 'number' | 'text'> = {
|
||||
number: 'number',
|
||||
serial: 'number',
|
||||
text: 'text',
|
||||
uuid: 'text',
|
||||
}
|
||||
idType = typeMap[mixedType]
|
||||
idType = getCollectionIdType({
|
||||
adapter,
|
||||
collection: adapter.payload.collections[field.relationTo],
|
||||
})
|
||||
} else {
|
||||
if (isPolymorphicRelationship(val)) {
|
||||
if (operator !== 'equals') {
|
||||
throw new APIError(
|
||||
`Only 'equals' operator is supported for polymorphic relationship object notation. Given - ${operator}`,
|
||||
)
|
||||
}
|
||||
idType = getCollectionIdType({
|
||||
adapter,
|
||||
collection: adapter.payload.collections[val.relationTo],
|
||||
})
|
||||
|
||||
return {
|
||||
operator,
|
||||
value: idType === 'number' ? Number(val.value) : String(val.value),
|
||||
}
|
||||
}
|
||||
|
||||
formattedColumns = columns
|
||||
.map(({ idType, rawColumn }) => {
|
||||
let formattedValue: number | string
|
||||
if (idType === 'number') {
|
||||
let formattedValue: number | number[] | string | string[]
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
formattedValue = val
|
||||
.map((eachVal) => {
|
||||
let formattedValue: number | string
|
||||
|
||||
if (idType === 'number') {
|
||||
formattedValue = Number(eachVal)
|
||||
|
||||
if (Number.isNaN(formattedValue)) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
if (idType === 'uuid' && !uuidValidate(eachVal)) {
|
||||
return null
|
||||
}
|
||||
|
||||
formattedValue = String(eachVal)
|
||||
}
|
||||
|
||||
return formattedValue
|
||||
})
|
||||
.filter(Boolean) as number[] | string[]
|
||||
} else if (idType === 'number') {
|
||||
formattedValue = Number(val)
|
||||
|
||||
if (Number.isNaN(formattedValue)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (idType === 'text') {
|
||||
} else {
|
||||
formattedValue = String(val)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const fields = buildVersionCollectionFields(collectionConfig)
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
|
||||
|
||||
const combinedWhere = combineQueries({ latest: { equals: true } }, where)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field, SanitizedConfig, TypeWithID } from 'payload'
|
||||
import type { Field, JoinQuery, SanitizedConfig, TypeWithID } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
|
||||
@@ -12,6 +12,7 @@ type TransformArgs = {
|
||||
data: Record<string, unknown>
|
||||
fallbackLocale?: false | string
|
||||
fields: Field[]
|
||||
joinQuery?: JoinQuery
|
||||
locale?: string
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
|
||||
config,
|
||||
data,
|
||||
fields,
|
||||
joinQuery,
|
||||
}: TransformArgs): T => {
|
||||
let relationships: Record<string, Record<string, unknown>[]> = {}
|
||||
let texts: Record<string, Record<string, unknown>[]> = {}
|
||||
@@ -55,6 +57,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
|
||||
deletions,
|
||||
fieldPrefix: '',
|
||||
fields,
|
||||
joinQuery,
|
||||
numbers,
|
||||
path: '',
|
||||
relationships,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Field, SanitizedConfig, TabAsField } from 'payload'
|
||||
import type { Field, JoinQuery, SanitizedConfig, TabAsField } from 'payload'
|
||||
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||
@@ -38,6 +38,10 @@ type TraverseFieldsArgs = {
|
||||
* An array of Payload fields to traverse
|
||||
*/
|
||||
fields: (Field | TabAsField)[]
|
||||
/**
|
||||
*
|
||||
*/
|
||||
joinQuery?: JoinQuery
|
||||
/**
|
||||
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path
|
||||
*/
|
||||
@@ -74,6 +78,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
deletions,
|
||||
fieldPrefix,
|
||||
fields,
|
||||
joinQuery,
|
||||
numbers,
|
||||
path,
|
||||
relationships,
|
||||
@@ -93,6 +98,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
deletions,
|
||||
fieldPrefix,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
joinQuery,
|
||||
numbers,
|
||||
path,
|
||||
relationships,
|
||||
@@ -115,6 +121,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
deletions,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
joinQuery,
|
||||
numbers,
|
||||
path,
|
||||
relationships,
|
||||
@@ -125,6 +132,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (fieldIsVirtual(field)) {
|
||||
return result
|
||||
}
|
||||
|
||||
const fieldName = `${fieldPrefix || ''}${field.name}`
|
||||
const fieldData = table[fieldName]
|
||||
const localizedFieldData = {}
|
||||
@@ -386,6 +397,44 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'join') {
|
||||
const { limit = 10 } = joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
|
||||
|
||||
let fieldResult:
|
||||
| { docs: unknown[]; hasNextPage: boolean }
|
||||
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
|
||||
if (Array.isArray(fieldData)) {
|
||||
if (field.localized) {
|
||||
fieldResult = fieldData.reduce((joinResult, row) => {
|
||||
if (typeof row._locale === 'string') {
|
||||
if (!joinResult[row._locale]) {
|
||||
joinResult[row._locale] = {
|
||||
docs: [],
|
||||
hasNextPage: false,
|
||||
}
|
||||
}
|
||||
joinResult[row._locale].docs.push(row._parentID)
|
||||
}
|
||||
|
||||
return joinResult
|
||||
}, {})
|
||||
Object.keys(fieldResult).forEach((locale) => {
|
||||
fieldResult[locale].hasNextPage = fieldResult[locale].docs.length > limit
|
||||
fieldResult[locale].docs = fieldResult[locale].docs.slice(0, limit)
|
||||
})
|
||||
} else {
|
||||
const hasNextPage = limit !== 0 && fieldData.length > limit
|
||||
fieldResult = {
|
||||
docs: hasNextPage ? fieldData.slice(0, limit) : fieldData,
|
||||
hasNextPage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result[field.name] = fieldResult
|
||||
return result
|
||||
}
|
||||
|
||||
if (field.type === 'text' && field?.hasMany) {
|
||||
const textPathMatch = texts[`${sanitizedPath}${field.name}`]
|
||||
if (!textPathMatch) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
@@ -93,6 +93,10 @@ export const traverseFields = ({
|
||||
let fieldData: unknown
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (fieldIsVirtual(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}`
|
||||
fieldName = `${fieldPrefix || ''}${field.name}`
|
||||
fieldData = data[field.name]
|
||||
@@ -264,6 +268,10 @@ export const traverseFields = ({
|
||||
if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
if ('name' in tab) {
|
||||
if (fieldIsVirtual(tab)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof data[tab.name] === 'object' && data[tab.name] !== null) {
|
||||
if (tab.localized) {
|
||||
Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { upsertRow } from './upsertRow/index.js'
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
this: DrizzleAdapter,
|
||||
{ id, collection: collectionSlug, data, draft, locale, req, where: whereArg },
|
||||
{ id, collection: collectionSlug, data, draft, joins: joinQuery, locale, req, where: whereArg },
|
||||
) {
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -18,7 +18,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
const whereToUse = whereArg || { id: { equals: id } }
|
||||
let idToUpdate = id
|
||||
|
||||
const { joins, selectFields, where } = await buildQuery({
|
||||
const { joins, selectFields, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.fields,
|
||||
locale,
|
||||
@@ -46,6 +46,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
data,
|
||||
db,
|
||||
fields: collection.fields,
|
||||
joinQuery,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user