Compare commits
114 Commits
feat/conso
...
revert-111
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac46c289a1 | ||
|
|
4c54ccc25a | ||
|
|
67c4a20237 | ||
|
|
38131ed2c3 | ||
|
|
96d1d90e78 | ||
|
|
9e97319c6f | ||
|
|
a65289c211 | ||
|
|
4a1b74952f | ||
|
|
8b55e7b51a | ||
|
|
48e613b61f | ||
|
|
428c133033 | ||
|
|
c8c578f5ef | ||
|
|
d53f166476 | ||
|
|
dfddee2125 | ||
|
|
e055565ca8 | ||
|
|
41c7413f59 | ||
|
|
3709950d50 | ||
|
|
6ce5e8b83b | ||
|
|
f3844ee533 | ||
|
|
c21dac1b53 | ||
|
|
b3e7a9d194 | ||
|
|
c4bc0ae48a | ||
|
|
f7b1cd9d63 | ||
|
|
9c25e7b68e | ||
|
|
1d252cbacf | ||
|
|
7118b6418f | ||
|
|
bdf0113b2f | ||
|
|
3436fb16ea | ||
|
|
bcc68572bf | ||
|
|
6aa9da73f8 | ||
|
|
2a3682ff68 | ||
|
|
958e195017 | ||
|
|
45cee23add | ||
|
|
67b7a730ba | ||
|
|
88a2841500 | ||
|
|
7e713a454a | ||
|
|
b975858e76 | ||
|
|
b540da53ec | ||
|
|
c6ab312286 | ||
|
|
526e535763 | ||
|
|
e4712a822b | ||
|
|
81fd42ef69 | ||
|
|
6b6c289d79 | ||
|
|
69c0d09437 | ||
|
|
48f183bd42 | ||
|
|
36e152d69d | ||
|
|
1e698c2bdf | ||
|
|
7bb1c9d3c6 | ||
|
|
4410a49132 | ||
|
|
820a6ec55d | ||
|
|
0a1af45549 | ||
|
|
09ca5143eb | ||
|
|
f1b005c4f5 | ||
|
|
dc9e8fa655 | ||
|
|
2477fc6c75 | ||
|
|
37781808eb | ||
|
|
d92c0009ed | ||
|
|
d32608649c | ||
|
|
f9121c1a3a | ||
|
|
f477e0e3c4 | ||
|
|
4224c68002 | ||
|
|
b014416584 | ||
|
|
1725af5e3a | ||
|
|
a13d4fe5c6 | ||
|
|
3ffc268438 | ||
|
|
d766b1904c | ||
|
|
f31568c69c | ||
|
|
a8bec9a1b2 | ||
|
|
5d7be15c52 | ||
|
|
0058f82d87 | ||
|
|
6ff380ce59 | ||
|
|
f779e48a58 | ||
|
|
1dc748d341 | ||
|
|
c7c5018675 | ||
|
|
9728d80592 | ||
|
|
0594701004 | ||
|
|
845c647ebc | ||
|
|
f6f6a1dc99 | ||
|
|
ee5e96a965 | ||
|
|
22f61ad79e | ||
|
|
460d50baa3 | ||
|
|
76bd05cc5d | ||
|
|
af92c1562c | ||
|
|
6fad5d7c0a | ||
|
|
c05f10abbc | ||
|
|
26163a7535 | ||
|
|
c517e7e688 | ||
|
|
563c21bec0 | ||
|
|
b1e9aa53ab | ||
|
|
26127567b6 | ||
|
|
f3161f9405 | ||
|
|
e83318b156 | ||
|
|
009e9085fc | ||
|
|
9fc1cd0d24 | ||
|
|
0651ae0727 | ||
|
|
618624e110 | ||
|
|
cd48904798 | ||
|
|
8b0ae902e7 | ||
|
|
80b33adf6b | ||
|
|
9b8f8d70ca | ||
|
|
af5554981c | ||
|
|
7024da8be3 | ||
|
|
acead1083b | ||
|
|
bf103cc025 | ||
|
|
8bbe7bcbbe | ||
|
|
bd8ced1b60 | ||
|
|
132852290a | ||
|
|
7f5aaad6a5 | ||
|
|
38c1c113ca | ||
|
|
7922d66181 | ||
|
|
0d7cf3fca2 | ||
|
|
8166784ba2 | ||
|
|
be52a2a223 | ||
|
|
0824b4f34c |
37
.github/CODEOWNERS
vendored
37
.github/CODEOWNERS
vendored
@@ -1,37 +0,0 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Package Exports
|
||||
|
||||
**/exports/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Packages
|
||||
|
||||
/packages/plugin-cloud*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/live-preview*/src/ @jacobsfletch
|
||||
/packages/plugin-stripe/src/ @jacobsfletch
|
||||
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
|
||||
/packages/richtext-*/src/ @AlessioGr
|
||||
/packages/next/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
|
||||
/packages/ui/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
|
||||
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Templates
|
||||
|
||||
/templates/_data/ @denolfe @jmikrut @DanRibbens
|
||||
/templates/_template/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Build Files
|
||||
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Root
|
||||
|
||||
/package.json @denolfe @jmikrut @DanRibbens
|
||||
/tools/ @denolfe @jmikrut @DanRibbens
|
||||
/.husky/ @denolfe @jmikrut @DanRibbens
|
||||
/.vscode/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
/.github/ @denolfe @jmikrut @DanRibbens
|
||||
2
.github/comments/invalid-reproduction.md
vendored
2
.github/comments/invalid-reproduction.md
vendored
@@ -4,7 +4,7 @@ Depending on the quality of reproduction steps, this issue may be closed if no r
|
||||
|
||||
### Why was this issue marked with the `invalid-reproduction` label?
|
||||
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@latest -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
|
||||
|
||||
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.
|
||||
|
||||
|
||||
@@ -1054,13 +1054,13 @@ import { usePayloadAPI } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// Fetch data from a collection item using its ID
|
||||
const [{ data, error, isLoading }, { setParams }] = usePayloadAPI(
|
||||
const [{ data, isError, isLoading }, { setParams }] = usePayloadAPI(
|
||||
'/api/posts/123',
|
||||
{ initialParams: { depth: 1 } }
|
||||
)
|
||||
|
||||
if (isLoading) return <p>Loading...</p>
|
||||
if (error) return <p>Error: {error.message}</p>
|
||||
if (isError) return <p>Error occurred while fetching data.</p>
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -1094,7 +1094,7 @@ The first item in the returned array is an object containing the following prope
|
||||
| Property | Description |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| **`data`** | The API response data. |
|
||||
| **`error`** | If an error occurs, this contains the error object. |
|
||||
| **`isError`** | A boolean indicating whether the request failed. |
|
||||
| **`isLoading`** | A boolean indicating whether the request is in progress. |
|
||||
|
||||
The second item is an object with the following methods:
|
||||
|
||||
@@ -63,42 +63,41 @@ export default buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
|
||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
|
||||
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
|
||||
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
|
||||
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
|
||||
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
|
||||
| **`debug`** | Enable to expose more detailed error information. |
|
||||
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
|
||||
| **`rateLimit`** | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks, etc. [More details](../production/preventing-abuse#rate-limiting-requests). |
|
||||
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
|
||||
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
|
||||
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
|
||||
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
|
||||
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
|
||||
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
|
||||
| Option | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`db`** * | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
|
||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
|
||||
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
|
||||
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
|
||||
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
|
||||
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
|
||||
| **`debug`** | Enable to expose more detailed error information. |
|
||||
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
|
||||
| **`hooks`** | An array of Root Hooks. [More details](../hooks/overview). |
|
||||
| **`plugins`** | An array of Plugins. [More details](../plugins/overview). |
|
||||
| **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](./i18n). |
|
||||
| **`secret`** * | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. |
|
||||
| **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. |
|
||||
| **`typescript`** | Configure TypeScript settings here. [More details](#typescript). |
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -266,3 +265,43 @@ The Payload Config can accept compatibility flags for running the newest version
|
||||
Payload localization works on a field-by-field basis. As you can nest fields within other fields, you could potentially nest a localized field within a localized field—but this would be redundant and unnecessary. There would be no reason to define a localized field within a localized parent field, given that the entire data structure from the parent field onward would be localized.
|
||||
|
||||
By default, Payload will remove the `localized: true` property from sub-fields if a parent field is localized. Set this compatibility flag to `true` only if you have an existing Payload MongoDB database from pre-3.0, and you have nested localized fields that you would like to maintain without migrating.
|
||||
|
||||
|
||||
## Custom bin scripts
|
||||
|
||||
Using the `bin` configuration property, you can inject your own scripts to `npx payload`.
|
||||
Example for `pnpm payload seed`:
|
||||
|
||||
|
||||
Step 1: create `seed.ts` file in the same folder with `payload.config.ts` with:
|
||||
|
||||
```ts
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import payload from 'payload'
|
||||
|
||||
// Script must define a "script" function export that accepts the sanitized config
|
||||
export const script = async (config: SanitizedConfig) => {
|
||||
await payload.init({ config })
|
||||
await payload.create({ collection: 'pages', data: { title: 'my title' } })
|
||||
payload.logger.info('Succesffully seeded!')
|
||||
process.exit(0)
|
||||
}
|
||||
```
|
||||
|
||||
Step 2: add the `seed` script to `bin`:
|
||||
```ts
|
||||
export default buildConfig({
|
||||
bin: [
|
||||
{
|
||||
scriptPath: path.resolve(dirname, 'seed.ts'),
|
||||
key: 'seed',
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Now you can run the command using:
|
||||
```sh
|
||||
pnpm payload seed
|
||||
```
|
||||
@@ -276,7 +276,7 @@ export default async function MyServerComponent({
|
||||
|
||||
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
|
||||
|
||||
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/hooks#useconfig) hook:
|
||||
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/react-hooks#useconfig) hook:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -375,7 +375,7 @@ export function MyClientComponent() {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
|
||||
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Getting the Current Locale
|
||||
@@ -422,12 +422,12 @@ function Greeting() {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
|
||||
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Using Hooks
|
||||
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](../admin/react-hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -444,7 +444,7 @@ export function MyClientComponent() {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](../admin/hooks) documentation for a full list of available hooks.
|
||||
See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Adding Styles
|
||||
|
||||
@@ -79,10 +79,11 @@ export const MyBlocksField: Field = {
|
||||
|
||||
The Blocks Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ---------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
| Option | Description |
|
||||
| ------------------- | -------------------------------------------------------------------------- |
|
||||
| **`group`** | Text or localization object used to group this Block in the Blocks Drawer. |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
|
||||
#### Customizing the way your block is rendered in Lexical
|
||||
|
||||
|
||||
@@ -239,7 +239,9 @@ This will add a dropdown to the date picker that allows users to select a timezo
|
||||
|
||||
You can customise the available list of timezones in the [global admin config](../admin/overview#timezones).
|
||||
|
||||
<Banner type='info'>
|
||||
<Banner type="info">
|
||||
**Good to know:**
|
||||
The date itself will be stored in UTC so it's up to you to handle the conversion to the user's timezone when displaying the date in your frontend.
|
||||
|
||||
Dates without a specific time are normalised to 12:00 in the selected timezone.
|
||||
</Banner>
|
||||
|
||||
@@ -121,22 +121,22 @@ powerful Admin UI.
|
||||
|
||||
## Config Options
|
||||
|
||||
| Option | Description |
|
||||
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** * | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
|
||||
| **`collection`** * | The `slug`s having the relationship field. |
|
||||
| **`on`** * | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
|
||||
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
|
||||
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
|
||||
| Option | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** * | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
|
||||
| **`collection`** * | The `slug`s having the relationship field or an array of collection slugs. |
|
||||
| **`on`** * | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections |
|
||||
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
|
||||
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
|
||||
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -158,6 +158,7 @@ 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
|
||||
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -171,7 +172,39 @@ object with:
|
||||
}
|
||||
// { ... }
|
||||
],
|
||||
"hasNextPage": false
|
||||
"hasNextPage": false,
|
||||
"totalDocs": 10, // if count: true is passed
|
||||
}
|
||||
// other fields...
|
||||
}
|
||||
```
|
||||
|
||||
## Join Field Data (polymorphic)
|
||||
|
||||
When a document is returned that for a polymorphic Join field (with `collection` as an array) is populated with related documents. The structure returned is an
|
||||
object with:
|
||||
|
||||
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
|
||||
- `hasNextPage` a boolean indicating if there are additional documents
|
||||
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "66e3431a3f23e684075aae9c",
|
||||
"relatedPosts": {
|
||||
"docs": [
|
||||
{
|
||||
"relationTo": "posts",
|
||||
"value": {
|
||||
"id": "66e3431a3f23e684075aaeb9",
|
||||
// other fields...
|
||||
"category": "66e3431a3f23e684075aae9c"
|
||||
}
|
||||
}
|
||||
// { ... }
|
||||
],
|
||||
"hasNextPage": false,
|
||||
"totalDocs": 10, // if count: true is passed
|
||||
}
|
||||
// other fields...
|
||||
}
|
||||
@@ -186,10 +219,11 @@ returning. This is useful for performance reasons when you don't need the relate
|
||||
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. Will be merged with the field `where` object. |
|
||||
| **`sort`** | A string used to order related results |
|
||||
| **`count`** | Whether include the count of related documents or not. Not included by default |
|
||||
|
||||
These can be applied to the local API, GraphQL, and REST API.
|
||||
|
||||
@@ -198,7 +232,8 @@ These can be applied to the local API, GraphQL, and REST 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', {
|
||||
const result = await payload.find({
|
||||
collection: 'categories',
|
||||
where: {
|
||||
title: {
|
||||
equals: 'My Category'
|
||||
@@ -218,6 +253,25 @@ const result = await db.findOne('categories', {
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
Currently, `Where` query support on joined documents for join fields with an array of `collection` is limited and not supported for fields inside arrays and blocks.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
Currently, querying by the Join Field itself is not supported, meaning:
|
||||
```ts
|
||||
payload.find({
|
||||
collection: 'categories',
|
||||
where: {
|
||||
'relatedPosts.title': { // relatedPosts is a join field
|
||||
equals: "post"
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
does not work yet.
|
||||
</Banner>
|
||||
|
||||
### Rest API
|
||||
|
||||
The rest API supports the same query options as the local API. You can use the `joins` query parameter to customize the
|
||||
|
||||
@@ -658,7 +658,7 @@ In addition to the above props, all Server Components will also receive the foll
|
||||
|
||||
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
|
||||
|
||||
To do so, import the [`useField`](../admin/hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
|
||||
To do so, import the [`useField`](../admin/react-hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -677,7 +677,7 @@ export const CustomTextField: React.FC = () => {
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](../admin/react-hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
##### TypeScript#field-component-types
|
||||
|
||||
@@ -50,6 +50,7 @@ export const MyRadioField: Field = {
|
||||
| **`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. |
|
||||
| **`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) |
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export const MySelectField: Field = {
|
||||
| **`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. |
|
||||
| **`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) |
|
||||
|
||||
|
||||
@@ -71,8 +71,7 @@ The following arguments are provided to all Field Hooks:
|
||||
| **`schemaPath`** | The path of the [Field](../fields/overview) in the schema. |
|
||||
| **`siblingData`** | The data of sibling fields adjacent to the field that the Hook is running against. |
|
||||
| **`siblingDocWithLocales`** | The sibling data of the Document with all [Locales](../configuration/localization). |
|
||||
| **`siblingFields`** | The sibling fields of the field which the hook is running against.
|
||||
|
|
||||
| **`siblingFields`** | The sibling fields of the field which the hook is running against. |
|
||||
| **`value`** | The value of the [Field](../fields/overview). |
|
||||
|
||||
<Banner type="success">
|
||||
|
||||
@@ -27,7 +27,7 @@ There are four main types of Hooks in Payload:
|
||||
|
||||
<Banner type="warning">
|
||||
**Reminder:**
|
||||
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/hooks).
|
||||
Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/react-hooks).
|
||||
</Banner>
|
||||
|
||||
## Root Hooks
|
||||
|
||||
@@ -44,3 +44,31 @@ const createdJob = await payload.jobs.queue({
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### Cancelling Jobs
|
||||
|
||||
Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks.
|
||||
|
||||
##### Cancel a Single Job
|
||||
|
||||
To cancel a specific job, use the `payload.jobs.cancelByID` method with the job's ID:
|
||||
|
||||
```ts
|
||||
await payload.jobs.cancelByID({
|
||||
id: createdJob.id,
|
||||
})
|
||||
```
|
||||
|
||||
##### Cancel Multiple Jobs
|
||||
|
||||
To cancel multiple jobs at once, use the `payload.jobs.cancel` method with a `Where` query:
|
||||
|
||||
```ts
|
||||
await payload.jobs.cancel({
|
||||
where: {
|
||||
workflowSlug: {
|
||||
equals: 'createPost',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -256,18 +256,14 @@ If you are using relationships or uploads in your front-end application, and you
|
||||
// ...
|
||||
// If your site is running on a different domain than your Payload server,
|
||||
// This will allows requests to be made between the two domains
|
||||
cors: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
cors: [
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
// If you are protecting resources behind user authentication,
|
||||
// This will allow cookies to be sent between the two domains
|
||||
csrf: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
csrf: [
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -345,8 +345,7 @@ const result = await payload.login({
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'rip',
|
||||
},
|
||||
req: req, // pass a Request object to be provided to all hooks
|
||||
res: res, // used to automatically set an HTTP-only auth cookie
|
||||
req: req, // optional, pass a Request object to be provided to all hooks
|
||||
depth: 2,
|
||||
locale: 'en',
|
||||
fallbackLocale: false,
|
||||
@@ -384,8 +383,7 @@ const result = await payload.resetPassword({
|
||||
password: req.body.password, // the new password to set
|
||||
token: 'afh3o2jf2p3f...', // the token generated from the forgotPassword operation
|
||||
},
|
||||
req: req, // pass a Request object to be provided to all hooks
|
||||
res: res, // used to automatically set an HTTP-only auth cookie
|
||||
req: req, // optional, pass a Request object to be provided to all hooks
|
||||
})
|
||||
```
|
||||
|
||||
@@ -399,7 +397,7 @@ const result = await payload.unlock({
|
||||
// required
|
||||
email: 'dev@payloadcms.com',
|
||||
},
|
||||
req: req, // pass a Request object to be provided to all hooks
|
||||
req: req, // optional, pass a Request object to be provided to all hooks
|
||||
overrideAccess: true,
|
||||
})
|
||||
```
|
||||
|
||||
@@ -414,6 +414,15 @@ For more details, see the [Documentation](https://payloadcms.com/docs/getting-st
|
||||
```
|
||||
1. The `./src/public` directory is now located directly at root level `./public` [see Next.js docs for details](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets)
|
||||
|
||||
1. Payload now automatically removes `localized: true` property from sub-fields if a parent is localized, as it's redunant and unnecessary. If you have some existing data in this structure and you want to disable that behavior, you need to enable `allowLocalizedWithinLocalized` flag in your payload.config [read more in documentation](https://payloadcms.com/docs/configuration/overview#compatibility-flags), or create a migration script that aligns your data.
|
||||
Mongodb example for a link in a page layout.
|
||||
|
||||
```diff
|
||||
- layout.columns.en.link.en.type.en
|
||||
+ layout.columns.en.link.type
|
||||
```
|
||||
|
||||
|
||||
## Custom Components
|
||||
|
||||
1. All Payload React components have been moved from the `payload` package to `@payloadcms/ui`. If you were previously importing components into your app from the `payload` package, for example to create Custom Components, you will need to change your import paths:
|
||||
@@ -1104,6 +1113,57 @@ plugins: [
|
||||
|
||||
If you have custom features for `@payloadcms/richtext-lexical` you will need to migrate your code to the new API. Read more about the new API in the [documentation](https://payloadcms.com/docs/rich-text/building-custom-features).
|
||||
|
||||
## Reserved Field names
|
||||
|
||||
Payload reserves certain field names for internal use. Using any of the following names in your collections or globals will result in those fields being sanitized from the config, which can cause deployment errors. Ensure that any conflicting fields are renamed before migrating.
|
||||
|
||||
### General Reserved Names
|
||||
|
||||
- `file`
|
||||
- `_id` (MongoDB only)
|
||||
- `__v` (MongoDB only)
|
||||
|
||||
**Important Note**: It is recommended to avoid using field names with an underscore (`_`) prefix unless explicitly required by a plugin. Payload uses this prefix for internal columns, which can lead to conflicts in certain SQL conditions. The following are examples of reserved internal columns (this list is not exhaustive and other internal fields may also apply):
|
||||
|
||||
- `_order`
|
||||
- `_path`
|
||||
- `_uuid`
|
||||
- `_parent_id`
|
||||
- `_locale`
|
||||
|
||||
### Auth-Related Reserved Names
|
||||
|
||||
These are restricted if your collection uses `auth: true` and does not have `disableAuthStrategy: true`:
|
||||
- `salt`
|
||||
- `hash`
|
||||
- `apiKey` (when `auth.useAPIKey: true` is enabled)
|
||||
- `useAPIKey` (when `auth.useAPIKey: true` is enabled)
|
||||
- `resetPasswordToken`
|
||||
- `resetPasswordExpiration`
|
||||
- `password`
|
||||
- `email`
|
||||
- `username`
|
||||
|
||||
### Upload-Related Reserved Names
|
||||
|
||||
These apply if your collection has `upload: true` configured:
|
||||
|
||||
- `filename`
|
||||
- `mimetype`
|
||||
- `filesize`
|
||||
- `width`
|
||||
- `height`
|
||||
- `focalX`
|
||||
- `focalY`
|
||||
- `url`
|
||||
- `thumbnailURL`
|
||||
|
||||
If `imageSizes` is configured, the following are also reserved:
|
||||
|
||||
- `sizes`
|
||||
|
||||
If any of these names are found in your collection / global fields, update them before migrating to avoid unexpected issues.
|
||||
|
||||
## Upgrade from previous beta
|
||||
|
||||
Reference this [community-made site](https://payload-releases-filter.vercel.app/?version=3&from=152429656&to=188243150&sort=asc&breaking=on). Set your version, sort by oldest first, enable breaking changes only.
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Starting to build your own plugin? Find everything you need and learn best
|
||||
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
|
||||
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
|
||||
|
||||
<Banner type="success">
|
||||
To use the template, run `npx create-payload-app@latest --template plugin` directly in
|
||||
@@ -19,7 +19,7 @@ Our plugin template includes everything you need to build a full life-cycle plug
|
||||
- A local dev environment to develop the plugin
|
||||
- Test suite with integrated GitHub workflow
|
||||
|
||||
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
|
||||
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
|
||||
|
||||
## Plugins Recap
|
||||
|
||||
@@ -75,7 +75,7 @@ The purpose of the **dev** folder is to provide a sanitized local Payload projec
|
||||
|
||||
Do **not** store any of the plugin functionality in this folder - it is purely an environment to _assist_ you with developing the plugin.
|
||||
|
||||
If you're starting from scratch, you can easily setup a dev environment like this:
|
||||
If you're starting from scratch, you can easily setup a dev environment like this:
|
||||
|
||||
```
|
||||
mkdir dev
|
||||
@@ -83,7 +83,7 @@ cd dev
|
||||
npx create-payload-app@latest
|
||||
```
|
||||
|
||||
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
|
||||
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
|
||||
|
||||
```
|
||||
plugins: [
|
||||
@@ -96,7 +96,7 @@ If you're using the plugin template, the dev folder is built out for you an
|
||||
|
||||
You can add to the `dev/payload.config.ts` and build out the dev project as needed to test your plugin.
|
||||
|
||||
When you're ready to start development, navigate into this folder with `cd dev`
|
||||
When you're ready to start development, navigate into this folder with `cd dev`
|
||||
|
||||
And then start the project with `pnpm dev` and pull up `http://localhost:3000` in your browser.
|
||||
|
||||
@@ -108,7 +108,7 @@ A good test suite is essential to ensure quality and stability in your plugin. P
|
||||
|
||||
Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the [Jest documentation.](https://jestjs.io/)
|
||||
|
||||
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set!
|
||||
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set!
|
||||
|
||||
```
|
||||
let payload: Payload
|
||||
@@ -160,7 +160,7 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
|
||||
## Building a Plugin
|
||||
|
||||
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
|
||||
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
|
||||
|
||||
|
||||
```
|
||||
@@ -217,7 +217,7 @@ To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend
|
||||
|
||||
We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly, else this can cause adverse behavior and conflicts with Payload Config and other plugins.
|
||||
|
||||
Let's say you want to build a plugin that adds a new collection:
|
||||
Let's say you want to build a plugin that adds a new collection:
|
||||
|
||||
```
|
||||
config.collections = [
|
||||
@@ -227,7 +227,7 @@ config.collections = [
|
||||
]
|
||||
```
|
||||
|
||||
First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
|
||||
First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
|
||||
|
||||
This same logic is applied to other array and object like properties such as admin, globals and hooks:
|
||||
|
||||
@@ -284,7 +284,7 @@ For a better user experience, provide a way to disable the plugin without uninst
|
||||
|
||||
### Include tests in your GitHub CI workflow
|
||||
|
||||
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
|
||||
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
|
||||
|
||||
### Publish your finished plugin to npm
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ The plugin accepts an object with the following properties:
|
||||
|
||||
```ts
|
||||
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
/**
|
||||
/**
|
||||
* After a tenant is deleted, the plugin will attempt to clean up related documents
|
||||
* - removing documents with the tenant ID
|
||||
* - removing the tenant from users
|
||||
@@ -158,6 +158,16 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
rowFields?: never
|
||||
tenantFieldAccess?: never
|
||||
}
|
||||
/**
|
||||
* Customize tenant selector label
|
||||
*
|
||||
* Either a string or an object where the keys are locales and the values are the string labels
|
||||
*/
|
||||
tenantSelectorLabel?:
|
||||
| Partial<{
|
||||
[key in AcceptedLanguages]?: string
|
||||
}>
|
||||
| string
|
||||
/**
|
||||
* The slug for the tenant collection
|
||||
*
|
||||
@@ -176,6 +186,14 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
/**
|
||||
* Opt out including the baseListFilter to filter tenants by selected tenant
|
||||
*/
|
||||
useTenantsListFilter?: boolean
|
||||
/**
|
||||
* Opt out including the baseListFilter to filter users by selected tenant
|
||||
*/
|
||||
useUsersTenantFilter?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
@@ -278,6 +296,50 @@ async rewrites() {
|
||||
}
|
||||
```
|
||||
|
||||
### React Hooks
|
||||
|
||||
Below are the hooks exported from the plugin that you can import into your own custom components to consume.
|
||||
|
||||
#### useTenantSelection
|
||||
|
||||
You can import this like so:
|
||||
|
||||
```tsx
|
||||
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
|
||||
|
||||
...
|
||||
|
||||
const tenantContext = useTenantSelection()
|
||||
```
|
||||
|
||||
The hook returns the following context:
|
||||
|
||||
```ts
|
||||
type ContextType = {
|
||||
/**
|
||||
* Array of options to select from
|
||||
*/
|
||||
options: OptionObject[]
|
||||
/**
|
||||
* The currently selected tenant ID
|
||||
*/
|
||||
selectedTenantID: number | string | undefined
|
||||
/**
|
||||
* Prevents a refresh when the tenant is changed
|
||||
*
|
||||
* If not switching tenants while viewing a "global", set to true
|
||||
*/
|
||||
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
|
||||
/**
|
||||
* Sets the selected tenant ID
|
||||
*
|
||||
* @param args.id - The ID of the tenant to select
|
||||
* @param args.refresh - Whether to refresh the page after changing the tenant
|
||||
*/
|
||||
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Lexical saves data in JSON - this is great for storage and flexibility and allow
|
||||
|
||||
## Lexical => JSX
|
||||
|
||||
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
|
||||
For React-based frontends, converting Lexical content to JSX is the recommended rendering approach. Import the RichText component from @payloadcms/richtext-lexical/react and pass the Lexical content to it:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
@@ -24,46 +24,130 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
}
|
||||
```
|
||||
|
||||
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
|
||||
|
||||
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
|
||||
|
||||
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop. In our [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) you have an example of how to use `converters` to render custom blocks, custom nodes and override existing converters.
|
||||
|
||||
<Banner type="default">
|
||||
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.
|
||||
When fetching data, ensure your `depth` setting is high enough to fully populate Lexical nodes such as uploads. The JSX converter requires fully populated data to work correctly.
|
||||
</Banner>
|
||||
|
||||
### Converting Lexical Blocks to JSX
|
||||
### Converting Internal Links
|
||||
|
||||
In order to convert lexical blocks or inline blocks to JSX, you will have to pass the converter for your block to the RichText component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
|
||||
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
|
||||
|
||||
To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document.
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import {
|
||||
type JSXConvertersFunction,
|
||||
RichText,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
import type { DefaultNodeTypes, SerializedLinkNode } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
||||
import {
|
||||
type JSXConvertersFunction,
|
||||
LinkJSXConverter,
|
||||
RichText,
|
||||
} from '@payloadcms/richtext-lexical/react'
|
||||
import React from 'react'
|
||||
|
||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
|
||||
const { relationTo, value } = linkNode.fields.doc!
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error('Expected value to be an object')
|
||||
}
|
||||
const slug = value.slug
|
||||
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
|
||||
}
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
...LinkJSXConverter({ internalDocToHref }),
|
||||
})
|
||||
|
||||
export const MyComponent: React.FC<{
|
||||
lexicalData: SerializedEditorState
|
||||
}> = ({ lexicalData }) => {
|
||||
return <RichText converters={jsxConverters} data={lexicalData} />
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
|
||||
To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your block to the `RichText` component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
|
||||
import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import React from 'react'
|
||||
|
||||
// Extend the default node types with your custom blocks for full type safety
|
||||
type NodeTypes = DefaultNodeTypes | SerializedBlockNode<MyInlineBlock | MyTextBlock>
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
// myTextBlock is the slug of the block
|
||||
// Each key should match your block's slug
|
||||
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
|
||||
},
|
||||
inlineBlocks: {
|
||||
// myInlineBlock is the slug of the block
|
||||
// Each key should match your inline block's slug
|
||||
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
|
||||
},
|
||||
})
|
||||
|
||||
export const MyComponent = ({ lexicalData }) => {
|
||||
return (
|
||||
<RichText
|
||||
converters={jsxConverters}
|
||||
data={lexicalData.lexicalWithBlocks as SerializedEditorState}
|
||||
/>
|
||||
)
|
||||
export const MyComponent: React.FC<{
|
||||
lexicalData: SerializedEditorState
|
||||
}> = ({ lexicalData }) => {
|
||||
return <RichText converters={jsxConverters} data={lexicalData} />
|
||||
}
|
||||
```
|
||||
|
||||
### Overriding Default JSX Converters
|
||||
|
||||
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
|
||||
|
||||
Example - overriding the upload node converter to use next/image:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import type { DefaultNodeTypes, SerializedUploadNode } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
|
||||
type NodeTypes = DefaultNodeTypes
|
||||
|
||||
// Custom upload converter component that uses next/image
|
||||
const CustomUploadComponent: React.FC<{
|
||||
node: SerializedUploadNode
|
||||
}> = ({ node }) => {
|
||||
if (node.relationTo === 'uploads') {
|
||||
const uploadDoc = node.value
|
||||
if (typeof uploadDoc !== 'object') {
|
||||
return null
|
||||
}
|
||||
const { alt, height, url, width } = uploadDoc
|
||||
return <Image alt={alt} height={height} src={url} width={width} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
// Override the default upload converter
|
||||
upload: ({ node }) => {
|
||||
return <CustomUploadComponent node={node} />
|
||||
},
|
||||
})
|
||||
|
||||
export const MyComponent: React.FC<{
|
||||
lexicalData: SerializedEditorState
|
||||
}> = ({ lexicalData }) => {
|
||||
return <RichText converters={jsxConverters} data={lexicalData} />
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ Using the BlocksFeature, you can add both inline blocks (= can be inserted into
|
||||
|
||||
### Example: Code Field Block with language picker
|
||||
|
||||
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. Make sure to manually install `@payloadcms/ui`first.
|
||||
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. First, make sure to explicitly install `@payloadcms/ui` in your project.
|
||||
|
||||
Field config:
|
||||
Field Config:
|
||||
|
||||
```ts
|
||||
import {
|
||||
@@ -91,7 +91,6 @@ CodeComponent.tsx:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { CodeFieldClient, CodeFieldClientProps } from 'payload'
|
||||
|
||||
import { CodeField, useFormFields } from '@payloadcms/ui'
|
||||
@@ -105,6 +104,8 @@ const languageKeyToMonacoLanguageMap = {
|
||||
tsx: 'typescript',
|
||||
}
|
||||
|
||||
type Language = keyof typeof languageKeyToMonacoLanguageMap
|
||||
|
||||
export const Code: React.FC<CodeFieldClientProps> = ({
|
||||
autoComplete,
|
||||
field,
|
||||
@@ -118,10 +119,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
|
||||
}) => {
|
||||
const languageField = useFormFields(([fields]) => fields['language'])
|
||||
|
||||
const language: string =
|
||||
(languageField?.value as string) || (languageField.initialValue as string) || 'typescript'
|
||||
const language: Language =
|
||||
(languageField?.value as Language) || (languageField?.initialValue as Language) || 'ts'
|
||||
|
||||
const label = languages[language as keyof typeof languages]
|
||||
const label = languages[language]
|
||||
|
||||
const props: CodeFieldClient = useMemo<CodeFieldClient>(
|
||||
() => ({
|
||||
@@ -129,9 +130,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
|
||||
type: 'code',
|
||||
admin: {
|
||||
...field.admin,
|
||||
label,
|
||||
editorOptions: undefined,
|
||||
language: languageKeyToMonacoLanguageMap[language] || language,
|
||||
},
|
||||
label,
|
||||
}),
|
||||
[field, language, label],
|
||||
)
|
||||
|
||||
@@ -145,13 +145,13 @@ Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
| --- | --- | --- |
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`BoldFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
|
||||
@@ -30,6 +30,7 @@ pnpm add @payloadcms/storage-vercel-blob
|
||||
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
|
||||
|
||||
```ts
|
||||
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
|
||||
@@ -64,6 +65,7 @@ export default buildConfig({
|
||||
| `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` |
|
||||
| `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) |
|
||||
| `token` | Vercel Blob storage read/write token | `''` |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
## S3 Storage
|
||||
[`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3)
|
||||
@@ -79,6 +81,7 @@ pnpm add @payloadcms/storage-s3
|
||||
- Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
|
||||
|
||||
```ts
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
@@ -126,6 +129,7 @@ pnpm add @payloadcms/storage-azure
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website.
|
||||
|
||||
```ts
|
||||
import { azureStorage } from '@payloadcms/storage-azure'
|
||||
@@ -161,6 +165,7 @@ export default buildConfig({
|
||||
| `baseURL` | Base URL for the Azure Blob storage account | |
|
||||
| `connectionString` | Azure Blob storage connection string | |
|
||||
| `containerName` | Azure Blob storage container name | |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
## Google Cloud Storage
|
||||
[`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs)
|
||||
@@ -175,6 +180,7 @@ pnpm add @payloadcms/storage-gcs
|
||||
|
||||
- Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
|
||||
|
||||
```ts
|
||||
import { gcsStorage } from '@payloadcms/storage-gcs'
|
||||
@@ -203,13 +209,14 @@ export default buildConfig({
|
||||
|
||||
### Configuration Options#gcs-configuration
|
||||
|
||||
| Option | Description | Default |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------- | --------- |
|
||||
| `enabled` | Whether or not to enable the plugin | `true` |
|
||||
| `collections` | Collections to apply the storage to | |
|
||||
| `bucket` | The name of the bucket to use | |
|
||||
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
|
||||
| `acl` | Access control list for files that are uploaded | `Private` |
|
||||
| Option | Description | Default |
|
||||
| --------------- | --------------------------------------------------------------------------------------------------- | --------- |
|
||||
| `enabled` | Whether or not to enable the plugin | `true` |
|
||||
| `collections` | Collections to apply the storage to | |
|
||||
| `bucket` | The name of the bucket to use | |
|
||||
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
|
||||
| `acl` | Access control list for files that are uploaded | `Private` |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
|
||||
## Uploadthing Storage
|
||||
@@ -226,6 +233,7 @@ pnpm add @payloadcms/storage-uploadthing
|
||||
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
|
||||
- Get a token from Uploadthing and set it as `token` in the `options` object.
|
||||
- `acl` is optional and defaults to `public-read`.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
@@ -246,13 +254,14 @@ export default buildConfig({
|
||||
|
||||
### Configuration Options#uploadthing-configuration
|
||||
|
||||
| Option | Description | Default |
|
||||
| ---------------- | ----------------------------------------------- | ------------- |
|
||||
| `token` | Token from Uploadthing. Required. | |
|
||||
| `acl` | Access control list for files that are uploaded | `public-read` |
|
||||
| `logLevel` | Log level for Uploadthing | `info` |
|
||||
| `fetch` | Custom fetch function | `fetch` |
|
||||
| `defaultKeyType` | Default key type for file operations | `fileKey` |
|
||||
| Option | Description | Default |
|
||||
| ---------------- | ------------------------------------------------------------- | ------------- |
|
||||
| `token` | Token from Uploadthing. Required. | |
|
||||
| `acl` | Access control list for files that are uploaded | `public-read` |
|
||||
| `logLevel` | Log level for Uploadthing | `info` |
|
||||
| `fetch` | Custom fetch function | `fetch` |
|
||||
| `defaultKeyType` | Default key type for file operations | `fileKey` |
|
||||
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
|
||||
|
||||
|
||||
## Custom Storage Adapters
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CollectionAfterLoginHook } from 'payload'
|
||||
|
||||
import { mergeHeaders } from '@payloadcms/next/utilities'
|
||||
import { generateCookie, getCookieExpiration } from 'payload'
|
||||
import { mergeHeaders, generateCookie, getCookieExpiration } from 'payload'
|
||||
|
||||
export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, user }) => {
|
||||
const relatedOrg = await req.payload.find({
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -117,7 +117,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@next/bundle-analyzer": "15.1.5",
|
||||
"@next/bundle-analyzer": "15.2.0",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
@@ -132,8 +132,8 @@
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/shelljs": "0.8.15",
|
||||
"chalk": "^4.1.2",
|
||||
"comment-json": "^4.2.3",
|
||||
@@ -153,7 +153,7 @@
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^10",
|
||||
"next": "15.1.5",
|
||||
"next": "15.2.0",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"playwright": "1.50.0",
|
||||
@@ -173,10 +173,6 @@
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ConnectOptions } from 'mongoose'
|
||||
import type { Connect } from 'payload'
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
import { captureError, defaultBeginTransaction } from 'payload'
|
||||
import { defaultBeginTransaction } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
@@ -70,12 +70,10 @@ export const connect: Connect = async function connect(
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
}
|
||||
} catch (err) {
|
||||
await captureError({
|
||||
this.payload.logger.error({
|
||||
err,
|
||||
msg: `Error: cannot connect to MongoDB. Details: ${err.message}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { CreateOptions } from 'mongoose'
|
||||
import type { Create, Document } from 'payload'
|
||||
import type { Create } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, req },
|
||||
{ collection, data, req, returning },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options: CreateOptions = {
|
||||
@@ -18,31 +18,34 @@ export const create: Create = async function create(
|
||||
|
||||
let doc
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
if (this.payload.collections[collection].customIDType) {
|
||||
sanitizedData._id = sanitizedData.id
|
||||
data._id = data.id
|
||||
}
|
||||
|
||||
try {
|
||||
;[doc] = await Model.create([sanitizedData], options)
|
||||
;[doc] = await Model.create([data], options)
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
doc = doc.toObject()
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -4,35 +4,39 @@ import type { CreateGlobal } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, data, req },
|
||||
{ slug, data, req, returning },
|
||||
) {
|
||||
const Model = this.globals
|
||||
|
||||
const global = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
globalType: slug,
|
||||
...data,
|
||||
},
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
|
||||
globalSlug: slug,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
const options: CreateOptions = {
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
let [result] = (await Model.create([global], options)) as any
|
||||
let [result] = (await Model.create([data], options)) as any
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = result.toObject()
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result,
|
||||
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CreateOptions } from 'mongoose'
|
||||
|
||||
import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload'
|
||||
import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
|
||||
this: MongooseAdapter,
|
||||
@@ -16,6 +16,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
@@ -26,25 +27,30 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
session: await getSession(this, 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 data = {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
}
|
||||
|
||||
const fields = buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
let [doc] = await VersionModel.create([data], options, req)
|
||||
|
||||
await VersionModel.updateMany(
|
||||
{
|
||||
@@ -70,13 +76,18 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
return result
|
||||
|
||||
doc = doc.toObject()
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { CreateOptions } from 'mongoose'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload'
|
||||
import { buildVersionCollectionFields, type CreateVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createVersion: CreateVersion = async function createVersion(
|
||||
this: MongooseAdapter,
|
||||
@@ -17,6 +16,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
@@ -27,25 +27,30 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
session: await getSession(this, 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 data = {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
}
|
||||
|
||||
const fields = buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collectionSlug].config,
|
||||
)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
let [doc] = await VersionModel.create([data], options, req)
|
||||
|
||||
const parentQuery = {
|
||||
$or: [
|
||||
@@ -56,13 +61,6 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
},
|
||||
],
|
||||
}
|
||||
if (data.parent instanceof Types.ObjectId) {
|
||||
parentQuery.$or.push({
|
||||
parent: {
|
||||
$eq: data.parent.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await VersionModel.updateMany(
|
||||
{
|
||||
@@ -89,13 +87,18 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
return result
|
||||
|
||||
doc = doc.toObject()
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { DeleteOne, Document } from 'payload'
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { DeleteOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, req, select, where },
|
||||
{ collection, req, returning, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options: QueryOptions = {
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
@@ -29,13 +29,23 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, options).lean()
|
||||
if (returning === false) {
|
||||
await Model.deleteOne(query, options)?.lean()
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const doc = await Model.findOneAndDelete(query, options)?.lean()
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const find: Find = async function find(
|
||||
this: MongooseAdapter,
|
||||
@@ -133,13 +133,12 @@ export const find: Find = async function find(
|
||||
result = await Model.paginate(query, paginationOptions)
|
||||
}
|
||||
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -8,14 +8,15 @@ import type { MongooseAdapter } from './index.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findGlobal: FindGlobal = async function findGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, locale, req, select, where },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
|
||||
const globalConfig = this.payload.globals.config.find((each) => each.slug === slug)
|
||||
const fields = globalConfig.flattenedFields
|
||||
const options: QueryOptions = {
|
||||
lean: true,
|
||||
select: buildProjectionFromSelect({
|
||||
@@ -34,18 +35,18 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
||||
where: combineQueries({ globalType: { equals: slug } }, where),
|
||||
})
|
||||
|
||||
let doc = (await Model.findOne(query, {}, options)) as any
|
||||
const doc = (await Model.findOne(query, {}, options)) as any
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
if (doc._id) {
|
||||
doc.id = doc._id
|
||||
delete doc._id
|
||||
}
|
||||
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
doc = sanitizeInternalFields(doc)
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields: globalConfig.fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -9,18 +9,15 @@ import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
|
||||
this: MongooseAdapter,
|
||||
{ global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where },
|
||||
) {
|
||||
const globalConfig = this.payload.globals.config.find(({ slug }) => slug === global)
|
||||
const Model = this.versions[global]
|
||||
const versionFields = buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.globals.config.find(({ slug }) => slug === global),
|
||||
true,
|
||||
)
|
||||
const versionFields = buildVersionGlobalFields(this.payload.config, globalConfig, true)
|
||||
|
||||
const session = await getSession(this, req)
|
||||
const options: QueryOptions = {
|
||||
@@ -103,13 +100,13 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: buildVersionGlobalFields(this.payload.config, globalConfig),
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AggregateOptions, QueryOptions } from 'mongoose'
|
||||
import type { Document, FindOne } from 'payload'
|
||||
import type { FindOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findOne: FindOne = async function findOne(
|
||||
this: MongooseAdapter,
|
||||
@@ -58,11 +58,7 @@ export const findOne: FindOne = async function findOne(
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
transform({ adapter: this, data: doc, fields: collectionConfig.fields, operation: 'read' })
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findVersions: FindVersions = async function findVersions(
|
||||
this: MongooseAdapter,
|
||||
@@ -104,13 +104,13 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
TypeWithVersion,
|
||||
UpdateGlobalArgs,
|
||||
UpdateGlobalVersionArgs,
|
||||
UpdateManyArgs,
|
||||
UpdateOneArgs,
|
||||
UpdateVersionArgs,
|
||||
} from 'payload'
|
||||
@@ -53,6 +54,7 @@ import { commitTransaction } from './transactions/commitTransaction.js'
|
||||
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||
import { updateGlobal } from './updateGlobal.js'
|
||||
import { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
import { updateMany } from './updateMany.js'
|
||||
import { updateOne } from './updateOne.js'
|
||||
import { updateVersion } from './updateVersion.js'
|
||||
import { upsert } from './upsert.js'
|
||||
@@ -160,6 +162,7 @@ declare module 'payload' {
|
||||
updateGlobalVersion: <T extends TypeWithID = TypeWithID>(
|
||||
args: { options?: QueryOptions } & UpdateGlobalVersionArgs<T>,
|
||||
) => Promise<TypeWithVersion<T>>
|
||||
|
||||
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
|
||||
updateVersion: <T extends TypeWithID = TypeWithID>(
|
||||
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
|
||||
@@ -200,6 +203,7 @@ export function mongooseAdapter({
|
||||
mongoMemoryServer,
|
||||
sessions: {},
|
||||
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
|
||||
updateMany,
|
||||
url,
|
||||
versions: {},
|
||||
// DatabaseAdapter
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import {
|
||||
captureError,
|
||||
commitTransaction,
|
||||
createLocalReq,
|
||||
initTransaction,
|
||||
killTransaction,
|
||||
readMigrationFiles,
|
||||
} from 'payload'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
@@ -50,7 +45,7 @@ export async function migrateFresh(
|
||||
msg: `Found ${migrationFiles.length} migration files.`,
|
||||
})
|
||||
|
||||
const req = await createLocalReq({}, payload)
|
||||
const req = { payload }
|
||||
|
||||
// Run all migrate up
|
||||
for (const migration of migrationFiles) {
|
||||
@@ -73,12 +68,10 @@ export async function migrateFresh(
|
||||
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
|
||||
} catch (err: unknown) {
|
||||
await killTransaction(req)
|
||||
await captureError({
|
||||
payload.logger.error({
|
||||
err,
|
||||
msg: `Error running migration ${migration.name}. Rolling back.`,
|
||||
req,
|
||||
})
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,6 +476,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
|
||||
schemaToReturn = {
|
||||
_id: false,
|
||||
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
@@ -698,6 +699,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
|
||||
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
|
||||
schemaToReturn = {
|
||||
_id: false,
|
||||
type: payload.config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { getSession } from '../utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from '../utilities/transform.js'
|
||||
|
||||
const migrateModelWithBatching = async ({
|
||||
batchSize,
|
||||
config,
|
||||
db,
|
||||
fields,
|
||||
Model,
|
||||
parentIsLocalized,
|
||||
@@ -18,6 +19,7 @@ const migrateModelWithBatching = async ({
|
||||
}: {
|
||||
batchSize: number
|
||||
config: SanitizedConfig
|
||||
db: MongooseAdapter
|
||||
fields: Field[]
|
||||
Model: Model<any>
|
||||
parentIsLocalized: boolean
|
||||
@@ -49,7 +51,7 @@ const migrateModelWithBatching = async ({
|
||||
}
|
||||
|
||||
for (const doc of docs) {
|
||||
sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized })
|
||||
transform({ adapter: db, data: doc, fields, operation: 'write', parentIsLocalized })
|
||||
}
|
||||
|
||||
await Model.collection.bulkWrite(
|
||||
@@ -124,6 +126,7 @@ export async function migrateRelationshipsV2_V3({
|
||||
await migrateModelWithBatching({
|
||||
batchSize,
|
||||
config,
|
||||
db,
|
||||
fields: collection.fields,
|
||||
Model: db.collections[collection.slug],
|
||||
parentIsLocalized: false,
|
||||
@@ -139,6 +142,7 @@ export async function migrateRelationshipsV2_V3({
|
||||
await migrateModelWithBatching({
|
||||
batchSize,
|
||||
config,
|
||||
db,
|
||||
fields: buildVersionCollectionFields(config, collection),
|
||||
Model: db.versions[collection.slug],
|
||||
parentIsLocalized: false,
|
||||
@@ -167,10 +171,11 @@ export async function migrateRelationshipsV2_V3({
|
||||
|
||||
// in case if the global doesn't exist in the database yet (not saved)
|
||||
if (doc) {
|
||||
sanitizeRelationshipIDs({
|
||||
config,
|
||||
transform({
|
||||
adapter: db,
|
||||
data: doc,
|
||||
fields: global.fields,
|
||||
operation: 'write',
|
||||
})
|
||||
|
||||
await GlobalsModel.collection.updateOne(
|
||||
@@ -191,6 +196,7 @@ export async function migrateRelationshipsV2_V3({
|
||||
await migrateModelWithBatching({
|
||||
batchSize,
|
||||
config,
|
||||
db,
|
||||
fields: buildVersionGlobalFields(config, global),
|
||||
Model: db.versions[global.slug],
|
||||
parentIsLocalized: false,
|
||||
|
||||
@@ -255,6 +255,25 @@ export async function buildSearchParam({
|
||||
return result
|
||||
}
|
||||
|
||||
if (formattedOperator === 'not_like' && typeof formattedValue === 'string') {
|
||||
const words = formattedValue.split(' ')
|
||||
|
||||
const result = {
|
||||
value: {
|
||||
$and: words.map((word) => ({
|
||||
[path]: {
|
||||
$not: {
|
||||
$options: 'i',
|
||||
$regex: word.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Some operators like 'near' need to define a full query
|
||||
// so if there is no operator key, just return the value
|
||||
if (!operatorKey) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
this: MongooseAdapter,
|
||||
@@ -124,18 +124,18 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
result = await VersionModel.paginate(versionQuery, paginationOptions)
|
||||
}
|
||||
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc = {
|
||||
_id: doc.parent,
|
||||
id: doc.parent,
|
||||
...doc.version,
|
||||
}
|
||||
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
for (let i = 0; i < result.docs.length; i++) {
|
||||
const id = result.docs[i].parent
|
||||
result.docs[i] = result.docs[i].version
|
||||
result.docs[i].id = id
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { UpdateGlobal } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, data, options: optionsArgs = {}, req, select },
|
||||
{ slug, data, options: optionsArgs = {}, req, returning, select },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
|
||||
|
||||
const options: QueryOptions = {
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
@@ -27,21 +26,16 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
let result
|
||||
transform({ adapter: this, data, fields, globalSlug: slug, operation: 'write' })
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields,
|
||||
})
|
||||
if (returning === false) {
|
||||
await Model.updateOne({ globalType: slug }, data, options)
|
||||
return null
|
||||
}
|
||||
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
|
||||
const result: any = await Model.findOneAndUpdate({ globalType: slug }, data, options)
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
transform({ adapter: this, data: result, fields, globalSlug: slug, operation: 'read' })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
|
||||
import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload'
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { MongooseAdapter } from './index.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
this: MongooseAdapter,
|
||||
@@ -17,6 +17,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
versionData,
|
||||
where,
|
||||
@@ -28,7 +29,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
|
||||
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
|
||||
const flattenedFields = buildVersionGlobalFields(this.payload.config, currentGlobal, true)
|
||||
const options: QueryOptions = {
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
@@ -47,22 +48,20 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: versionData,
|
||||
fields,
|
||||
})
|
||||
transform({ adapter: this, data: versionData, fields, operation: 'write' })
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
if (returning === false) {
|
||||
await VersionModel.updateOne(query, versionData, options)
|
||||
return null
|
||||
}
|
||||
return result
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
transform({ adapter: this, data: doc, fields, operation: 'read' })
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
61
packages/db-mongodb/src/updateMany.ts
Normal file
61
packages/db-mongodb/src/updateMany.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { UpdateMany } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateMany: UpdateMany = async function updateMany(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, locale, options: optionsArgs = {}, req, returning, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const fields = this.payload.collections[collection].config.fields
|
||||
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
transform({ adapter: this, data, fields, operation: 'write' })
|
||||
|
||||
try {
|
||||
await Model.updateMany(query, data, options)
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await Model.find(query, {}, options)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { UpdateOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
@@ -7,17 +7,26 @@ import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
this: MongooseAdapter,
|
||||
{ id, collection, data, locale, options: optionsArgs = {}, req, select, where: whereArg },
|
||||
{
|
||||
id,
|
||||
collection,
|
||||
data,
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
where: whereArg,
|
||||
},
|
||||
) {
|
||||
const where = id ? { id: { equals: id } } : whereArg
|
||||
const Model = this.collections[collection]
|
||||
const fields = this.payload.collections[collection].config.fields
|
||||
const options: QueryOptions = {
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
@@ -39,21 +48,24 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
|
||||
let result
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields,
|
||||
})
|
||||
transform({ adapter: this, data, fields, operation: 'write' })
|
||||
|
||||
try {
|
||||
result = await Model.findOneAndUpdate(query, sanitizedData, options)
|
||||
if (returning === false) {
|
||||
await Model.updateOne(query, data, options)
|
||||
return null
|
||||
} else {
|
||||
result = await Model.findOneAndUpdate(query, data, options)
|
||||
}
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
|
||||
transform({ adapter: this, data: result, fields, operation: 'read' })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
|
||||
import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
|
||||
|
||||
@@ -7,11 +7,11 @@ import type { MongooseAdapter } from './index.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
this: MongooseAdapter,
|
||||
{ id, collection, locale, options: optionsArgs = {}, req, select, versionData, where },
|
||||
{ id, collection, locale, options: optionsArgs = {}, req, returning, select, versionData, where },
|
||||
) {
|
||||
const VersionModel = this.versions[collection]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
@@ -26,7 +26,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
true,
|
||||
)
|
||||
|
||||
const options: QueryOptions = {
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
@@ -45,22 +45,20 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: versionData,
|
||||
fields,
|
||||
})
|
||||
transform({ adapter: this, data: versionData, fields, operation: 'write' })
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
if (returning === false) {
|
||||
await VersionModel.updateOne(query, versionData, options)
|
||||
return null
|
||||
}
|
||||
return result
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
transform({ adapter: this, data: doc, fields, operation: 'write' })
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -4,7 +4,16 @@ import type { MongooseAdapter } from './index.js'
|
||||
|
||||
export const upsert: Upsert = async function upsert(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, locale, req, select, where },
|
||||
{ collection, data, locale, req, returning, select, where },
|
||||
) {
|
||||
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, select, where })
|
||||
return this.updateOne({
|
||||
collection,
|
||||
data,
|
||||
locale,
|
||||
options: { upsert: true },
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
where,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ export const buildJoinAggregation = async ({
|
||||
}
|
||||
|
||||
const {
|
||||
count = false,
|
||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||
page,
|
||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||
@@ -121,6 +122,28 @@ export const buildJoinAggregation = async ({
|
||||
const alias = `${as}.docs.${collectionSlug}`
|
||||
aliases.push(alias)
|
||||
|
||||
const basePipeline = [
|
||||
{
|
||||
$addFields: {
|
||||
relationTo: {
|
||||
$literal: collectionSlug,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
$and: [
|
||||
{
|
||||
$expr: {
|
||||
$eq: [`$${join.field.on}`, '$$root_id_'],
|
||||
},
|
||||
},
|
||||
$match,
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
aggregate.push({
|
||||
$lookup: {
|
||||
as: alias,
|
||||
@@ -129,25 +152,7 @@ export const buildJoinAggregation = async ({
|
||||
root_id_: '$_id',
|
||||
},
|
||||
pipeline: [
|
||||
{
|
||||
$addFields: {
|
||||
relationTo: {
|
||||
$literal: collectionSlug,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
$and: [
|
||||
{
|
||||
$expr: {
|
||||
$eq: [`$${join.field.on}`, '$$root_id_'],
|
||||
},
|
||||
},
|
||||
$match,
|
||||
],
|
||||
},
|
||||
},
|
||||
...basePipeline,
|
||||
{
|
||||
$sort: {
|
||||
[sortProperty]: sortDirection,
|
||||
@@ -169,6 +174,24 @@ export const buildJoinAggregation = async ({
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (count) {
|
||||
aggregate.push({
|
||||
$lookup: {
|
||||
as: `${as}.totalDocs.${alias}`,
|
||||
from: adapter.collections[collectionSlug].collection.name,
|
||||
let: {
|
||||
root_id_: '$_id',
|
||||
},
|
||||
pipeline: [
|
||||
...basePipeline,
|
||||
{
|
||||
$count: 'result',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
aggregate.push({
|
||||
@@ -179,6 +202,23 @@ export const buildJoinAggregation = async ({
|
||||
},
|
||||
})
|
||||
|
||||
if (count) {
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
[`${as}.totalDocs`]: {
|
||||
$add: aliases.map((alias) => ({
|
||||
$ifNull: [
|
||||
{
|
||||
$first: `$${as}.totalDocs.${alias}.result`,
|
||||
},
|
||||
0,
|
||||
],
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
aggregate.push({
|
||||
$set: {
|
||||
[`${as}.docs`]: {
|
||||
@@ -195,17 +235,17 @@ export const buildJoinAggregation = async ({
|
||||
const sliceValue = page ? [(page - 1) * limitJoin, limitJoin] : [limitJoin]
|
||||
|
||||
aggregate.push({
|
||||
$set: {
|
||||
[`${as}.docs`]: {
|
||||
$slice: [`$${as}.docs`, ...sliceValue],
|
||||
$addFields: {
|
||||
[`${as}.hasNextPage`]: {
|
||||
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
[`${as}.hasNextPage`]: {
|
||||
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
|
||||
$set: {
|
||||
[`${as}.docs`]: {
|
||||
$slice: [`$${as}.docs`, ...sliceValue],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -222,6 +262,7 @@ export const buildJoinAggregation = async ({
|
||||
}
|
||||
|
||||
const {
|
||||
count,
|
||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||
page,
|
||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||
@@ -274,6 +315,31 @@ export const buildJoinAggregation = async ({
|
||||
polymorphicSuffix = '.value'
|
||||
}
|
||||
|
||||
const addTotalDocsAggregation = (as: string, foreignField: string) =>
|
||||
aggregate.push(
|
||||
{
|
||||
$lookup: {
|
||||
as: `${as}.totalDocs`,
|
||||
foreignField,
|
||||
from: adapter.collections[slug].collection.name,
|
||||
localField: versions ? 'parent' : '_id',
|
||||
pipeline: [
|
||||
{
|
||||
$match,
|
||||
},
|
||||
{
|
||||
$count: 'result',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
[`${as}.totalDocs`]: { $ifNull: [{ $first: `$${as}.totalDocs.result` }, 0] },
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (adapter.payload.config.localization && locale === 'all') {
|
||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
||||
@@ -304,6 +370,7 @@ export const buildJoinAggregation = async ({
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (limitJoin > 0) {
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
@@ -313,6 +380,10 @@ export const buildJoinAggregation = async ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (count) {
|
||||
addTotalDocsAggregation(as, `${join.field.on}${code}${polymorphicSuffix}`)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const localeSuffix =
|
||||
@@ -359,6 +430,11 @@ export const buildJoinAggregation = async ({
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (count) {
|
||||
addTotalDocsAggregation(as, foreignField)
|
||||
}
|
||||
|
||||
if (limitJoin > 0) {
|
||||
aggregate.push({
|
||||
$addFields: {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
const internalFields = ['__v']
|
||||
|
||||
export const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
|
||||
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
if (key === '_id') {
|
||||
return {
|
||||
...newDoc,
|
||||
id: val,
|
||||
}
|
||||
}
|
||||
|
||||
if (internalFields.indexOf(key) > -1) {
|
||||
return newDoc
|
||||
}
|
||||
|
||||
return {
|
||||
...newDoc,
|
||||
[key]: val,
|
||||
}
|
||||
}, {} as T)
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { traverseFields } from 'payload'
|
||||
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
data: Record<string, unknown>
|
||||
fields: Field[]
|
||||
parentIsLocalized?: boolean
|
||||
}
|
||||
|
||||
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
|
||||
}): number | string | Types.ObjectId => {
|
||||
const customIDField = relatedCollection.fields.find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
)
|
||||
|
||||
if (customIDField) {
|
||||
return value
|
||||
}
|
||||
|
||||
try {
|
||||
return new Types.ObjectId(value)
|
||||
} catch {
|
||||
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,
|
||||
parentIsLocalized,
|
||||
}: Args): Record<string, unknown> => {
|
||||
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
|
||||
if (!ref || typeof ref !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
if (!ref[field.name]) {
|
||||
return
|
||||
}
|
||||
|
||||
// handle localized relationships
|
||||
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
const locales = config.localization.locales
|
||||
const fieldRef = ref[field.name]
|
||||
if (typeof fieldRef !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
for (const { code } of locales) {
|
||||
const value = ref[field.name][code]
|
||||
if (value) {
|
||||
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,
|
||||
config,
|
||||
fields,
|
||||
fillEmpty: false,
|
||||
parentIsLocalized,
|
||||
ref: data,
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import { flattenAllFields, type Field, type SanitizedConfig } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
|
||||
import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js'
|
||||
import { transform } from './transform.js'
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
const flattenRelationshipValues = (obj: Record<string, any>, prefix = ''): Record<string, any> => {
|
||||
return Object.keys(obj).reduce(
|
||||
@@ -297,7 +298,7 @@ const relsData = {
|
||||
},
|
||||
}
|
||||
|
||||
describe('sanitizeRelationshipIDs', () => {
|
||||
describe('transform', () => {
|
||||
it('should sanitize relationships', () => {
|
||||
const data = {
|
||||
...relsData,
|
||||
@@ -382,7 +383,18 @@ describe('sanitizeRelationshipIDs', () => {
|
||||
}
|
||||
const flattenValuesBefore = Object.values(flattenRelationshipValues(data))
|
||||
|
||||
sanitizeRelationshipIDs({ config, data, fields: config.collections[0].fields })
|
||||
const mockAdapter = {
|
||||
payload: {
|
||||
config,
|
||||
},
|
||||
} as MongooseAdapter
|
||||
|
||||
transform({
|
||||
adapter: mockAdapter,
|
||||
operation: 'write',
|
||||
data,
|
||||
fields: config.collections[0].fields,
|
||||
})
|
||||
const flattenValuesAfter = Object.values(flattenRelationshipValues(data))
|
||||
|
||||
flattenValuesAfter.forEach((value, i) => {
|
||||
347
packages/db-mongodb/src/utilities/transform.ts
Normal file
347
packages/db-mongodb/src/utilities/transform.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import type {
|
||||
CollectionConfig,
|
||||
DateField,
|
||||
Field,
|
||||
JoinField,
|
||||
RelationshipField,
|
||||
SanitizedConfig,
|
||||
TraverseFieldsCallback,
|
||||
UploadField,
|
||||
} from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { traverseFields } from 'payload'
|
||||
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
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 convertRelationshipValue = ({
|
||||
operation,
|
||||
relatedCollection,
|
||||
validateRelationships,
|
||||
value,
|
||||
}: {
|
||||
operation: Args['operation']
|
||||
relatedCollection: CollectionConfig
|
||||
validateRelationships?: boolean
|
||||
value: unknown
|
||||
}) => {
|
||||
const customIDField = relatedCollection.fields.find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
)
|
||||
|
||||
if (operation === 'read') {
|
||||
if (value instanceof Types.ObjectId) {
|
||||
return value.toHexString()
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
if (customIDField) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return new Types.ObjectId(value)
|
||||
} catch (e) {
|
||||
if (validateRelationships) {
|
||||
throw e
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const sanitizeRelationship = ({
|
||||
config,
|
||||
field,
|
||||
locale,
|
||||
operation,
|
||||
ref,
|
||||
validateRelationships,
|
||||
value,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
field: JoinField | RelationshipField | UploadField
|
||||
locale?: string
|
||||
operation: Args['operation']
|
||||
ref: Record<string, unknown>
|
||||
validateRelationships?: boolean
|
||||
value?: unknown
|
||||
}) => {
|
||||
if (field.type === 'join') {
|
||||
if (
|
||||
operation === 'read' &&
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'docs' in value &&
|
||||
Array.isArray(value.docs)
|
||||
) {
|
||||
for (let i = 0; i < value.docs.length; i++) {
|
||||
const item = value.docs[i]
|
||||
|
||||
if (item instanceof Types.ObjectId) {
|
||||
value.docs[i] = item.toHexString()
|
||||
} else if (Array.isArray(field.collection) && item) {
|
||||
// Fields here for polymorphic joins cannot be determinted, JSON.parse needed
|
||||
value.docs[i] = JSON.parse(JSON.stringify(value.docs[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 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 - polymorphic
|
||||
if (isValidRelationObject(val)) {
|
||||
const relatedCollectionForSingleValue = config.collections?.find(
|
||||
({ slug }) => slug === val.relationTo,
|
||||
)
|
||||
|
||||
if (relatedCollectionForSingleValue) {
|
||||
return {
|
||||
relationTo: val.relationTo,
|
||||
value: convertRelationshipValue({
|
||||
operation,
|
||||
relatedCollection: relatedCollectionForSingleValue,
|
||||
validateRelationships,
|
||||
value: val.value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (relatedCollection) {
|
||||
return convertRelationshipValue({
|
||||
operation,
|
||||
relatedCollection,
|
||||
validateRelationships,
|
||||
value: val,
|
||||
})
|
||||
}
|
||||
|
||||
return val
|
||||
})
|
||||
}
|
||||
// Handle has one - polymorphic
|
||||
else if (isValidRelationObject(value)) {
|
||||
relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo)
|
||||
|
||||
if (relatedCollection) {
|
||||
result = {
|
||||
relationTo: value.relationTo,
|
||||
value: convertRelationshipValue({
|
||||
operation,
|
||||
relatedCollection,
|
||||
validateRelationships,
|
||||
value: value.value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle has one
|
||||
else if (relatedCollection) {
|
||||
result = convertRelationshipValue({
|
||||
operation,
|
||||
relatedCollection,
|
||||
validateRelationships,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
ref[locale] = result
|
||||
} else {
|
||||
ref[field.name] = result
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeDate = ({
|
||||
field,
|
||||
locale,
|
||||
ref,
|
||||
value,
|
||||
}: {
|
||||
field: DateField
|
||||
locale?: string
|
||||
ref: Record<string, unknown>
|
||||
value: unknown
|
||||
}) => {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString()
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
ref[locale] = value
|
||||
} else {
|
||||
ref[field.name] = value
|
||||
}
|
||||
}
|
||||
|
||||
type Args = {
|
||||
/** instance of the adapter */
|
||||
adapter: MongooseAdapter
|
||||
/** data to transform, can be an array of documents or a single document */
|
||||
data: Record<string, unknown> | Record<string, unknown>[]
|
||||
/** fields accossiated with the data */
|
||||
fields: Field[]
|
||||
/** slug of the global, pass only when the operation is `write` */
|
||||
globalSlug?: string
|
||||
/**
|
||||
* Type of the operation
|
||||
* read - sanitizes ObjectIDs, Date to strings.
|
||||
* write - sanitizes string relationships to ObjectIDs.
|
||||
*/
|
||||
operation: 'read' | 'write'
|
||||
parentIsLocalized?: boolean
|
||||
/**
|
||||
* Throw errors on invalid relationships
|
||||
* @default true
|
||||
*/
|
||||
validateRelationships?: boolean
|
||||
}
|
||||
|
||||
export const transform = ({
|
||||
adapter,
|
||||
data,
|
||||
fields,
|
||||
globalSlug,
|
||||
operation,
|
||||
parentIsLocalized,
|
||||
validateRelationships = true,
|
||||
}: Args) => {
|
||||
if (Array.isArray(data)) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
transform({ adapter, data: data[i], fields, globalSlug, operation, validateRelationships })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
payload: { config },
|
||||
} = adapter
|
||||
|
||||
if (operation === 'read') {
|
||||
delete data['__v']
|
||||
data.id = data._id
|
||||
delete data['_id']
|
||||
|
||||
if (data.id instanceof Types.ObjectId) {
|
||||
data.id = data.id.toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'write' && globalSlug) {
|
||||
data.globalType = globalSlug
|
||||
}
|
||||
|
||||
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
|
||||
if (!ref || typeof ref !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
if (field.type === 'date' && operation === 'read' && ref[field.name]) {
|
||||
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
const fieldRef = ref[field.name]
|
||||
if (!fieldRef || typeof fieldRef !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
for (const locale of config.localization.localeCodes) {
|
||||
sanitizeDate({
|
||||
field,
|
||||
ref: fieldRef,
|
||||
value: fieldRef[locale],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
sanitizeDate({
|
||||
field,
|
||||
ref: ref as Record<string, unknown>,
|
||||
value: ref[field.name],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === 'relationship' ||
|
||||
field.type === 'upload' ||
|
||||
(operation === 'read' && field.type === 'join')
|
||||
) {
|
||||
if (!ref[field.name]) {
|
||||
return
|
||||
}
|
||||
|
||||
// handle localized relationships
|
||||
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
const locales = config.localization.locales
|
||||
const fieldRef = ref[field.name]
|
||||
if (typeof fieldRef !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
for (const { code } of locales) {
|
||||
const value = ref[field.name][code]
|
||||
if (value) {
|
||||
sanitizeRelationship({
|
||||
config,
|
||||
field,
|
||||
locale: code,
|
||||
operation,
|
||||
ref: fieldRef,
|
||||
validateRelationships,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// handle non-localized relationships
|
||||
sanitizeRelationship({
|
||||
config,
|
||||
field,
|
||||
locale: undefined,
|
||||
operation,
|
||||
ref: ref as Record<string, unknown>,
|
||||
validateRelationships,
|
||||
value: ref[field.name],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
callback: sanitize,
|
||||
config,
|
||||
fields,
|
||||
fillEmpty: false,
|
||||
parentIsLocalized,
|
||||
ref: data,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect, Payload } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { captureError, type Connect, type Payload } from 'payload'
|
||||
import pg from 'pg'
|
||||
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
@@ -87,18 +87,16 @@ export const connect: Connect = async function connect(
|
||||
await this.connect(options)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
this.payload.logger.error({
|
||||
err,
|
||||
msg: `Error: cannot connect to Postgres. Details: ${err.message}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof this.rejectInitializing === 'function') {
|
||||
this.rejectInitializing()
|
||||
}
|
||||
|
||||
await captureError({
|
||||
err,
|
||||
msg: `Error: cannot connect to Postgres. Details: ${err.message}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
@@ -185,6 +186,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert: updateOne,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect } from 'payload'
|
||||
|
||||
import { createClient } from '@libsql/client'
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { drizzle } from 'drizzle-orm/libsql'
|
||||
import { captureError, type Connect } from 'payload'
|
||||
|
||||
import type { SQLiteAdapter } from './types.js'
|
||||
|
||||
@@ -36,14 +36,10 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error({ err, msg: `Error: cannot connect to SQLite: ${err.message}` })
|
||||
if (typeof this.rejectInitializing === 'function') {
|
||||
this.rejectInitializing()
|
||||
}
|
||||
await captureError({
|
||||
err,
|
||||
msg: `Error: cannot connect to SQLite: ${err.message}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,10 +50,12 @@ const createConstraint = ({
|
||||
const newAlias = `${pathSegments[0]}_alias_${pathSegments.length - 1}`
|
||||
let formattedValue = value
|
||||
let formattedOperator = operator
|
||||
|
||||
if (['contains', 'like'].includes(operator)) {
|
||||
formattedOperator = 'like'
|
||||
formattedValue = `%${value}%`
|
||||
} else if (['not_like', 'notlike'].includes(operator)) {
|
||||
formattedOperator = 'not like'
|
||||
formattedValue = `%${value}%`
|
||||
} else if (operator === 'equals') {
|
||||
formattedOperator = '='
|
||||
}
|
||||
@@ -61,7 +63,7 @@ const createConstraint = ({
|
||||
return `EXISTS (
|
||||
SELECT 1
|
||||
FROM json_each(${alias}.value -> '${pathSegments[0]}') AS ${newAlias}
|
||||
WHERE ${newAlias}.value ->> '${pathSegments[1]}' ${formattedOperator} '${formattedValue}'
|
||||
WHERE COALESCE(${newAlias}.value ->> '${pathSegments[1]}', '') ${formattedOperator} '${formattedValue}'
|
||||
)`
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,11 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
import { like } from 'drizzle-orm'
|
||||
import { like, notLike } from 'drizzle-orm'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -81,6 +82,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
...operatorMap,
|
||||
contains: like,
|
||||
like,
|
||||
not_like: notLike,
|
||||
} as unknown as Operators
|
||||
|
||||
return createDatabaseAdapter<SQLiteAdapter>({
|
||||
@@ -119,6 +121,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
tableNameMap: new Map<string, string>(),
|
||||
tables: {},
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
updateMany,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
// DatabaseAdapter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { sql, VercelPool } from '@vercel/postgres'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { captureError, type Connect } from 'payload'
|
||||
import pg from 'pg'
|
||||
|
||||
import type { VercelPostgresAdapter } from './types.js'
|
||||
@@ -72,18 +72,16 @@ export const connect: Connect = async function connect(
|
||||
await this.connect(options)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
this.payload.logger.error({
|
||||
err,
|
||||
msg: `Error: cannot connect to Postgres. Details: ${err.message}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof this.rejectInitializing === 'function') {
|
||||
this.rejectInitializing()
|
||||
}
|
||||
|
||||
await captureError({
|
||||
err,
|
||||
msg: `Error: cannot connect to Postgres. Details: ${err.message}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
@@ -186,6 +187,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert: updateOne,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, data, req, select },
|
||||
{ collection: collectionSlug, data, req, select, returning },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -25,7 +25,12 @@ export const create: Create = async function create(
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function createGlobal<T extends Record<string, unknown>>(
|
||||
this: DrizzleAdapter,
|
||||
{ slug, data, req }: CreateGlobalArgs,
|
||||
{ slug, data, req, returning }: CreateGlobalArgs,
|
||||
): Promise<T> {
|
||||
const db = await getTransaction(this, req)
|
||||
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
|
||||
@@ -26,8 +26,13 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
operation: 'create',
|
||||
req,
|
||||
tableName,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
returning,
|
||||
}: CreateGlobalVersionArgs,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
@@ -45,6 +46,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
ignoreResult: returning === false ? 'idOnly' : false,
|
||||
})
|
||||
|
||||
const table = this.tables[tableName]
|
||||
@@ -59,5 +61,9 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
})
|
||||
}
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
returning,
|
||||
}: CreateVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
@@ -72,5 +73,9 @@ export async function createVersion<T extends TypeWithID>(
|
||||
})
|
||||
}
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, req, select, where: whereArg },
|
||||
{ collection: collectionSlug, req, select, where: whereArg, returning },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -59,13 +59,16 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
docToDelete = await db.query[tableName].findFirst(findManyArgs)
|
||||
}
|
||||
|
||||
const result = transform({
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
data: docToDelete,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
})
|
||||
const result =
|
||||
returning === false
|
||||
? null
|
||||
: transform({
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
data: docToDelete,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
})
|
||||
|
||||
await this.deleteWhere({
|
||||
db,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
||||
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
|
||||
|
||||
import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
|
||||
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
|
||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
@@ -386,6 +386,7 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
const {
|
||||
count: shouldCount = false,
|
||||
limit: limitArg = field.defaultLimit ?? 10,
|
||||
page,
|
||||
sort = field.defaultSort,
|
||||
@@ -480,6 +481,13 @@ export const traverseFields = ({
|
||||
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias))
|
||||
}
|
||||
|
||||
if (shouldCount) {
|
||||
currentArgs.extras[`${columnName}_count`] = sql`${db
|
||||
.select({ count: count() })
|
||||
.from(sql`${currentQuery.as(subQueryAlias)}`)
|
||||
.where(sqlWhere)}`.as(`${columnName}_count`)
|
||||
}
|
||||
|
||||
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`)) as SQLSelect
|
||||
|
||||
if (page && limit !== 0) {
|
||||
@@ -611,6 +619,20 @@ export const traverseFields = ({
|
||||
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
||||
}).as(subQueryAlias)
|
||||
|
||||
if (shouldCount) {
|
||||
currentArgs.extras[`${columnName}_count`] = sql`${db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(
|
||||
sql`${db
|
||||
.select(selectFields as any)
|
||||
.from(newAliasTable)
|
||||
.where(subQueryWhere)
|
||||
.as(`${subQueryAlias}_count_subquery`)}`,
|
||||
)}`.as(`${subQueryAlias}_count`)
|
||||
}
|
||||
|
||||
currentArgs.extras[columnName] = sql`${db
|
||||
.select({
|
||||
result: jsonAggBuildObject(adapter, {
|
||||
|
||||
@@ -31,9 +31,10 @@ export { buildRawSchema } from './schema/buildRawSchema.js'
|
||||
export { beginTransaction } from './transactions/beginTransaction.js'
|
||||
export { commitTransaction } from './transactions/commitTransaction.js'
|
||||
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||
export { updateOne } from './update.js'
|
||||
export { updateGlobal } from './updateGlobal.js'
|
||||
export { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
export { updateMany } from './updateMany.js'
|
||||
export { updateOne } from './updateOne.js'
|
||||
export { updateVersion } from './updateVersion.js'
|
||||
export { upsertRow } from './upsertRow/index.js'
|
||||
export { buildCreateMigration } from './utilities/buildCreateMigration.js'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import {
|
||||
captureError,
|
||||
commitTransaction,
|
||||
createLocalReq,
|
||||
initTransaction,
|
||||
@@ -107,12 +106,10 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
|
||||
await commitTransaction(req)
|
||||
} catch (err: unknown) {
|
||||
await killTransaction(req)
|
||||
await captureError({
|
||||
payload.logger.error({
|
||||
err,
|
||||
msg: parseError(err, `Error running migration ${migration.name}`),
|
||||
req,
|
||||
})
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
captureError,
|
||||
commitTransaction,
|
||||
createLocalReq,
|
||||
getMigrations,
|
||||
@@ -64,10 +63,9 @@ export async function migrateDown(this: DrizzleAdapter): Promise<void> {
|
||||
} catch (err: unknown) {
|
||||
await killTransaction(req)
|
||||
|
||||
await captureError({
|
||||
payload.logger.error({
|
||||
err,
|
||||
msg: parseError(err, `Error migrating down ${migrationFile.name}. Rolling back.`),
|
||||
req,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
captureError,
|
||||
commitTransaction,
|
||||
createLocalReq,
|
||||
initTransaction,
|
||||
@@ -80,10 +79,9 @@ export async function migrateFresh(
|
||||
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
|
||||
} catch (err: unknown) {
|
||||
await killTransaction(req)
|
||||
await captureError({
|
||||
payload.logger.error({
|
||||
err,
|
||||
msg: parseError(err, `Error running migration ${migration.name}. Rolling back`),
|
||||
req,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
captureError,
|
||||
commitTransaction,
|
||||
createLocalReq,
|
||||
getMigrations,
|
||||
@@ -70,10 +69,9 @@ export async function migrateRefresh(this: DrizzleAdapter) {
|
||||
await commitTransaction(req)
|
||||
} catch (err: unknown) {
|
||||
await killTransaction(req)
|
||||
await captureError({
|
||||
payload.logger.error({
|
||||
err,
|
||||
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),
|
||||
req,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -99,10 +97,9 @@ export async function migrateRefresh(this: DrizzleAdapter) {
|
||||
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
|
||||
} catch (err: unknown) {
|
||||
await killTransaction(req)
|
||||
await captureError({
|
||||
payload.logger.error({
|
||||
err,
|
||||
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),
|
||||
req,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
captureError,
|
||||
commitTransaction,
|
||||
createLocalReq,
|
||||
getMigrations,
|
||||
@@ -64,7 +63,10 @@ export async function migrateReset(this: DrizzleAdapter): Promise<void> {
|
||||
}
|
||||
|
||||
await killTransaction(req)
|
||||
await captureError({ err, msg, req })
|
||||
payload.logger.error({
|
||||
err,
|
||||
msg,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +85,7 @@ export async function migrateReset(this: DrizzleAdapter): Promise<void> {
|
||||
},
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
await captureError({ err, msg: 'Error deleting dev migration', req })
|
||||
payload.logger.error({ err, msg: 'Error deleting dev migration' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { ClientConfig } from 'pg'
|
||||
|
||||
import { captureError } from 'payload'
|
||||
|
||||
import type { BasePostgresAdapter } from './types.js'
|
||||
|
||||
const setConnectionStringDatabase = ({
|
||||
@@ -90,10 +88,9 @@ export const createDatabase = async function (this: BasePostgresAdapter, args: A
|
||||
await createdDatabaseClient.query(`CREATE SCHEMA ${schemaName}`)
|
||||
this.payload.logger.info(`Created schema "${dbName}.${schemaName}"`)
|
||||
} catch (err) {
|
||||
await captureError({
|
||||
this.payload.logger.error({
|
||||
err,
|
||||
msg: `Error: failed to create schema "${dbName}.${schemaName}". Details: ${err.message}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
} finally {
|
||||
await createdDatabaseClient.end()
|
||||
@@ -102,10 +99,9 @@ export const createDatabase = async function (this: BasePostgresAdapter, args: A
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
await captureError({
|
||||
this.payload.logger.error({
|
||||
err,
|
||||
msg: `Error: failed to create database ${dbName}. Details: ${err.message}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
|
||||
return false
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { captureError } from 'payload'
|
||||
|
||||
import type { BasePostgresAdapter } from './types.js'
|
||||
|
||||
export const createExtensions = async function (this: BasePostgresAdapter): Promise<void> {
|
||||
@@ -8,11 +6,7 @@ export const createExtensions = async function (this: BasePostgresAdapter): Prom
|
||||
try {
|
||||
await this.drizzle.execute(`CREATE EXTENSION IF NOT EXISTS "${extension}"`)
|
||||
} catch (err) {
|
||||
await captureError({
|
||||
err,
|
||||
msg: `Failed to create extension ${extension}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
this.payload.logger.error({ err, msg: `Failed to create extension ${extension}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ const operatorMap: Record<string, string> = {
|
||||
like: 'like_regex',
|
||||
not_equals: '!=',
|
||||
not_in: 'in',
|
||||
not_like: '!like_regex',
|
||||
}
|
||||
|
||||
const sanitizeValue = (value: unknown, operator?: string) => {
|
||||
if (typeof value === 'string') {
|
||||
// ignore casing with like
|
||||
return `"${operator === 'like' ? '(?i)' : ''}${value}"`
|
||||
// ignore casing with like or not_like
|
||||
return `"${['like', 'not_like'].includes(operator) ? '(?i)' : ''}${value}"`
|
||||
}
|
||||
|
||||
return value as string
|
||||
@@ -35,6 +36,10 @@ export const createJSONQuery = ({ column, operator, pathSegments, value }: Creat
|
||||
})
|
||||
} else if (operator === 'exists') {
|
||||
sql = `${value === false ? 'NOT ' : ''}jsonb_path_exists(${columnName}, '$.${jsonPaths}')`
|
||||
} else if (['not_like'].includes(operator)) {
|
||||
const mappedOperator = operatorMap[operator]
|
||||
|
||||
sql = `NOT jsonb_path_exists(${columnName}, '$.${jsonPaths} ? (@ ${mappedOperator.substring(1)} ${sanitizeValue(value, operator)})')`
|
||||
} else {
|
||||
sql = `jsonb_path_exists(${columnName}, '$.${jsonPaths} ? (@ ${operatorMap[operator]} ${sanitizeValue(value, operator)})')`
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
lt,
|
||||
lte,
|
||||
ne,
|
||||
notIlike,
|
||||
notInArray,
|
||||
or,
|
||||
type SQL,
|
||||
@@ -31,6 +32,7 @@ type OperatorKeys =
|
||||
| 'like'
|
||||
| 'not_equals'
|
||||
| 'not_in'
|
||||
| 'not_like'
|
||||
| 'or'
|
||||
|
||||
export type Operators = Record<OperatorKeys, (column: Column, value: SQLWrapper | unknown) => SQL>
|
||||
@@ -48,6 +50,7 @@ export const operatorMap: Operators = {
|
||||
less_than_equal: lte,
|
||||
like: ilike,
|
||||
not_equals: ne,
|
||||
not_like: notIlike,
|
||||
// TODO: support this
|
||||
// all: all,
|
||||
not_in: notInArray,
|
||||
|
||||
@@ -161,6 +161,7 @@ export function parseParams({
|
||||
like: { operator: 'like', wildcard: '%' },
|
||||
not_equals: { operator: '<>', wildcard: '' },
|
||||
not_in: { operator: 'not in', wildcard: '' },
|
||||
not_like: { operator: 'not like', wildcard: '%' },
|
||||
}
|
||||
|
||||
let formattedValue = val
|
||||
@@ -175,11 +176,15 @@ export function parseParams({
|
||||
formattedValue = ''
|
||||
}
|
||||
|
||||
constraints.push(
|
||||
sql.raw(
|
||||
`${table[columnName].name}${jsonQuery} ${operatorKeys[operator].operator} ${formattedValue}`,
|
||||
),
|
||||
)
|
||||
let jsonQuerySelector = `${table[columnName].name}${jsonQuery}`
|
||||
|
||||
if (adapter.name === 'sqlite' && operator === 'not_like') {
|
||||
jsonQuerySelector = `COALESCE(${table[columnName].name}${jsonQuery}, '')`
|
||||
}
|
||||
|
||||
const rawSQLQuery = `${jsonQuerySelector} ${operatorKeys[operator].operator} ${formattedValue}`
|
||||
|
||||
constraints.push(sql.raw(rawSQLQuery))
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type BeginTransaction, captureError } from 'payload'
|
||||
import type { BeginTransaction } from 'payload'
|
||||
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { DrizzleAdapter, DrizzleTransaction } from '../types.js'
|
||||
@@ -57,11 +58,7 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
|
||||
resolve,
|
||||
}
|
||||
} catch (err) {
|
||||
await captureError({
|
||||
err,
|
||||
msg: `Error: cannot begin transaction: ${err.message}`,
|
||||
payload: this.payload,
|
||||
})
|
||||
this.payload.logger.error({ err, msg: `Error: cannot begin transaction: ${err.message}` })
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
||||
|
||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||
@@ -398,7 +399,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
if (field.type === 'join') {
|
||||
const { limit = field.defaultLimit ?? 10 } =
|
||||
const { count, limit = field.defaultLimit ?? 10 } =
|
||||
joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
|
||||
|
||||
// raw hasMany results from SQLite
|
||||
@@ -407,8 +408,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
let fieldResult:
|
||||
| { docs: unknown[]; hasNextPage: boolean }
|
||||
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
|
||||
| { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }
|
||||
| Record<string, { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }>
|
||||
if (Array.isArray(fieldData)) {
|
||||
if (isLocalized && adapter.payload.config.localization) {
|
||||
fieldResult = fieldData.reduce(
|
||||
@@ -449,6 +450,17 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
}
|
||||
|
||||
if (count) {
|
||||
const countPath = `${fieldName}_count`
|
||||
if (typeof table[countPath] !== 'undefined') {
|
||||
let value = Number(table[countPath])
|
||||
if (Number.isNaN(value)) {
|
||||
value = 0
|
||||
}
|
||||
fieldResult.totalDocs = value
|
||||
}
|
||||
}
|
||||
|
||||
result[field.name] = fieldResult
|
||||
return result
|
||||
}
|
||||
@@ -607,6 +619,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
deletions,
|
||||
fieldPrefix: groupFieldPrefix,
|
||||
fields: field.flattenedFields,
|
||||
joinQuery,
|
||||
numbers,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
path: `${sanitizedPath}${field.name}`,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
this: DrizzleAdapter,
|
||||
{ slug, data, req, select }: UpdateGlobalArgs,
|
||||
{ slug, data, req, select, returning }: UpdateGlobalArgs,
|
||||
): Promise<T> {
|
||||
const db = await getTransaction(this, req)
|
||||
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
|
||||
@@ -26,8 +26,13 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
|
||||
@@ -16,7 +16,16 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
this: DrizzleAdapter,
|
||||
{ id, global, locale, req, select, versionData, where: whereArg }: UpdateGlobalVersionArgs<T>,
|
||||
{
|
||||
id,
|
||||
global,
|
||||
locale,
|
||||
req,
|
||||
select,
|
||||
versionData,
|
||||
where: whereArg,
|
||||
returning,
|
||||
}: UpdateGlobalVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
|
||||
@@ -49,7 +58,12 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
select,
|
||||
tableName,
|
||||
where,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
97
packages/drizzle/src/updateMany.ts
Normal file
97
packages/drizzle/src/updateMany.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { UpdateMany } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const updateMany: UpdateMany = async function updateMany(
|
||||
this: DrizzleAdapter,
|
||||
{
|
||||
collection: collectionSlug,
|
||||
data,
|
||||
joins: joinQuery,
|
||||
locale,
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
where: whereToUse,
|
||||
},
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
|
||||
const { joins, selectFields, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.flattenedFields,
|
||||
locale,
|
||||
tableName,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
let idsToUpdate: (number | string)[] = []
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
db,
|
||||
joins,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
idsToUpdate = selectDistinctResult?.map((doc) => doc.id)
|
||||
|
||||
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
|
||||
} else if (whereToUse && !joins.length) {
|
||||
const _db = db as LibSQLDatabase
|
||||
|
||||
const table = this.tables[tableName]
|
||||
|
||||
const docsToUpdate = await _db
|
||||
.select({
|
||||
id: table.id,
|
||||
})
|
||||
.from(table)
|
||||
.where(where)
|
||||
|
||||
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
|
||||
}
|
||||
|
||||
if (!idsToUpdate.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const results = []
|
||||
|
||||
// TODO: We need to batch this to reduce the amount of db calls. This can get very slow if we are updating a lot of rows.
|
||||
for (const idToUpdate of idsToUpdate) {
|
||||
const result = await upsertRow({
|
||||
id: idToUpdate,
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
joinQuery,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { UpdateOne } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
@@ -12,7 +12,17 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
this: DrizzleAdapter,
|
||||
{ id, collection: collectionSlug, data, joins: joinQuery, locale, req, select, where: whereArg },
|
||||
{
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
data,
|
||||
joins: joinQuery,
|
||||
locale,
|
||||
req,
|
||||
select,
|
||||
where: whereArg,
|
||||
returning,
|
||||
},
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -28,6 +38,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
// selectDistinct will only return if there are joins
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: [{ args: [1], method: 'limit' }],
|
||||
@@ -40,22 +51,18 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
idToUpdate = selectDistinctResult?.[0]?.id
|
||||
|
||||
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
|
||||
} else if (whereArg && !joins.length) {
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter: this,
|
||||
depth: 0,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
select: {},
|
||||
tableName,
|
||||
})
|
||||
const table = this.tables[tableName]
|
||||
|
||||
findManyArgs.where = where
|
||||
|
||||
const docToUpdate = await db.query[tableName].findFirst(findManyArgs)
|
||||
idToUpdate = docToUpdate?.id
|
||||
const docsToUpdate = await (db as LibSQLDatabase)
|
||||
.select({
|
||||
id: table.id,
|
||||
})
|
||||
.from(table)
|
||||
.where(where)
|
||||
.limit(1)
|
||||
idToUpdate = docsToUpdate?.[0]?.id
|
||||
}
|
||||
|
||||
const result = await upsertRow({
|
||||
@@ -69,7 +76,12 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -16,7 +16,16 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function updateVersion<T extends TypeWithID>(
|
||||
this: DrizzleAdapter,
|
||||
{ id, collection, locale, req, select, versionData, where: whereArg }: UpdateVersionArgs<T>,
|
||||
{
|
||||
id,
|
||||
collection,
|
||||
locale,
|
||||
req,
|
||||
select,
|
||||
versionData,
|
||||
where: whereArg,
|
||||
returning,
|
||||
}: UpdateVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
@@ -47,7 +56,12 @@ export async function updateVersion<T extends TypeWithID>(
|
||||
select,
|
||||
tableName,
|
||||
where,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -428,6 +428,10 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreResult === 'idOnly') {
|
||||
return { id: insertedRow.id } as T
|
||||
}
|
||||
|
||||
if (ignoreResult) {
|
||||
return data as T
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type BaseArgs = {
|
||||
* When true, skips reading the data back from the database and returns the input data
|
||||
* @default false
|
||||
*/
|
||||
ignoreResult?: boolean
|
||||
ignoreResult?: boolean | 'idOnly'
|
||||
joinQuery?: JoinQuery
|
||||
path?: string
|
||||
req?: Partial<PayloadRequest>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -45,8 +45,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.23.0",
|
||||
"version": "3.25.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -96,7 +96,6 @@
|
||||
"qs-esm": "7.0.2",
|
||||
"react-diff-viewer-continued": "4.0.4",
|
||||
"sass": "1.77.4",
|
||||
"sonner": "^1.7.0",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -108,8 +107,8 @@
|
||||
"@next/eslint-plugin-next": "15.1.5",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/busboy": "1.5.4",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/uuid": "10.0.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
|
||||
"esbuild": "0.24.2",
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { NavPreferences, Payload, User } from 'payload'
|
||||
import type { DefaultDocumentIDType, NavPreferences, Payload, User } from 'payload'
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getNavPrefs = cache(
|
||||
async ({ payload, user }: { payload: Payload; user: User }): Promise<NavPreferences> =>
|
||||
user
|
||||
async (
|
||||
payload: Payload,
|
||||
userID: DefaultDocumentIDType,
|
||||
userSlug: string,
|
||||
): Promise<NavPreferences> => {
|
||||
return userSlug
|
||||
? await payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
user,
|
||||
pagination: false,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
@@ -20,17 +24,18 @@ export const getNavPrefs = cache(
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
equals: userSlug,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
equals: userID,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)
|
||||
: null,
|
||||
: null
|
||||
},
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user