Compare commits

..

58 Commits

Author SHA1 Message Date
Elliot DeNolf
7faa6253fc chore(release): v3.0.0-beta.108 [skip ci] 2024-09-20 15:58:38 -04:00
Germán Jabloñski
7d2022f28b feat(richtext-lexical)!: dropdown menu disabled status (#8177)
**Breaking change**: ToolbarDropdown no longer receives
`groupKey={group.key}` and `items={group.items}` as props, but instead
`group={group}`

___

Similar to #8159, but in this case it allows you to disable an entire
dropdown menu, not just individual items in the dropdown.

This adds a new property to `ToolbarGroup` when used with `type:
'dropdown'`.

For example, if you add `isEnabled: () => false,` inside
`packages/richtext-lexical/src/features/shared/toolbar/textDropdownGroup.ts`
and run `pnpm dev fields`, this is what you'll see in the Lexical
editor:


![image](https://github.com/user-attachments/assets/4efe2e92-2e78-473f-8c97-0995e3d44671)
2024-09-20 15:55:34 -04:00
Patrik
493b121ae8 fix(ui): hide dot menu in read-only mode for locked documents (#8342) 2024-09-20 15:48:57 -04:00
Elliot DeNolf
55afbe589c chore(cpa): prefer pnpm if exists (#8345)
Prefer pnpm if it exists.
2024-09-20 15:35:30 -04:00
Patrik
e916512fa7 fix(db-mongodb): treat empty strings as null / undefined for exists queries (#8337)
v2 PR [here](https://github.com/payloadcms/payload/pull/8336)
2024-09-20 15:27:07 -04:00
Germán Jabloñski
81a972d966 chore(richtext-lexical): add strictNullChecks to the richtext-lexical package (#8295)
This PR addresses around 500 TypeScript errors by enabling
strictNullChecks in the richtext-lexical package. In the process,
several bugs were identified and fixed.

In some cases, I applied non-null assertions where necessary, although
there may be room for further type refinement in the future. The focus
of this PR is to resolve the immediate issues without introducing
additional technical debt, rather than aiming for perfect type
definitions at this stage.

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
2024-09-20 18:42:30 +00:00
Paul
1f7d47a361 fix(ui): client side error when passing labels as functions for custom view tabs (#8339) 2024-09-20 17:32:46 +00:00
Louis Ba
afb8805a9b fix: add logger to serverOnlyConfigProperties (#8325)
## Description

Add `logger` field to `serverOnlyConfigProperties` to prevent it being
passed to client components, which could cause issues.

### Reproduction Steps
``` typescript
// payload.config.ts

export default buildConfig({
  // ...
  logger: pino({
    name: 'test',
  }),
  // ...
})
```


![image](https://github.com/user-attachments/assets/21d155b7-3d13-4a78-9ba6-036475a6633f)

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->

- [ ] Chore (non-breaking change which does not add functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)
- [ ] Change to the
[examples](https://github.com/payloadcms/payload/tree/main/examples)
directory (does not affect core functionality)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-09-20 13:00:24 -04:00
Paul
0789f4d0d4 fix(plugin-form-builder)!: emails array field has read access by authenticated users only by default now (#8338) 2024-09-20 16:54:33 +00:00
Alessio Gravili
cb831362c7 chore: add re-usable run against prod command (#8333) 2024-09-20 16:10:06 +00:00
Alessio Gravili
1afcaa30ed feat!: upgrade next, react and react-dom, move react/next dependency checker from payload to next package (#8323)
Fixes https://github.com/payloadcms/payload/issues/8013

**BREAKING:**
- Upgrades minimum supported @types/react version from
npm:types-react@19.0.0-rc.0 to npm:types-react@19.0.0-rc.1
- Upgrades minimum supported @types/react-dom version from
npm:types-react-dom@19.0.0-rc.0 to npm:types-react-dom@19.0.0-rc.1
- Upgrades minimum supported react and react-dom version from
19.0.0-rc-06d0b89e-20240801 to 19.0.0-rc-5dcb0097-20240918
- Upgrades minimum supported Next.js version from 15.0.0-canary.104 to
15.0.0-canary.160

---------

Co-authored-by: PatrikKozak <patrik@payloadcms.com>
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2024-09-20 12:09:42 -04:00
Alessio Gravili
d3b982f38d chore(templates): remove now unnecessary ts-ignore from lexical serializer (#8332) 2024-09-20 15:21:43 +00:00
Sasha
265d7fa0e2 fix(drizzle): equals polymorphic querying with object notation (#8316)
Previously, this wasn't valid in Postgres / SQLite:
```ts
const res = await payload.find({
  collection: 'polymorphic-relationships',
  where: {
    polymorphic: {
      equals: {
        relationTo: 'movies',
        value: movie.id,
      },
    },
  },
})
```

Now it works and actually in more performant way than this:
```ts
const res = await payload.find({
  collection: 'polymorphic-relationships',
  where: {
    and: [
      {
        'polymorphic.relationTo': {
          equals: 'movies',
        },
      },
      {
        'polymorphic.value': {
          equals: 'movies',
        },
      },
    ],
  },
})
``` 

Why? Because with the object notation, the output SQL is: `movies_id =
1` - checks exactly 1 column in the `*_rels` table, while with the
separate query by `relationTo` and `value` we need to check against
_each_ possible relationship collection with OR.
2024-09-20 11:18:13 -04:00
Sasha
2596b85027 fix(drizzle): nested arrays with localized items and versions (#8331)
Fixes https://github.com/payloadcms/payload/issues/7695

Previosuly, trying to append a new item to an array that contains
another array with localized items and enabled versions led to a unique
`_locale` and `_parent_id` error
```ts
{
  name: 'nestedArrayLocalized',
  type: 'array',
  fields: [
    {
      type: 'array',
      name: 'array',
      fields: [
        {
          name: 'text',
          type: 'text',
          localized: true,
        },
      ],
    },
  ],
}
```
2024-09-20 11:16:14 -04:00
Dan Ribbens
6ef2bdea15 feat!: join field (#7518)
## Description

- Adds a new "join" field type to Payload and is supported by all database adapters
- The UI uses a table view for the new field
- `db-mongodb` changes relationships to be stored as ObjectIDs instead of strings (for now querying works using both types internally to the DB so no data migration should be necessary unless you're querying directly, see breaking changes for details
- Adds a reusable traverseFields utility to Payload to make it easier to work with nested fields, used internally and for plugin maintainers

```ts
export const Categories: CollectionConfig = {
    slug: 'categories',
    fields: [
        {
            name: 'relatedPosts',
            type: 'join',
            collection: 'posts',
            on: 'category',
        }
    ]
}
```

BREAKING CHANGES:
All mongodb relationship and upload values will be stored as MongoDB ObjectIDs instead of strings going forward. If you have existing data and you are querying data directly, outside of Payload's APIs, you get different results. For example, a `contains` query will no longer works given a partial ID of a relationship since the ObjectID requires the whole identifier to work. 

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
2024-09-20 11:10:16 -04:00
Tylan Davis
b51d2bcb39 fix(ui): adjust list view table alignment (#8330)
### Description
- Fixes checkbox alignment issues within the collection list view table.
(Closes #8307)
- Aligns table cells to top for better readability across rows.

**Before:**
![Screenshot 2024-09-20 at 10 01
43 AM](https://github.com/user-attachments/assets/c35804d9-941b-4b52-a37d-0fac5734312e)

**After:**
![Screenshot 2024-09-20 at 9 10
35 AM](https://github.com/user-attachments/assets/52bb8405-b1ca-4083-a76d-30e7468bdad5)
2024-09-20 10:58:18 -04:00
Alessio Gravili
0cdd5b628c perf: disable router cache refresh that occurs after every page transition or page load (#8318)
This speeds up all page loads and reduces the amount of requests

## Example

### Clientside transition from dashboard => ui-fields list view

#### Router cache disabled
 GET /admin/collections/ui-fields 200 in 33ms
 POST /api/form-state 200 in 9ms
 POST /api/form-state 200 in 10ms
 GET /api/payload-preferences/ui-fields-list 200 in 11ms
 GET /admin/collections/ui-fields?limit=10&sort=id 200 in 42ms
 
#### Router cache enabled
 GET /admin/collections/ui-fields 200 in 33ms
 POST /api/form-state 200 in 11ms
 POST /api/form-state 200 in 12ms
 GET /api/payload-preferences/ui-fields-list 200 in 15ms
 GET /admin/collections/ui-fields?limit=10&sort=id 200 in 42ms
**GET /admin/collections/ui-fields?limit=10&sort=id 200 in 82ms** <==
this is gone
2024-09-20 09:57:09 -04:00
Elliot DeNolf
63b446c82b ci: bring back node setup action 2024-09-20 09:13:52 -04:00
Dan Ribbens
dbdc7d9308 chore: use updatedAt instead of editedAt for locked documents (#8324)
- Removes locked documents `editedAt` as it was redundant with the
`updatedAt` timestamp
- Adjust stale lock tests to configure the duration down to 1 second and
await it to not lose any test coverage
- DB performance changes: 
1. Switch to payload.db.find instead of payload.find for
checkDocumentLockStatus to avoid populating the user and other payload
find overhead
   2. Add maxDepth: 1 to user relationship
   3. Add index to global slug
2024-09-20 09:08:58 -04:00
Alessio Gravili
79c117c664 chore: fix lingering jest and next-server processes in monorepo (#8320) 2024-09-20 01:15:20 +00:00
Paul
faaa1188f4 chore: add warning in documentation about custom endpoints and authentication (#8321) 2024-09-20 01:03:30 +00:00
James Mikrut
c6a7ad2817 perf: optimizes admin ui loading by using react cache to memoize req / auth (#8315)
Uses React `cache` to memoize a lot of the work that the Payload Admin
UI had to perform in parallel, in multiple places.

Specifically, we were running `auth` in three places:

1. `not-found.tsx` - for some reason this renders even if not used
2. `initPage.ts`
3. `RootLayout`

Now, a lot of expensive calculations only happen once and are memoized
per-request. 🎉
2024-09-19 21:58:34 +00:00
Patrik
9c3f863ad2 feat: adds overrideLock flag to update & delete operations (#8294)
- Adds `overrideLock` flag to `update` & `delete` operations

- Instead of throwing an `APIError` (500) when trying to update / delete
a locked document - now throw a `Locked` (423) error status
2024-09-19 17:07:02 -04:00
Patrik
879f690161 feat(ui): hides lock icon when locked by current user (#8309) 2024-09-19 14:13:14 -04:00
Sasha
405a6c3447 feat(ui): threads collectionSlugs through useListDrawer (#8292)
Exposes `collectionSlugs` state from the `useListDrawer` hook to control
it outside of the hook. We can't use `collectionSlug` from the hook
props because it's memoized inside of the hook state.

```ts
const [
  ListDrawer,
  ListDrawerToggler,
  { collectionSlugs, setCollectionSlugs },
] = useListDrawer({
});
```
2024-09-19 12:16:18 -04:00
gervickas.js
a94e40f762 feat(storage-vercel-blob): threads cacheControlMaxAge through static handler (#8065)
## Description
I'm facing a issue while trying to set a cache age for vercel blob
storage plugin, this way I changed to accept and set as response the
cache control.

### Before changes

![image](https://github.com/user-attachments/assets/b67ee1a0-f4af-4f1f-942a-2063ec2fa5a2)

![image](https://github.com/user-attachments/assets/a3a7bce7-70be-4c06-ade6-3708f47d524c)


### After changes


![image](https://github.com/user-attachments/assets/9085193a-ef2d-4fa0-9aae-6817fe97aae0)


![image](https://github.com/user-attachments/assets/69a44948-402d-423b-873b-026be39c7501)

### Using plugin
```
vercelBlobStorage({
      enabled: true, // Optional, defaults to true
      // dev: Specify which collections should use Vercel Blob
      collections: {
        [Media.slug]: true,
      },
      // dev:Token provided by Vercel once Blob storage is added to your Vercel project
      token: process.env.BLOB_READ_WRITE_TOKEN!,
      cacheControlMaxAge: 31536000, /// the same we see
    }),
```

<!-- Please include a summary of the pull request and any related issues
it fixes. Please also include relevant motivation and context. -->

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->

- [ ] Chore (non-breaking change which does not add functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)
- [ ] Change to the
[examples](https://github.com/payloadcms/payload/tree/main/examples)
directory (does not affect core functionality)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-09-19 08:51:03 -06:00
Jessica Chowdhury
2d29b7e254 fix: only add snapshot to versions query when localization is enabled (#8293)
Versions list view should not query `snapshot` unless localization is
enabled.

Closes https://github.com/payloadcms/payload/issues/8289
2024-09-19 10:40:47 -04:00
Paul
7b907a8701 chore: add best practices for authenticating with cookies cross domains in documentation (#8301) 2024-09-18 21:08:34 -06:00
Sasha
72995fccf5 feat(ui): useTableColumns add abillity to reset columns state (#8296)
Fixes https://github.com/payloadcms/payload/issues/8205

Adds `resetColumnsState` method to `useTableColumns` return
Example of a `BeforeList` component for column state reset:
```ts
'use client'

import { Pill, useTableColumns } from '@payloadcms/ui'

function ResetDefaultColumnsButton() {
  const { resetColumnsState } = useTableColumns()

  return <Pill onClick={resetColumnsState}>Reset to default columns</Pill>
}

export { ResetDefaultColumnsButton }

```

Additionally, fixes that `setActiveColumns` didn't respect the passed
order of columns and didn't update the UI immediately
2024-09-18 20:35:03 -06:00
Frank Omondi
a095a6f891 docs: correct grammatical mistake (#8287) 2024-09-18 19:28:07 +00:00
Sasha
37e1adfa5c fix: findByID adjust type to null if disableErrors: true is passed (#8282)
Fixes https://github.com/payloadcms/payload/issues/8280

Now, the result type of this operation:
```ts
const post = await payload.findByID({
  collection: "posts",
  id,
  disableErrors: true
})
```
is `Post | null` instead of `Post` when `disableErrors: true` is passed

Adds test for the `disableErrors` property and docs.
2024-09-18 16:39:58 +00:00
Paul
9821aeb67a feat(plugin-form-builder): new wildcards {{*}} and {{*:table}} are now supported in email bodies and emails fall back to a global configuration value in addition to base email configuration (#8271)
Email bodies in the plugin form builder now support wildcards `{{*}}`
and `{{*:table}}` to export all the form submission data in key:value
pairs with the latter formatted as a table.

Emails also fallback to a global plugin configuration item and then to
the `defaultFromAddress` address in email transport config.
2024-09-18 00:28:52 +00:00
Paul
dd96f9a058 fix(richtext-slate): fix issue with richText field cell not being a link when used as a useAsTitle (#8272) 2024-09-18 00:00:00 +00:00
Patrik
023c650e03 chore: cleans up locked-documents (#8269)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-09-17 18:19:18 -04:00
Germán Jabloñski
4d54cc6e02 fix(templates): optional chaining bug (#8268)
Another one that ts non-strict mode is not catching 🙃🫠


https://github.com/user-attachments/assets/7cf78db9-4181-4a2d-9a8e-cdf3dd4f1951
2024-09-17 14:11:09 -06:00
Germán Jabloñski
f011e4fa26 fix(ui): code field adjusts its height to its content dynamically. Scrolling over the container is not prevented. (#8209)
Closes #8051.

- The scrolling problem reported in the issue is solved with Monaco's
`alwaysConsumeMouseWheel` property.
- In addition to that, it is necessary to dynamically adjust the height
of the editor so that it fits its content and does not require
scrolling.
- Additionally, I disabled the `overviewRuler` which is the indicator
strip on the side (above the scrollbar) that makes no sense when there
is no scroll.

**Gotchas**

- Unfortunately, there is a bit of CLS since the editor doesn't know the
height of its content before rendering. In Lexical these things are
possible since it has a lifecycle that allows interaction before or
after rendering, but this is not the case with Monaco.
- I've noticed that sometimes when I press enter the letters in the
editor flicker or move with a small, rapid shake. Maybe it has to do
with the new height being calculated as an effect.


## Before


https://github.com/user-attachments/assets/0747f79d-a3ac-42ae-8454-0bf46dc43f34


## After


https://github.com/user-attachments/assets/976ab97c-9d20-4e93-afb5-023083a6608b
2024-09-17 15:55:09 -04:00
Jacob Fletcher
c0aad3cccb fix: strongly types field validation args (#8263)
Continuation of #8243. Strongly types the `value` argument within
`field.validate` functions:

- Uses existing internal validation types for field `validate` property
- Exports additional validation types to cover `hasMany` fields
- Includes `null` and `undefined` values
2024-09-17 15:14:10 -04:00
Paul
110fda7533 fix(ui): mobile menu button sizing (#8264) 2024-09-17 18:14:40 +00:00
Patrik
f98d032617 feat: lock documents while being edited (#7970)
## Description

Adds a new property to `collection` / `global` configs called
`lockDocuments`.

Set to `true` by default - the lock is automatically triggered when a
user begins editing a document within the Admin Panel and remains in
place until the user exits the editing view or the lock expires due to
inactivity.

Set to `false` to disable document locking entirely - i.e.
`lockDocuments: false`

You can pass an object to this property to configure the `duration` in
seconds, which defines how long the document remains locked without user
interaction. If no edits are made within the specified time (default:
300 seconds), the lock expires, allowing other users to edit / update or
delete the document.

```
lockDocuments: {
  duration: 180, // 180 seconds or 3 minutes
}
```

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-09-17 14:04:48 -04:00
Because789
05a3cc47a6 docs: fixes link to ecommerce template in nested-docs.mdx (#8237) 2024-09-17 17:44:19 +00:00
Thành Trang
89601f18f5 feat(plugin-seo): add vietnamese translation (#8179) 2024-09-17 17:25:57 +00:00
Sasha
31ffc57366 fix(drizzle): in query on polymorphic relations across ID types (#8240)
Fixes querying using `in` operator by polymorphic relationship value.
The previous PR https://github.com/payloadcms/payload/pull/8191 didn't
handle the case when the incoming query value is an array and therefore
each item of the array can have a different type.
Ensures test coverage
2024-09-17 12:57:00 -04:00
Germán Jabloñski
9035467998 fix(ui): filter collection crashes when navigating away (#8260)
Fix https://github.com/payloadcms/payload/issues/8198

Caused by not having ts in strict mode.
2024-09-17 16:45:28 +00:00
Jacob Fletcher
029eba57b2 fix: properly infers field validation args (#8243)
Field validation functions currently do not type their `value` arg. This
is because the underlying `FieldBase` type breaks the type inferences
for these functions. The fix is to `Omit` the `validate` property from
this type before overriding it with our own, typed version for each
field.

Here's an example of the problem:

<img width="373" alt="Screenshot 2024-09-16 at 2 50 10 PM"
src="https://github.com/user-attachments/assets/a99e32fb-5645-4df6-82f2-0efab26b9831">

Here's an example of the fix:

<img width="363" alt="Screenshot 2024-09-16 at 3 59 42 PM"
src="https://github.com/user-attachments/assets/f83909bc-2169-4378-b5a7-5cca78b6ad64">

This PR also fixes the `hasMany` type inferences (shown above), where
the `value` type changes to an array when this property is set. Here's a
minimal example of the solution:

```ts
export type NumberField = {
  type: 'number'
} & (
  | {
      hasMany: true
      validate?: Validate<number[], unknown, unknown, NumberField>
    }
  | {
      hasMany?: false | undefined
      validate?: Validate<number, unknown, unknown, NumberField>
    }
)
```

```ts
{
  type: 'text',
  validate: (value) => '' // value is `string`
},
{
  type: 'text',
  hasMany: true,
  validate: (value) => '' // value is `string[]`
}
```

Disclaimer: in order for these types to properly infer their values,
`strictNullChecks: true` must be set in your `tsconfig.json`. This is
_not_ currently set in the Payload Monorepo, but consuming apps _should_
have this defined in order properly infer these types.
 
This PR also adds stronger types for misc. untyped values such as the
`point` field, etc.
2024-09-17 16:29:38 +00:00
Germán Jabloñski
67cd3b3cf8 fix(richtext-lexical): dropdown item disabled status (#8159)
## Description

It is possible to disable arbitrary items from a toolbar dropdown menu
with the `isEnabled` property. In order to illustrate the changes in
this PR, I have introduced the following lines in the
`HeadingFeatureClient` located at
`packages/richtext-lexical/src/features/heading/client/index.tsx`.

```ts
          isEnabled: () => {
            return headingSize === 'h2'
          },
```
**Before this PR**

![Screenshot 2024-09-10 at 4 50
08 PM](https://github.com/user-attachments/assets/bffe93db-9bb1-4ddd-8ca4-2cd9e37b7811)
![Screenshot 2024-09-10 at 4 50
40 PM](https://github.com/user-attachments/assets/a63d316d-bb28-4072-95c1-96b66bcdb519)

**After this PR**

![image](https://github.com/user-attachments/assets/427b2fa5-b015-4542-a632-aee094dd881b)

![image](https://github.com/user-attachments/assets/d5123d1b-c0cd-468f-a94b-bb5910dff1e9)


packages/richtext-lexical/src/features/heading/client/index.tsx


- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-09-17 10:55:50 -04:00
Sasha
bf48af411d feat: add virtual property to the fields config (#7621)
## Description

Adds `virtual` property to the fields config. Providing `true`
completely disables the field in the DB, which is useful for [Virtual
Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges)
Disables abillity to query by a field with `virtual: true`.
Currently, they bloat the DB with unused tables / columns, which may as
well introduce additional joins.
Discussion https://github.com/payloadcms/payload/discussions/6270
Prev PR (this one contains only this feature):
https://github.com/payloadcms/payload/pull/6983

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-09-17 10:40:54 -04:00
Because789
a7e8828e5e docs: fixes link to i18n in components.mdx (#8253) 2024-09-17 08:12:23 -04:00
Paul
a06458d70d fix(ui): pass label as prop through to the textarea input (#8248) 2024-09-16 22:58:22 +00:00
Paul
149b7cb26c fix(ui): field alignment when labels overflow inside rows (#8246) 2024-09-16 22:42:01 +00:00
Paul
ccd0b1ef48 fix(ui): draft docs are now correctly provided in preview URL (#8245) 2024-09-16 22:30:28 +00:00
Paul
d2eafdfaf8 fix: error on forgot-password route (#8244) 2024-09-16 20:41:57 +00:00
Paul
a68f0cec4a fix(ui): nav button height being stretched and navWrapper border on RTL (#8242) 2024-09-16 20:35:25 +00:00
Sasha
8520fd9570 fix(drizzle): optimize count querying when no joins (#7749)
Closes https://github.com/payloadcms/payload/issues/6321


To run benchmark:
`git checkout b840222` - from r1tsuu/payload
b840222784
`pnpm dev:postgres _community`

Benchmark results: (Before / After)
Postgres 400 000 rows:

![image](https://github.com/user-attachments/assets/cd7c478f-2057-4c7c-adec-5dbf0b05ec7b)
Postgres 2 000 000 rows:

![image](https://github.com/user-attachments/assets/04224f95-77eb-42ab-9591-887b197c597a)

SQLite 400 000 rows:

![image](https://github.com/user-attachments/assets/ba7482c2-30f1-4498-892d-59710639a7b3)
SQLite 2 000 000 rows:

![image](https://github.com/user-attachments/assets/c0a889f8-8e21-4b98-ac92-65ac735b8b32)



## Description

<!-- Please include a summary of the pull request and any related issues
it fixes. Please also include relevant motivation and context. -->

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

<!-- Please delete options that are not relevant. -->


- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes - See
https://github.com/payloadcms/payload/pull/7749#issuecomment-2295763721
2024-09-16 16:21:06 -04:00
Jessica Chowdhury
b7a0b15786 feat: add publish specific locale (#7669)
## Description

1. Adds ability to publish a specific individual locale (collections and
globals)
2. Shows published locale in versions list and version comparison
3. Adds new int tests to `versions` test suite

- [X] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [X] New feature (non-breaking change which adds functionality)
- [ ] This change requires a documentation update

## Checklist:

- [X] I have added tests that prove my fix is effective or that my
feature works
- [X] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-09-16 16:15:29 -04:00
Jacob Fletcher
aee76cb793 fix(plugin-seo): threads entity slug and document config through generation fn args (#8238)
The `generateTitle`, `generateDescription`, `generateURL`, and
`generateImage` functions in the SEO Plugin do not currently receive any
args representing the document's entity. This means that within these
functions, it is currently not possible to discern the _type_ of
document you are working with, i.e. a collection or global. The
underlying problem here was that the request made to execute these
functions was threading through `slug` as `undefined`. This is because
the `DocumentInfoProvider` was failing to thread this prop through
context as the types suggest. Now, these functions receive their
respective `collectionConfig` and `globalConfig`.

```ts
import type { GenerateTitle } from '@payloadcms/plugin-seo/types'
import type { Page } from '@/payload-types'

const generateTitle: GenerateTitle<Page> = ({
  doc,
  collectionConfig,
  globalConfig,
}) => {
  return `Website.com — ${doc?.title}`
}
```
2024-09-16 19:27:39 +00:00
Sasha
b7db53cef4 fix(drizzle)!: localized fields uniqueness per locale (#8230)
Previously, this worked with MongoDB but failed with Postgres / SQLite
when the `slug` field has both `localized: true` and `unique: true`.

```ts
await payload.create({
  collection: "posts",
  locale: "en",
  data: {
    slug: "my-post"
  }
})

await payload.create({
  collection: "posts",
  locale: "de",
  data: {
    slug: "my-post"
  }
})
```

Now, we build unique constraints and indexes in combination with the
_locale column. This should also improve query performance for fields
with both index: true and localized: true.

### Migration steps (Postgres/SQLite only)
This change updates the database schema and requires a migration (if you
have any localized fields). To apply it, run the following commands:

```sh
pnpm payload migration:create locale_unique_indexes
pnpm payload migrate
```

Note that if you use `db.push: true` which is a default, you don't have
to run `pnpm payload migrate` in the development mode, only in the
production, as Payload automatically pushes the schema to your DB with
it.
2024-09-16 14:47:13 -04:00
Dan Ribbens
f72fd8543b chore: gitignore test/databaseAdapter (#8235) 2024-09-16 17:02:08 +00:00
Elliot DeNolf
d046e0d18f chore(deps): bump turbo 2024-09-16 11:57:02 -04:00
568 changed files with 16168 additions and 13635 deletions

48
.github/actions/setup/action.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Setup node and pnpm
description: Configure the Node.js and pnpm versions
inputs:
node-version:
description: 'The Node.js version to use'
required: true
default: 22.6.2
pnpm-version:
description: 'The pnpm version to use'
required: true
default: 9.7.1
runs:
using: composite
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
shell: bash
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ inputs.pnpm-version }}
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- shell: bash
run: pnpm install

View File

@@ -310,6 +310,7 @@ jobs:
- fields__collections__Upload
- live-preview
- localization
- locked-documents
- i18n
- plugin-cloud-storage
- plugin-form-builder

View File

@@ -52,7 +52,6 @@ jobs:
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-relationship-object-ids
plugin-search
plugin-sentry
plugin-seo

1
.gitignore vendored
View File

@@ -313,3 +313,4 @@ test/admin-root/app/(payload)/admin/importMap.js
test/app/(payload)/admin/importMap.js
/test/app/(payload)/admin/importMap.js
test/pnpm-lock.yaml
test/databaseAdapter.js

7
.vscode/launch.json vendored
View File

@@ -118,6 +118,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js locked-documents",
"cwd": "${workspaceFolder}",
"name": "Run Dev Locked Documents",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js uploads",
"cwd": "${workspaceFolder}",

View File

@@ -357,7 +357,7 @@ Each Custom Component receives the following props by default:
| Prop | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `payload` | The [Payload](../local-api/overview) class. |
| `i18n` | The [i18n](../i18n) object. |
| `i18n` | The [i18n](../configuration/i18n) object. |
Custom Components also receive various other props that are specific to the context in which the Custom Component is being rendered. For example, [Custom Views](./views) receive the `user` prop. For a full list of available props, consult the documentation related to the specific component you are working with.

View File

@@ -0,0 +1,79 @@
---
title: Document Locking
label: Document Locking
order: 90
desc: Ensure your documents are locked while being edited, preventing concurrent edits from multiple users and preserving data integrity.
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
---
Document locking in Payload ensures that only one user at a time can edit a document, preventing data conflicts and accidental overwrites. When a document is locked, other users are prevented from making changes until the lock is released, ensuring data integrity in collaborative environments.
The lock is automatically triggered when a user begins editing a document within the Admin Panel and remains in place until the user exits the editing view or the lock expires due to inactivity.
## How it works
When a user starts editing a document, Payload locks the document for that user. If another user tries to access the same document, they will be notified that it is currently being edited and can choose one of the following options:
- View in Read-Only Mode: View the document without making any changes.
- Take Over Editing: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
- Return to Dashboard: Navigate away from the locked document and continue with other tasks.
The lock will automatically expire after a set period of inactivity, configurable using the duration property in the lockDocuments configuration, after which others can resume editing.
<Banner type="info"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
### Config Options
The lockDocuments property exists on both the Collection Config and the Global Config. By default, document locking is enabled for all collections and globals, but you can customize the lock duration or disable the feature entirely.
Heres an example configuration for document locking:
```ts
import { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
},
// other fields...
],
lockDocuments: {
duration: 600, // Duration in seconds
},
}
```
#### Locking Options
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`lockDocuments`** | Enables or disables document locking for the collection or global. By default, document locking is enabled. Set to an object to configure, or set to false to disable locking. |
| **`duration`** | Specifies the duration (in seconds) for how long a document remains locked without user interaction. The default is 300 seconds (5 minutes). |
### Impact on APIs
Document locking affects both the Local API and the REST API, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
#### Overriding Locks
For operations like update and delete, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`.
```ts
const result = await payload.update({
collection: 'posts',
id: '123',
data: {
title: 'New title',
},
overrideLock: false, // Enforces the document lock, preventing updates if the document is locked
})
```
This option is particularly useful in scenarios where administrative privileges or specific workflows require you to override the lock and ensure the operation is completed.

View File

@@ -85,3 +85,47 @@ const config = buildConfig({
export default config
```
#### Cross domain authentication
If your frontend is on a different domain than your Payload API then you will not be able to use HTTP-only cookies for authentication by default as they will be considered third-party cookies by the browser.
There are a few strategies to get around this:
##### 1. Use subdomains
Cookies can cross subdomains without being considered third party cookies, for example if your API is at api.example.com then you can authenticate from example.com.
##### 2. Configure cookies
If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](https://payloadcms.com/docs/beta/authentication/overview#config-options) on your authentication collection to achieve the following setup:
```
SameSite: None // allows the cookie to cross domains
Secure: true // ensures its sent over HTTPS only
HttpOnly: true // ensures its not accessible via client side JavaScript
```
Configuration example:
```ts
{
slug: 'users',
auth: {
cookies: {
sameSite: 'None',
secure: true,
}
},
fields: [
// your auth fields here
]
},
```
If you're configuring [cors](https://payloadcms.com/docs/beta/production/preventing-abuse#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains.
<Banner type="success">
<strong>Good to know:</strong>
Setting up <code>secure: true</code> will not work if you're developing on <code>http://localhost</code> or any non-https domain. For local development you should conditionally set this to <code>false</code> based on the environment.
</Banner>

View File

@@ -57,25 +57,26 @@ export const Posts: CollectionConfig = {
The following options are available:
| Option | Description |
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/collections). |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| **`auth`** | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| **`defaultSort`** | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`dbName`** | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| Option | Description |
|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/collections). |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
| **`auth`** | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
| **`defaultSort`** | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`dbName`** | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
_\* An asterisk denotes that a property is required._

View File

@@ -65,21 +65,22 @@ export const Nav: GlobalConfig = {
The following options are available:
| Option | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/globals). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`description`** | Text or React component to display below the Global header to give editors more information. |
| **`endpoints`** | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
| **`label`** | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#globals-config). |
| Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`access`** | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
| **`admin`** | The configuration options for the Admin Panel. [More details](../admin/globals). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. |
| **`description`** | Text or React component to display below the Global header to give editors more information. |
| **`endpoints`** | Add custom routes to the REST API. [More details](../rest-api/overview#custom-endpoints). |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). |
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`hooks`** | Entry point for Hooks. [More details](../hooks/overview#global-hooks). |
| **`label`** | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. |
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#globals-config). |
_\* An asterisk denotes that a property is required._

View File

@@ -42,24 +42,25 @@ export const MyArrayField: Field = {
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) or an object with keys for each language. Auto-generated from name if not defined. |
| **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) or an object with keys for each language. Auto-generated from name if not defined. |
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More](/docs/fields/overview#validation) |
| **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](../admin/overview) and the backend. [More](/docs/fields/overview#validation) |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. |
| **`required`** | Require this field to have a value. |
| **`labels`** | Customize the row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`dbName`** | Custom table name for the field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -47,17 +47,18 @@ export const MyBlocksField: Field = {
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin Panel. |
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -34,16 +34,17 @@ export const MyCheckboxField: Field = {
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -38,16 +38,17 @@ export const MyBlocksField: Field = {
| **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. |
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -34,16 +34,17 @@ export const MyDateField: Field = {
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -35,16 +35,17 @@ export const MyEmailField: Field = {
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -39,16 +39,17 @@ export const MyGroupField: Field = {
| **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide an object of data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

235
docs/fields/join.mdx Normal file
View File

@@ -0,0 +1,235 @@
---
title: Join Field
label: Join
order: 140
desc: The Join field provides the ability to work on related documents. Learn how to use Join field, see examples and options.
keywords: join, relationship, junction, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Join Field is used to make Relationship fields in the opposite direction. It is used to show the relationship from
the other side. The field itself acts as a virtual field, in that no new data is stored on the collection with a Join
field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's
APIs.
The Join field is useful in scenarios including:
- To surface `Order`s for a given `Product`
- To view and edit `Posts` belonging to a `Category`
- To work with any bi-directional relationship data
For the Join field to work, you must have an existing [relationship](./relationship) field in the collection you are
joining. This will reference the collection and path of the field of the related documents.
To add a Relationship Field, set the `type` to `join` in your [Field Config](./overview):
```ts
import type { Field } from 'payload/types'
export const MyJoinField: Field = {
// highlight-start
name: 'relatedPosts',
type: 'join',
collection: 'posts',
on: 'category',
// highlight-end
}
// relationship field in another collection:
export const MyRelationshipField: Field = {
name: 'category',
type: 'relationship',
relationTo: 'categories',
}
```
In this example, the field is defined to show the related `posts` when added to a `category` collection. The `on`
property is used to
specify the relationship field name of the field that relates to the collection document.
With this example, if you navigate to a Category in the Admin UI or an API response, you'll now see that the Posts which
are related to the Category are populated for you. This is extremely powerful and can be used to define a wide variety
of relationship types in an easy manner.
<Banner type="success">
The Join field is extremely performant and does not add additional query overhead to your API responses until you add depth of 1 or above. It works in all database adapters. In MongoDB, we use <strong>aggregations</strong> to automatically join in related documents, and in relational databases, we use joins.
</Banner>
### Schema advice
When modeling your database, you might come across many places where you'd like to feature bi-directional relationships.
But here's an important consideration—you generally only want to store information about a given relationship in _one_
place.
Let's take the Posts and Categories example. It makes sense to define which category a post belongs to while editing the
post.
It would generally not be necessary to have a list of post IDs stored directly on the category as well, for a few
reasons:
- You want to have a "single source of truth" for relationships, and not worry about keeping two sources in sync with
one another
- If you have hundreds, thousands, or even millions of posts, you would not want to store all of those post IDs on a
given category
- Etc.
This is where the `join` field is especially powerful. With it, you only need to store the `category_id` on the `post`,
and Payload will automatically join in related posts for you when you query for categories. The related category is only
stored on the post itself - and is not duplicated on both sides. However, the `join` field is what enables
bi-directional APIs and UI for you.
### Using the Join field to have full control of your database schema
For typical polymorphic / many relationships, if you're using Postgres or SQLite, Payload will automatically create
a `posts_rels` table, which acts as a junction table to store all of a given document's relationships.
However, this might not be appropriate for your use case if you'd like to have more control over your database
architecture. You might not want to have that `_rels` table, and would prefer to maintain / control your own junction
table design.
<Banner type="success">
With the Join field, you can control your own junction table design, and avoid Payload's automatic _rels table creation.
</Banner>
The `join` field can be used in conjunction with _any_ collection - and if you wanted to define your own "junction"
collection, which, say, is called `categories_posts` and has a `post_id` and a `category_id` column, you can achieve
complete control over the shape of that junction table.
You could go a step further and leverage the `admin.hidden` property of the `categories_posts` collection to hide the
collection from appearing in the Admin UI navigation.
#### Specifying additional fields on relationships
Another very powerful use case of the `join` field is to be able to define "context" fields on your relationships. Let's
say that you have Posts and Categories, and use join fields on both your Posts and Categories collection to join in
related docs from a new pseudo-junction collection called `categories_posts`. Now, the relations are stored in this
third junction collection, and can be surfaced on both Posts and Categories. But, importantly, you could add
additional "context" fields to this shared junction collection.
For example, on this `categories_posts` collection, in addition to having the `category` and
post` fields, we could add custom "context" fields like `featured` or `
spotlight`, which would allow you to store additional information directly on relationships. The `join` field gives you
complete control over any type of relational architecture in Payload, all wrapped up in a powerful Admin UI.
## Config Options
| Option | Description |
|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`collection`** \* | The `slug`s having the relationship field. |
| **`on`** \* | The relationship field name of the field that relates to collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
_\* An asterisk denotes that a property is required._
## Join Field Data
When a document is returned that for a Join field is populated with related documents. The structure returned is an
object with:
- `docs` an array of related documents or only IDs if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
```json
{
"id": "66e3431a3f23e684075aae9c",
"relatedPosts": {
"docs": [
{
"id": "66e3431a3f23e684075aaeb9",
// other fields...
"category": "66e3431a3f23e684075aae9c",
},
// { ... }
],
"hasNextPage": false
},
// other fields...
}
```
## Query Options
The Join Field supports custom queries to filter, sort, and limit the related documents that will be returned. In
addition to the specific query options for each Join Field, you can pass `joins: false` to disable all Join Field from
returning. This is useful for performance reasons when you don't need the related documents.
The following query options are supported:
| Property | Description |
|-------------|--------------------------------------------------------------|
| **`limit`** | The maximum related documents to be returned, default is 10. |
| **`where`** | An optional `Where` query to filter joined documents. |
| **`sort`** | A string used to order related results |
These can be applied to the local API, GraphQL, and REST API.
### Local API
By adding `joins` to the local API you can customize the request for each join field by the `name` of the field.
```js
const result = await db.findOne('categories', {
where: {
title: {
equals: 'My Category'
}
},
joins: {
relatedPosts: {
limit: 5,
where: {
title: {
equals: 'My Post'
}
},
sort: 'title'
}
}
})
```
### Rest API
The rest API supports the same query options as the local API. You can use the `joins` query parameter to customize the
request for each join field by the `name` of the field. For example, an API call to get a document with the related
posts limited to 5 and sorted by title:
`/api/categories/${id}?joins[relatedPosts][limit]=5&joins[relatedPosts][sort]=title`
You can specify as many `joins` parameters as needed for the same or different join fields for a single request.
### GraphQL
The GraphQL API supports the same query options as the local and REST APIs. You can specify the query options for each join field in your query.
Example:
```graphql
query {
Categories {
docs {
relatedPosts(
sort: "createdAt"
limit: 5
where: {
author: {
equals: "66e3431a3f23e684075aaeb9"
}
}
) {
docs {
title
}
hasNextPage
}
}
}
}
```

View File

@@ -37,16 +37,17 @@ export const MyJSONField: Field = {
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -40,16 +40,17 @@ export const MyNumberField: Field = {
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -40,16 +40,17 @@ export const MyPointField: Field = {
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](../admin/fields#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -40,17 +40,18 @@ export const MyRadioField: Field = {
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -50,16 +50,17 @@ export const MyRelationshipField: Field = {
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._
@@ -196,6 +197,12 @@ You can learn more about writing queries [here](/docs/queries/overview).
<strong>payload/shared</strong> in your validate function.
</Banner>
## Bi-directional relationships
The `relationship` field on its own is used to define relationships for the document that contains the relationship field, and this can be considered as a "one-way" relationship. For example, if you have a Post that has a `category` relationship field on it, the related `category` itself will not surface any information about the posts that have the category set.
However, the `relationship` field can be used in conjunction with the `Join` field to produce powerful bi-directional relationship authoring capabilities. If you're interested in bi-directional relationships, check out the [documentation for the Join field](./join).
## How the data is saved
Given the variety of options possible within the `relationship` field type, the shape of the data needed for creating

View File

@@ -38,22 +38,23 @@ Right now, Payload is officially supporting two rich text editors:
## Config Options
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`editor`** | Override the rich text editor specified in your base configuration for this field. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`editor`** | Override the rich text editor specified in your base configuration for this field. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -33,27 +33,28 @@ export const MySelectField: Field = {
## Config Options
| Option | Description |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-options) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| Option | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-options) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -50,6 +50,7 @@ Each tab must have either a `name` or `label` and the required `fields` array. Y
| **`fields`** \* | The fields to render within this tab. |
| **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). (`name` must be present) |
| **`virtual`** | Provide `true` to disable field in the database (`name` must be present). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -37,19 +37,20 @@ export const MyTextField: Field = {
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`hasMany`** | Makes this field an ordered array of text instead of just a single text. |
| **`minRows`** | Minimum number of texts in the array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of texts in the array, if `hasMany` is set to true. |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -37,16 +37,17 @@ export const MyTextareaField: Field = {
| **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -47,22 +47,23 @@ export const MyUploadField: Field = {
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`*relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [Admin Options](../admin/fields#admin-options). |
| **`admin`** | Admin-specific configuration. [Admin Options](../admin/fields#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
_\* An asterisk denotes that a property is required._

View File

@@ -85,10 +85,12 @@ You can specify more options within the Local API vs. REST or GraphQL due to the
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents).|
| `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. |
| `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. |
| `pagination` | Set to false to return all documents and avoid querying for document counts. |
| `context` | [Context](/docs/hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. |
| `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. |
_There are more options available on an operation by operation basis outlined below._
@@ -205,6 +207,7 @@ const result = await payload.update({
fallbackLocale: false,
user: dummyUser,
overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true,
// If your collection supports uploads, you can upload
@@ -243,6 +246,7 @@ const result = await payload.update({
fallbackLocale: false,
user: dummyUser,
overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true,
// If your collection supports uploads, you can upload
@@ -269,6 +273,7 @@ const result = await payload.delete({
fallbackLocale: false,
user: dummyUser,
overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true,
})
```
@@ -292,6 +297,7 @@ const result = await payload.delete({
fallbackLocale: false,
user: dummyUser,
overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true,
})
```
@@ -428,6 +434,7 @@ const result = await payload.updateGlobal({
fallbackLocale: false,
user: dummyUser,
overrideAccess: false,
overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks.
showHiddenFields: true,
})
```

View File

@@ -136,12 +136,28 @@ const beforeEmail: BeforeEmail<FormSubmission> = (emailsToSend, beforeChangePara
}
```
### `defaultToEmail`
Provide a fallback for the email address to send form submissions to. If the email in form configuration does not have a to email set, this email address will be used. If this is not provided then it falls back to the `defaultFromAddress` in your [email configuration](https://payloadcms.com/docs/beta/email/overview).
```ts
// payload.config.ts
formBuilder({
// ...
defaultToEmail: 'test@example.com',
})
```
### `formOverrides`
Override anything on the `forms` collection by sending a [Payload Collection Config](https://payloadcms.com/docs/configuration/collections) to the `formOverrides` property.
Note that the `fields` property is a function that receives the default fields and returns an array of fields. This is because the `fields` property is a special case that is merged with the default fields, rather than replacing them. This allows you to map over default fields and modify them as needed.
<Banner type="warning">
Good to know: The form collection is publicly available to read by default. The emails field is locked for authenticated users only. If you have any frontend users you should override the access permissions for both the collection and the emails field to make sure you don't leak out any private emails.
</Banner>
```ts
// payload.config.ts
formBuilder({
@@ -149,7 +165,7 @@ formBuilder({
formOverrides: {
slug: 'contact-forms',
access: {
read: () => true,
read: ({ req: { user } }) => !!user, // authenticated users only
update: () => false,
},
fields: ({ defaultFields }) => {
@@ -174,7 +190,7 @@ Override anything on the `form-submissions` collection by sending a [Payload Col
control](https://payloadcms.com/docs/access-control/collections) to restrict the `update` and
`read` operations on the `form-submissions` collection. This is because _anyone_ should be able to
create a form submission, even from a public-facing website, but _no one_ should be able to update
a submission one it has been created, or read a submission unless they have permission. You can
a submission once it has been created, or read a submission unless they have permission. You can
override this behavior or any other property as needed.
</Banner>
@@ -396,7 +412,19 @@ formBuilder({
## Email
This plugin relies on the [email configuration](https://payloadcms.com/docs/email/overview) defined in your `payload.init()`. It will read from your config and attempt to send your emails using the credentials provided.
This plugin relies on the [email configuration](https://payloadcms.com/docs/beta/email/overview) defined in your payload configuration. It will read from your config and attempt to send your emails using the credentials provided.
### Email formatting
The email contents supports rich text which will be serialised to HTML on the server before being sent. By default it reads the global configuration of your rich text editor.
The email subject and body supports inserting dynamic fields from the form submission data using the `{{field_name}}` syntax. For example, if you have a field called `name` in your form, you can include this in the email body like so:
```html
Thank you for your submission, {{name}}!
```
You can also use `{{*}}` as a wildcard to output all the data in a key:value format and `{{*:table}}` to output all the data in a table format.
## TypeScript

View File

@@ -243,5 +243,5 @@ official [Nested Docs Plugin Example](https://github.com/payloadcms/payload/tree
demonstrates exactly how to configure this plugin in Payload and implement it on your front-end.
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an
official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website)
and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere), both of which use this
and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce), both of which use this
plugin.

View File

@@ -119,11 +119,33 @@ A function that allows you to return any meta title, including from document's c
{
// ...
seoPlugin({
generateTitle: ({ ...docInfo, doc, locale, req }) => `Website.com — ${doc?.title}`,
generateTitle: ({ doc }) => `Website.com — ${doc?.title}`,
})
}
```
All "generate" functions receive the following arguments:
| Argument | Description |
| --- | --- |
| **`collectionConfig`** | The configuration of the collection. |
| **`collectionSlug`** | The slug of the collection. |
| **`doc`** | The data of the current document. |
| **`docPermissions`** | The permissions of the document. |
| **`globalConfig`** | The configuration of the global. |
| **`globalSlug`** | The slug of the global. |
| **`hasPublishPermission`** | Whether the user has permission to publish the document. |
| **`hasSavePermission`** | Whether the user has permission to save the document. |
| **`id`** | The ID of the document. |
| **`initialData`** | The initial data of the document. |
| **`initialState`** | The initial state of the document. |
| **`locale`** | The locale of the document. |
| **`preferencesKey`** | The preferences key of the document. |
| **`publishedDoc`** | The published document. |
| **`req`** | The Payload request object containing `user`, `payload`, `i18n`, etc. |
| **`title`** | The title of the document. |
| **`versionsCount`** | The number of versions of the document. |
##### `generateDescription`
A function that allows you to return any meta description, including from document's content.
@@ -133,11 +155,13 @@ A function that allows you to return any meta description, including from docume
{
// ...
seoPlugin({
generateDescription: ({ ...docInfo, doc, locale, req }) => doc?.excerpt,
generateDescription: ({ doc }) => doc?.excerpt,
})
}
```
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
##### `generateImage`
A function that allows you to return any meta image, including from document's content.
@@ -147,11 +171,13 @@ A function that allows you to return any meta image, including from document's c
{
// ...
seoPlugin({
generateImage: ({ ...docInfo, doc, locale, req }) => doc?.featuredImage,
generateImage: ({ doc }) => doc?.featuredImage,
})
}
```
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
##### `generateURL`
A function called by the search preview component to display the actual URL of your page.
@@ -161,12 +187,14 @@ A function called by the search preview component to display the actual URL of y
{
// ...
seoPlugin({
generateURL: ({ ...docInfo, doc, locale, req }) =>
`https://yoursite.com/${collection?.slug}/${doc?.slug}`,
generateURL: ({ doc, collectionSlug }) =>
`https://yoursite.com/${collectionSlug}/${doc?.slug}`,
})
}
```
For a full list of arguments, see the [`generateTitle`](#generateTitle) function.
#### `interfaceName`
Rename the meta group interface name that is generated for TypeScript and GraphQL.

View File

@@ -573,6 +573,11 @@ In addition to the dynamically generated endpoints above Payload also has REST e
Additional REST API endpoints can be added to your application by providing an array of `endpoints` in various places within a Payload Config. Custom endpoints are useful for adding additional middleware on existing routes or for building custom functionality into Payload apps and plugins. Endpoints can be added at the top of the Payload Config, `collections`, and `globals` and accessed respective of the api and slugs you have configured.
<Banner type="warning">
Custom endpoints are not authenticated by default. You are responsible for securing your own endpoints.
</Banner>
Each endpoint object needs to have:
| Property | Description |
@@ -625,6 +630,22 @@ export const Orders: CollectionConfig = {
// data to update the document with
}
})
return Response.json({
message: 'successfully updated tracking info'
})
}
},
{
path: '/:id/forbidden',
method: 'post',
handler: async (req) => {
// this is an example of an authenticated endpoint
if (!req.user) {
return Response.json({ error: 'forbidden' }, { status: 403 })
}
// do something
return Response.json({
message: 'successfully updated tracking info'
})

View File

@@ -23,10 +23,10 @@
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"graphql": "^16.9.0",
"next": "15.0.0-canary.104",
"next": "15.0.0-canary.160",
"payload": "3.0.0-beta.106",
"react": "19.0.0-rc-06d0b89e-20240801",
"react-dom": "19.0.0-rc-06d0b89e-20240801"
"react": "19.0.0-rc-5dcb0097-20240918",
"react-dom": "19.0.0-rc-5dcb0097-20240918"
},
"devDependencies": {
"@payloadcms/graphql": "3.0.0-beta.106",

View File

@@ -55,59 +55,108 @@ import { CustomDefaultRootView as CustomDefaultRootView_53 } from '@/components/
import { CustomMinimalRootView as CustomMinimalRootView_54 } from '@/components/views/CustomMinimalRootView'
export const importMap = {
"@/collections/Fields/array/components/server/Label#CustomArrayFieldLabelServer": CustomArrayFieldLabelServer_0,
"@/collections/Fields/array/components/server/Field#CustomArrayFieldServer": CustomArrayFieldServer_1,
"@/collections/Fields/array/components/client/Label#CustomArrayFieldLabelClient": CustomArrayFieldLabelClient_2,
"@/collections/Fields/array/components/client/Field#CustomArrayFieldClient": CustomArrayFieldClient_3,
"@/collections/Fields/blocks/components/server/Field#CustomBlocksFieldServer": CustomBlocksFieldServer_4,
"@/collections/Fields/blocks/components/client/Field#CustomBlocksFieldClient": CustomBlocksFieldClient_5,
"@/collections/Fields/checkbox/components/server/Label#CustomCheckboxFieldLabelServer": CustomCheckboxFieldLabelServer_6,
"@/collections/Fields/checkbox/components/server/Field#CustomCheckboxFieldServer": CustomCheckboxFieldServer_7,
"@/collections/Fields/checkbox/components/client/Label#CustomCheckboxFieldLabelClient": CustomCheckboxFieldLabelClient_8,
"@/collections/Fields/checkbox/components/client/Field#CustomCheckboxFieldClient": CustomCheckboxFieldClient_9,
"@/collections/Fields/date/components/server/Label#CustomDateFieldLabelServer": CustomDateFieldLabelServer_10,
"@/collections/Fields/date/components/server/Field#CustomDateFieldServer": CustomDateFieldServer_11,
"@/collections/Fields/date/components/client/Label#CustomDateFieldLabelClient": CustomDateFieldLabelClient_12,
"@/collections/Fields/date/components/client/Field#CustomDateFieldClient": CustomDateFieldClient_13,
"@/collections/Fields/email/components/server/Label#CustomEmailFieldLabelServer": CustomEmailFieldLabelServer_14,
"@/collections/Fields/email/components/server/Field#CustomEmailFieldServer": CustomEmailFieldServer_15,
"@/collections/Fields/email/components/client/Label#CustomEmailFieldLabelClient": CustomEmailFieldLabelClient_16,
"@/collections/Fields/email/components/client/Field#CustomEmailFieldClient": CustomEmailFieldClient_17,
"@/collections/Fields/number/components/server/Label#CustomNumberFieldLabelServer": CustomNumberFieldLabelServer_18,
"@/collections/Fields/number/components/server/Field#CustomNumberFieldServer": CustomNumberFieldServer_19,
"@/collections/Fields/number/components/client/Label#CustomNumberFieldLabelClient": CustomNumberFieldLabelClient_20,
"@/collections/Fields/number/components/client/Field#CustomNumberFieldClient": CustomNumberFieldClient_21,
"@/collections/Fields/point/components/server/Label#CustomPointFieldLabelServer": CustomPointFieldLabelServer_22,
"@/collections/Fields/point/components/server/Field#CustomPointFieldServer": CustomPointFieldServer_23,
"@/collections/Fields/point/components/client/Label#CustomPointFieldLabelClient": CustomPointFieldLabelClient_24,
"@/collections/Fields/point/components/client/Field#CustomPointFieldClient": CustomPointFieldClient_25,
"@/collections/Fields/radio/components/server/Label#CustomRadioFieldLabelServer": CustomRadioFieldLabelServer_26,
"@/collections/Fields/radio/components/server/Field#CustomRadioFieldServer": CustomRadioFieldServer_27,
"@/collections/Fields/radio/components/client/Label#CustomRadioFieldLabelClient": CustomRadioFieldLabelClient_28,
"@/collections/Fields/radio/components/client/Field#CustomRadioFieldClient": CustomRadioFieldClient_29,
"@/collections/Fields/relationship/components/server/Label#CustomRelationshipFieldLabelServer": CustomRelationshipFieldLabelServer_30,
"@/collections/Fields/relationship/components/server/Field#CustomRelationshipFieldServer": CustomRelationshipFieldServer_31,
"@/collections/Fields/relationship/components/client/Label#CustomRelationshipFieldLabelClient": CustomRelationshipFieldLabelClient_32,
"@/collections/Fields/relationship/components/client/Field#CustomRelationshipFieldClient": CustomRelationshipFieldClient_33,
"@/collections/Fields/select/components/server/Label#CustomSelectFieldLabelServer": CustomSelectFieldLabelServer_34,
"@/collections/Fields/select/components/server/Field#CustomSelectFieldServer": CustomSelectFieldServer_35,
"@/collections/Fields/select/components/client/Label#CustomSelectFieldLabelClient": CustomSelectFieldLabelClient_36,
"@/collections/Fields/select/components/client/Field#CustomSelectFieldClient": CustomSelectFieldClient_37,
"@/collections/Fields/text/components/server/Label#CustomTextFieldLabelServer": CustomTextFieldLabelServer_38,
"@/collections/Fields/text/components/server/Field#CustomTextFieldServer": CustomTextFieldServer_39,
"@/collections/Fields/text/components/client/Label#CustomTextFieldLabelClient": CustomTextFieldLabelClient_40,
"@/collections/Fields/text/components/client/Field#CustomTextFieldClient": CustomTextFieldClient_41,
"@/collections/Fields/textarea/components/server/Label#CustomTextareaFieldLabelServer": CustomTextareaFieldLabelServer_42,
"@/collections/Fields/textarea/components/server/Field#CustomTextareaFieldServer": CustomTextareaFieldServer_43,
"@/collections/Fields/textarea/components/client/Label#CustomTextareaFieldLabelClient": CustomTextareaFieldLabelClient_44,
"@/collections/Fields/textarea/components/client/Field#CustomTextareaFieldClient": CustomTextareaFieldClient_45,
"@/collections/Views/components/CustomTabEditView#CustomTabEditView": CustomTabEditView_46,
"@/collections/Views/components/CustomDefaultEditView#CustomDefaultEditView": CustomDefaultEditView_47,
"@/collections/RootViews/components/CustomRootEditView#CustomRootEditView": CustomRootEditView_48,
"@/components/afterNavLinks/LinkToCustomView#LinkToCustomView": LinkToCustomView_49,
"@/components/afterNavLinks/LinkToCustomMinimalView#LinkToCustomMinimalView": LinkToCustomMinimalView_50,
"@/components/afterNavLinks/LinkToCustomDefaultView#LinkToCustomDefaultView": LinkToCustomDefaultView_51,
"@/components/views/CustomRootView#CustomRootView": CustomRootView_52,
"@/components/views/CustomDefaultRootView#CustomDefaultRootView": CustomDefaultRootView_53,
"@/components/views/CustomMinimalRootView#CustomMinimalRootView": CustomMinimalRootView_54
'@/collections/Fields/array/components/server/Label#CustomArrayFieldLabelServer':
CustomArrayFieldLabelServer_0,
'@/collections/Fields/array/components/server/Field#CustomArrayFieldServer':
CustomArrayFieldServer_1,
'@/collections/Fields/array/components/client/Label#CustomArrayFieldLabelClient':
CustomArrayFieldLabelClient_2,
'@/collections/Fields/array/components/client/Field#CustomArrayFieldClient':
CustomArrayFieldClient_3,
'@/collections/Fields/blocks/components/server/Field#CustomBlocksFieldServer':
CustomBlocksFieldServer_4,
'@/collections/Fields/blocks/components/client/Field#CustomBlocksFieldClient':
CustomBlocksFieldClient_5,
'@/collections/Fields/checkbox/components/server/Label#CustomCheckboxFieldLabelServer':
CustomCheckboxFieldLabelServer_6,
'@/collections/Fields/checkbox/components/server/Field#CustomCheckboxFieldServer':
CustomCheckboxFieldServer_7,
'@/collections/Fields/checkbox/components/client/Label#CustomCheckboxFieldLabelClient':
CustomCheckboxFieldLabelClient_8,
'@/collections/Fields/checkbox/components/client/Field#CustomCheckboxFieldClient':
CustomCheckboxFieldClient_9,
'@/collections/Fields/date/components/server/Label#CustomDateFieldLabelServer':
CustomDateFieldLabelServer_10,
'@/collections/Fields/date/components/server/Field#CustomDateFieldServer':
CustomDateFieldServer_11,
'@/collections/Fields/date/components/client/Label#CustomDateFieldLabelClient':
CustomDateFieldLabelClient_12,
'@/collections/Fields/date/components/client/Field#CustomDateFieldClient':
CustomDateFieldClient_13,
'@/collections/Fields/email/components/server/Label#CustomEmailFieldLabelServer':
CustomEmailFieldLabelServer_14,
'@/collections/Fields/email/components/server/Field#CustomEmailFieldServer':
CustomEmailFieldServer_15,
'@/collections/Fields/email/components/client/Label#CustomEmailFieldLabelClient':
CustomEmailFieldLabelClient_16,
'@/collections/Fields/email/components/client/Field#CustomEmailFieldClient':
CustomEmailFieldClient_17,
'@/collections/Fields/number/components/server/Label#CustomNumberFieldLabelServer':
CustomNumberFieldLabelServer_18,
'@/collections/Fields/number/components/server/Field#CustomNumberFieldServer':
CustomNumberFieldServer_19,
'@/collections/Fields/number/components/client/Label#CustomNumberFieldLabelClient':
CustomNumberFieldLabelClient_20,
'@/collections/Fields/number/components/client/Field#CustomNumberFieldClient':
CustomNumberFieldClient_21,
'@/collections/Fields/point/components/server/Label#CustomPointFieldLabelServer':
CustomPointFieldLabelServer_22,
'@/collections/Fields/point/components/server/Field#CustomPointFieldServer':
CustomPointFieldServer_23,
'@/collections/Fields/point/components/client/Label#CustomPointFieldLabelClient':
CustomPointFieldLabelClient_24,
'@/collections/Fields/point/components/client/Field#CustomPointFieldClient':
CustomPointFieldClient_25,
'@/collections/Fields/radio/components/server/Label#CustomRadioFieldLabelServer':
CustomRadioFieldLabelServer_26,
'@/collections/Fields/radio/components/server/Field#CustomRadioFieldServer':
CustomRadioFieldServer_27,
'@/collections/Fields/radio/components/client/Label#CustomRadioFieldLabelClient':
CustomRadioFieldLabelClient_28,
'@/collections/Fields/radio/components/client/Field#CustomRadioFieldClient':
CustomRadioFieldClient_29,
'@/collections/Fields/relationship/components/server/Label#CustomRelationshipFieldLabelServer':
CustomRelationshipFieldLabelServer_30,
'@/collections/Fields/relationship/components/server/Field#CustomRelationshipFieldServer':
CustomRelationshipFieldServer_31,
'@/collections/Fields/relationship/components/client/Label#CustomRelationshipFieldLabelClient':
CustomRelationshipFieldLabelClient_32,
'@/collections/Fields/relationship/components/client/Field#CustomRelationshipFieldClient':
CustomRelationshipFieldClient_33,
'@/collections/Fields/select/components/server/Label#CustomSelectFieldLabelServer':
CustomSelectFieldLabelServer_34,
'@/collections/Fields/select/components/server/Field#CustomSelectFieldServer':
CustomSelectFieldServer_35,
'@/collections/Fields/select/components/client/Label#CustomSelectFieldLabelClient':
CustomSelectFieldLabelClient_36,
'@/collections/Fields/select/components/client/Field#CustomSelectFieldClient':
CustomSelectFieldClient_37,
'@/collections/Fields/text/components/server/Label#CustomTextFieldLabelServer':
CustomTextFieldLabelServer_38,
'@/collections/Fields/text/components/server/Field#CustomTextFieldServer':
CustomTextFieldServer_39,
'@/collections/Fields/text/components/client/Label#CustomTextFieldLabelClient':
CustomTextFieldLabelClient_40,
'@/collections/Fields/text/components/client/Field#CustomTextFieldClient':
CustomTextFieldClient_41,
'@/collections/Fields/textarea/components/server/Label#CustomTextareaFieldLabelServer':
CustomTextareaFieldLabelServer_42,
'@/collections/Fields/textarea/components/server/Field#CustomTextareaFieldServer':
CustomTextareaFieldServer_43,
'@/collections/Fields/textarea/components/client/Label#CustomTextareaFieldLabelClient':
CustomTextareaFieldLabelClient_44,
'@/collections/Fields/textarea/components/client/Field#CustomTextareaFieldClient':
CustomTextareaFieldClient_45,
'@/collections/Views/components/CustomTabEditView#CustomTabEditView': CustomTabEditView_46,
'@/collections/Views/components/CustomDefaultEditView#CustomDefaultEditView':
CustomDefaultEditView_47,
'@/collections/RootViews/components/CustomRootEditView#CustomRootEditView': CustomRootEditView_48,
'@/components/afterNavLinks/LinkToCustomView#LinkToCustomView': LinkToCustomView_49,
'@/components/afterNavLinks/LinkToCustomMinimalView#LinkToCustomMinimalView':
LinkToCustomMinimalView_50,
'@/components/afterNavLinks/LinkToCustomDefaultView#LinkToCustomDefaultView':
LinkToCustomDefaultView_51,
'@/components/views/CustomRootView#CustomRootView': CustomRootView_52,
'@/components/views/CustomDefaultRootView#CustomDefaultRootView': CustomDefaultRootView_53,
'@/components/views/CustomMinimalRootView#CustomMinimalRootView': CustomMinimalRootView_54,
}

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.107",
"version": "3.0.0-beta.108",
"private": true,
"type": "module",
"scripts": {
@@ -34,7 +34,6 @@
"build:plugin-form-builder": "turbo build --filter \"@payloadcms/plugin-form-builder\"",
"build:plugin-nested-docs": "turbo build --filter \"@payloadcms/plugin-nested-docs\"",
"build:plugin-redirects": "turbo build --filter \"@payloadcms/plugin-redirects\"",
"build:plugin-relationship-object-ids": "turbo build --filter \"@payloadcms/plugin-relationship-object-ids\"",
"build:plugin-search": "turbo build --filter \"@payloadcms/plugin-search\"",
"build:plugin-sentry": "turbo build --filter \"@payloadcms/plugin-sentry\"",
"build:plugin-seo": "turbo build --filter \"@payloadcms/plugin-seo\"",
@@ -70,6 +69,8 @@
"lint:fix": "turbo run lint:fix --concurrency 1 --continue",
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"prepare": "husky",
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
"reinstall": "pnpm clean:all && pnpm install",
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
@@ -83,8 +84,8 @@
"test:e2e": "pnpm runts ./test/runE2E.ts",
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
"test:e2e:prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd .. && pnpm runts ./test/runE2E.ts --prod",
"test:e2e:prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd .. && pnpm runts ./test/runE2E.ts --prod",
"test:e2e:prod": "pnpm prepare-run-test-against-prod && pnpm runts ./test/runE2E.ts --prod",
"test:e2e:prod:ci": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod",
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
@@ -104,7 +105,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.6.2",
"@next/bundle-analyzer": "15.0.0-canary.104",
"@next/bundle-analyzer": "15.0.0-canary.160",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -118,8 +119,8 @@
"@types/minimist": "1.2.5",
"@types/node": "22.5.4",
"@types/prompts": "^2.4.5",
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/semver": "^7.5.3",
"@types/shelljs": "0.8.15",
"chalk": "^4.1.2",
@@ -142,15 +143,15 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "15.0.0-canary.104",
"next": "15.0.0-canary.160",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.46.0",
"playwright-core": "1.46.0",
"prettier": "3.3.3",
"prompts": "2.4.2",
"react": "19.0.0-rc-06d0b89e-20240801",
"react-dom": "19.0.0-rc-06d0b89e-20240801",
"react": "19.0.0-rc-5dcb0097-20240918",
"react-dom": "19.0.0-rc-5dcb0097-20240918",
"rimraf": "3.0.2",
"semver": "^7.5.4",
"sharp": "0.32.6",
@@ -160,12 +161,12 @@
"swc-plugin-transform-remove-imports": "1.15.0",
"tempy": "1.0.1",
"tsx": "4.19.1",
"turbo": "^2.1.1",
"turbo": "^2.1.2",
"typescript": "5.6.2"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
"react": "^19.0.0 || ^19.0.0-rc-5dcb0097-20240918",
"react-dom": "^19.0.0 || ^19.0.0-rc-5dcb0097-20240918"
},
"packageManager": "pnpm@9.7.1",
"engines": {
@@ -178,8 +179,8 @@
"domexception": "4"
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"copyfiles": "$copyfiles",
"cross-env": "$cross-env",
"dotenv": "$dotenv",
@@ -192,8 +193,8 @@
}
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0"
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
},
"workspaces:": [
"packages/*",

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.107",
"version": "3.0.0-beta.108",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,8 +1,12 @@
import execa from 'execa'
import fse from 'fs-extra'
import type { CliArgs, PackageManager } from '../types.js'
export function getPackageManager(args: { cliArgs?: CliArgs; projectDir: string }): PackageManager {
export async function getPackageManager(args: {
cliArgs?: CliArgs
projectDir: string
}): Promise<PackageManager> {
const { cliArgs, projectDir } = args
try {
@@ -16,6 +20,9 @@ export function getPackageManager(args: { cliArgs?: CliArgs; projectDir: string
detected = 'npm'
} else if (cliArgs?.['--use-bun'] || fse.existsSync(`${projectDir}/bun.lockb`)) {
detected = 'bun'
} else if (await commandExists('pnpm')) {
// Prefer pnpm if it's installed
detected = 'pnpm'
} else {
// Otherwise check the execution environment
detected = getEnvironmentPackageManager()
@@ -44,3 +51,12 @@ function getEnvironmentPackageManager(): PackageManager {
return 'npm'
}
async function commandExists(command: string): Promise<boolean> {
try {
await execa.command(`command -v ${command}`)
return true
} catch {
return false
}
}

View File

@@ -133,7 +133,7 @@ export class Main {
? path.dirname(nextConfigPath)
: path.resolve(process.cwd(), slugify(projectName))
const packageManager = getPackageManager({ cliArgs: this.args, projectDir })
const packageManager = await getPackageManager({ cliArgs: this.args, projectDir })
if (nextConfigPath) {
p.log.step(

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.107",
"version": "3.0.0-beta.108",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -38,13 +38,14 @@
"bson-objectid": "2.0.4",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"uuid": "10.0.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
"@types/mongoose-aggregate-paginate-v2": "1.0.6",
"mongodb": "4.17.1",
"mongodb-memory-server": "^9",
"payload": "workspace:*"

View File

@@ -3,6 +3,7 @@ import type { Create, Document, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const create: Create = async function create(
@@ -12,8 +13,15 @@ export const create: Create = async function create(
const Model = this.collections[collection]
const options = await withSession(this, req)
let doc
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields: this.payload.collections[collection].config.fields,
})
try {
;[doc] = await Model.create([data], options)
;[doc] = await Model.create([sanitizedData], options)
} catch (error) {
handleError({ collection, error, req })
}

View File

@@ -3,6 +3,7 @@ import type { CreateGlobal, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const createGlobal: CreateGlobal = async function createGlobal(
@@ -10,10 +11,16 @@ export const createGlobal: CreateGlobal = async function createGlobal(
{ slug, data, req = {} as PayloadRequest },
) {
const Model = this.globals
const global = {
globalType: slug,
...data,
}
const global = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
globalType: slug,
...data,
},
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
})
const options = await withSession(this, req)
let [result] = (await Model.create([global], options)) as any

View File

@@ -1,30 +1,51 @@
import type { CreateGlobalVersion, Document, PayloadRequest } from 'payload'
import {
buildVersionGlobalFields,
type CreateGlobalVersion,
type Document,
type PayloadRequest,
} from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter,
{ autosave, createdAt, globalSlug, parent, req = {} as PayloadRequest, updatedAt, versionData },
{
autosave,
createdAt,
globalSlug,
parent,
publishedLocale,
req = {} as PayloadRequest,
snapshot,
updatedAt,
versionData,
},
) {
const VersionModel = this.versions[globalSlug]
const options = await withSession(this, req)
const [doc] = await VersionModel.create(
[
{
autosave,
createdAt,
latest: true,
parent,
updatedAt,
version: versionData,
},
],
options,
req,
)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
})
const [doc] = await VersionModel.create([data], options, req)
await VersionModel.updateMany(
{

View File

@@ -1,7 +1,13 @@
import type { CreateVersion, Document, PayloadRequest } from 'payload'
import {
buildVersionCollectionFields,
type CreateVersion,
type Document,
type PayloadRequest,
} from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const createVersion: CreateVersion = async function createVersion(
@@ -11,7 +17,9 @@ export const createVersion: CreateVersion = async function createVersion(
collectionSlug,
createdAt,
parent,
publishedLocale,
req = {} as PayloadRequest,
snapshot,
updatedAt,
versionData,
},
@@ -19,20 +27,25 @@ export const createVersion: CreateVersion = async function createVersion(
const VersionModel = this.versions[collectionSlug]
const options = await withSession(this, req)
const [doc] = await VersionModel.create(
[
{
autosave,
createdAt,
latest: true,
parent,
updatedAt,
version: versionData,
},
],
options,
req,
)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
),
})
const [doc] = await VersionModel.create([data], options, req)
await VersionModel.updateMany(
{
@@ -44,7 +57,7 @@ export const createVersion: CreateVersion = async function createVersion(
},
{
parent: {
$eq: parent,
$eq: data.parent,
},
},
{

View File

@@ -6,12 +6,24 @@ import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
export const find: Find = async function find(
this: MongooseAdapter,
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where },
{
collection,
joins = {},
limit,
locale,
page,
pagination,
projection,
req = {} as PayloadRequest,
sort: sortArg,
where,
},
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
@@ -50,6 +62,7 @@ export const find: Find = async function find(
options,
page,
pagination,
projection,
sort,
useEstimatedCount,
}
@@ -88,7 +101,24 @@ export const find: Find = async function find(
}
}
const result = await Model.paginate(query, paginationOptions)
let result
const aggregate = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
limit,
locale,
query,
})
// build join aggregation
if (aggregate) {
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
} else {
result = await Model.paginate(query, paginationOptions)
}
const docs = JSON.parse(JSON.stringify(result.docs))
return {

View File

@@ -25,6 +25,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
) {
const Model = this.versions[global]
const versionFields = buildVersionGlobalFields(
this.payload.config,
this.payload.globals.config.find(({ slug }) => slug === global),
)
const options = {

View File

@@ -3,14 +3,16 @@ import type { Document, FindOne, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
{ collection, joins, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
const options: MongooseQueryOptions = {
...(await withSession(this, req)),
lean: true,
@@ -22,7 +24,22 @@ export const findOne: FindOne = async function findOne(
where,
})
const doc = await Model.findOne(query, {}, options)
const aggregate = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
limit: 1,
locale,
query,
})
let doc
if (aggregate) {
;[doc] = await Model.aggregate(aggregate, options)
} else {
doc = await Model.findOne(query, {}, options)
}
if (!doc) {
return null

View File

@@ -2,6 +2,7 @@ import type { PaginateOptions } from 'mongoose'
import type { Init, SanitizedCollectionConfig } from 'payload'
import mongoose from 'mongoose'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
@@ -21,7 +22,7 @@ export const init: Init = function init(this: MongooseAdapter) {
if (collection.versions) {
const versionModelName = getDBName({ config: collection, versions: true })
const versionCollectionFields = buildVersionCollectionFields(collection)
const versionCollectionFields = buildVersionCollectionFields(this.payload.config, collection)
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
disableUnique: true,
@@ -40,12 +41,16 @@ export const init: Init = function init(this: MongooseAdapter) {
}),
)
if (Object.keys(collection.joins).length > 0) {
versionSchema.plugin(mongooseAggregatePaginate)
}
const model = mongoose.model(
versionModelName,
versionSchema,
this.autoPluralization === true ? undefined : versionModelName,
) as CollectionModel
// this.payload.versions[collection.slug] = model;
this.versions[collection.slug] = model
}
@@ -64,7 +69,7 @@ export const init: Init = function init(this: MongooseAdapter) {
if (global.versions) {
const versionModelName = getDBName({ config: global, versions: true })
const versionGlobalFields = buildVersionGlobalFields(global)
const versionGlobalFields = buildVersionGlobalFields(this.payload.config, global)
const versionSchema = buildSchema(this.payload.config, versionGlobalFields, {
disableUnique: true,

View File

@@ -1,6 +1,7 @@
import type { PaginateOptions, Schema } from 'mongoose'
import type { SanitizedCollectionConfig, SanitizedConfig } from 'payload'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
@@ -42,5 +43,9 @@ export const buildCollectionSchema = (
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
if (Object.keys(collection.joins).length > 0) {
schema.plugin(mongooseAggregatePaginate)
}
return schema
}

View File

@@ -34,6 +34,7 @@ import {
fieldAffectsData,
fieldIsLocalized,
fieldIsPresentationalOnly,
fieldIsVirtual,
tabHasName,
} from 'payload/shared'
@@ -136,6 +137,10 @@ export const buildSchema = (
const schema = new mongoose.Schema(fields, options)
schemaFields.forEach((field) => {
if (fieldIsVirtual(field)) {
return
}
if (!fieldIsPresentationalOnly(field)) {
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]

View File

@@ -165,7 +165,7 @@ export async function buildSearchParam({
const subQuery = priorQueryResult.value
const result = await SubModel.find(subQuery, subQueryOptions)
const $in = result.map((doc) => doc._id.toString())
const $in = result.map((doc) => doc._id)
// If it is the last recursion
// then pass through the search param

View File

@@ -1,5 +1,6 @@
import type { Field, TabAsField } from 'payload'
import ObjectIdImport from 'bson-objectid'
import mongoose from 'mongoose'
import { createArrayFromCommaDelineated } from 'payload'
@@ -11,6 +12,8 @@ type SanitizeQueryValueArgs = {
val: any
}
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
export const sanitizeQueryValue = ({
field,
hasCustomID,
@@ -26,21 +29,49 @@ export const sanitizeQueryValue = ({
let formattedOperator = operator
// Disregard invalid _ids
if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) {
if (!hasCustomID) {
const isValid = mongoose.Types.ObjectId.isValid(val)
if (path === '_id') {
if (typeof val === 'string' && val.split(',').length === 1) {
if (!hasCustomID) {
const isValid = mongoose.Types.ObjectId.isValid(val)
if (!isValid) {
return { operator: formattedOperator, val: undefined }
if (!isValid) {
return { operator: formattedOperator, val: undefined }
} else {
if (['in', 'not_in'].includes(operator)) {
formattedValue = createArrayFromCommaDelineated(formattedValue).map((id) =>
ObjectId(id),
)
} else {
formattedValue = ObjectId(val)
}
}
}
}
if (field.type === 'number') {
const parsedNumber = parseFloat(val)
if (field.type === 'number') {
const parsedNumber = parseFloat(val)
if (Number.isNaN(parsedNumber)) {
return { operator: formattedOperator, val: undefined }
if (Number.isNaN(parsedNumber)) {
return { operator: formattedOperator, val: undefined }
}
}
} else if (Array.isArray(val)) {
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
const newValues = [inVal]
if (!hasCustomID) {
if (mongoose.Types.ObjectId.isValid(inVal)) {
newValues.push(ObjectId(inVal))
}
}
if (field.type === 'number') {
const parsedNumber = parseFloat(inVal)
if (!Number.isNaN(parsedNumber)) {
newValues.push(parsedNumber)
}
}
return [...formattedValues, ...newValues]
}, [])
}
}
@@ -86,6 +117,13 @@ export const sanitizeQueryValue = ({
formattedValue.value &&
formattedValue.relationTo
) {
const { value } = formattedValue
const isValid = mongoose.Types.ObjectId.isValid(value)
if (isValid) {
formattedValue.value = ObjectId(value)
}
return {
rawQuery: {
$and: [
@@ -96,11 +134,11 @@ export const sanitizeQueryValue = ({
}
}
if (operator === 'in' && Array.isArray(formattedValue)) {
if (['in', 'not_in'].includes(operator) && Array.isArray(formattedValue)) {
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
const newValues = [inVal]
if (mongoose.Types.ObjectId.isValid(inVal)) {
newValues.push(new mongoose.Types.ObjectId(inVal))
newValues.push(ObjectId(inVal))
}
const parsedNumber = parseFloat(inVal)
@@ -111,6 +149,12 @@ export const sanitizeQueryValue = ({
return [...formattedValues, ...newValues]
}, [])
}
if (operator === 'contains' && typeof formattedValue === 'string') {
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
formattedValue = ObjectId(formattedValue)
}
}
}
// Set up specific formatting necessary by operators
@@ -152,12 +196,38 @@ export const sanitizeQueryValue = ({
}
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains') {
if (operator === 'contains' && !mongoose.Types.ObjectId.isValid(formattedValue)) {
formattedValue = {
$options: 'i',
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
}
}
if (operator === 'exists') {
formattedValue = formattedValue === 'true' || formattedValue === true
if (formattedValue) {
return {
rawQuery: {
$and: [
{ [path]: { $exists: true } },
{ [path]: { $ne: null } },
{ [path]: { $ne: '' } },
],
},
}
} else {
return {
rawQuery: {
$or: [
{ [path]: { $exists: false } },
{ [path]: { $eq: null } },
{ [path]: { $eq: '' } }, // Treat empty string as null / undefined
],
},
}
}
}
}
if (

View File

@@ -1,4 +1,11 @@
import type { IndexDefinition, IndexOptions, Model, PaginateModel, SchemaOptions } from 'mongoose'
import type {
AggregatePaginateModel,
IndexDefinition,
IndexOptions,
Model,
PaginateModel,
SchemaOptions,
} from 'mongoose'
import type {
ArrayField,
BlocksField,
@@ -9,6 +16,7 @@ import type {
EmailField,
Field,
GroupField,
JoinField,
JSONField,
NumberField,
Payload,
@@ -27,7 +35,10 @@ import type {
import type { BuildQueryArgs } from './queries/buildQuery.js'
export interface CollectionModel extends Model<any>, PaginateModel<any> {
export interface CollectionModel
extends Model<any>,
PaginateModel<any>,
AggregatePaginateModel<any> {
/** buildQuery is used to transform payload's where operator into what can be used by mongoose (e.g. id => _id) */
buildQuery: (args: BuildQueryArgs) => Promise<Record<string, unknown>> // TODO: Delete this
}
@@ -83,6 +94,7 @@ export type FieldToSchemaMap<TSchema> = {
date: FieldGeneratorFunction<TSchema, DateField>
email: FieldGeneratorFunction<TSchema, EmailField>
group: FieldGeneratorFunction<TSchema, GroupField>
join: FieldGeneratorFunction<TSchema, JoinField>
json: FieldGeneratorFunction<TSchema, JSONField>
number: FieldGeneratorFunction<TSchema, NumberField>
point: FieldGeneratorFunction<TSchema, PointField>

View File

@@ -3,6 +3,7 @@ import type { PayloadRequest, UpdateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
@@ -17,7 +18,14 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
}
let result
result = await Model.findOneAndUpdate({ globalType: slug }, data, options)
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields: this.payload.config.globals.find((global) => global.slug === slug).fields,
})
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
result = JSON.parse(JSON.stringify(result))

View File

@@ -1,21 +1,27 @@
import type { PayloadRequest, TypeWithID, UpdateGlobalVersionArgs } from 'payload'
import {
buildVersionGlobalFields,
type PayloadRequest,
type TypeWithID,
type UpdateGlobalVersionArgs,
} from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export async function updateGlobalVersion<T extends TypeWithID>(
this: MongooseAdapter,
{
id,
global,
global: globalSlug,
locale,
req = {} as PayloadRequest,
versionData,
where,
}: UpdateGlobalVersionArgs<T>,
) {
const VersionModel = this.versions[global]
const VersionModel = this.versions[globalSlug]
const whereToUse = where || { id: { equals: id } }
const options = {
...(await withSession(this, req)),
@@ -29,7 +35,16 @@ export async function updateGlobalVersion<T extends TypeWithID>(
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data: versionData,
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const result = JSON.parse(JSON.stringify(doc))

View File

@@ -4,6 +4,7 @@ import type { MongooseAdapter } from './index.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -26,8 +27,14 @@ export const updateOne: UpdateOne = async function updateOne(
let result
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields: this.payload.collections[collection].config.fields,
})
try {
result = await Model.findOneAndUpdate(query, data, options)
result = await Model.findOneAndUpdate(query, sanitizedData, options)
} catch (error) {
handleError({ collection, error, req })
}

View File

@@ -1,7 +1,8 @@
import type { PayloadRequest, UpdateVersion } from 'payload'
import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const updateVersion: UpdateVersion = async function updateVersion(
@@ -22,7 +23,16 @@ export const updateVersion: UpdateVersion = async function updateVersion(
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data: versionData,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
),
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const result = JSON.parse(JSON.stringify(doc))

View File

@@ -0,0 +1,174 @@
import type { PipelineStage } from 'mongoose'
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { buildSortParam } from '../queries/buildSortParam.js'
type BuildJoinAggregationArgs = {
adapter: MongooseAdapter
collection: CollectionSlug
collectionConfig: SanitizedCollectionConfig
joins: JoinQuery
// the number of docs to get at the top collection level
limit?: number
locale: string
// the where clause for the top collection
query?: Where
}
export const buildJoinAggregation = async ({
adapter,
collection,
collectionConfig,
joins,
limit,
locale,
query,
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
return
}
const joinConfig = adapter.payload.collections[collection].config.joins
const aggregate: PipelineStage[] = [
{
$sort: { createdAt: -1 },
},
]
if (query) {
aggregate.push({
$match: query,
})
}
if (limit) {
aggregate.push({
$limit: limit,
})
}
for (const slug of Object.keys(joinConfig)) {
for (const join of joinConfig[slug]) {
const joinModel = adapter.collections[join.field.collection]
const {
limit: limitJoin = 10,
sort: sortJoin,
where: whereJoin,
} = joins?.[join.schemaPath] || {}
const sort = buildSortParam({
config: adapter.payload.config,
fields: adapter.payload.collections[slug].config.fields,
locale,
sort: sortJoin || collectionConfig.defaultSort,
timestamps: true,
})
const sortProperty = Object.keys(sort)[0]
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
const $match = await joinModel.buildQuery({
locale,
payload: adapter.payload,
where: whereJoin,
})
const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [
{ $match },
{
$sort: { [sortProperty]: sortDirection },
},
]
if (limitJoin > 0) {
pipeline.push({
$limit: limitJoin + 1,
})
}
if (adapter.payload.config.localization && locale === 'all') {
adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${join.schemaPath}${code}`
aggregate.push(
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${code}`,
from: slug,
localField: '_id',
pipeline,
},
},
{
$addFields: {
[`${as}.docs`]: {
$map: {
as: 'doc',
in: '$$doc._id',
input: `$${as}.docs`,
},
}, // Slicing the docs to match the limit
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
}, // Boolean indicating if more docs than limit
},
},
)
if (limitJoin > 0) {
aggregate.push({
$addFields: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, limitJoin],
},
},
})
}
})
} else {
const localeSuffix =
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
const as = `${join.schemaPath}${localeSuffix}`
aggregate.push(
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${localeSuffix}`,
from: slug,
localField: '_id',
pipeline,
},
},
{
$addFields: {
[`${as}.docs`]: {
$map: {
as: 'doc',
in: '$$doc._id',
input: `$${as}.docs`,
},
}, // Slicing the docs to match the limit
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
}, // Boolean indicating if more docs than limit
},
},
)
if (limitJoin > 0) {
aggregate.push({
$addFields: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, limitJoin],
},
},
})
}
}
}
}
return aggregate
}

View File

@@ -0,0 +1,140 @@
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
import mongoose from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData } from 'payload/shared'
type Args = {
config: SanitizedConfig
data: Record<string, unknown>
fields: Field[]
}
interface RelationObject {
relationTo: string
value: number | string
}
function isValidRelationObject(value: unknown): value is RelationObject {
return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value
}
const convertValue = ({
relatedCollection,
value,
}: {
relatedCollection: CollectionConfig
value: number | string
}): mongoose.Types.ObjectId | number | string => {
const customIDField = relatedCollection.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (!customIDField) {
return new mongoose.Types.ObjectId(value)
}
return value
}
const sanitizeRelationship = ({ config, field, locale, ref, value }) => {
let relatedCollection: CollectionConfig | undefined
let result = value
const hasManyRelations = typeof field.relationTo !== 'string'
if (!hasManyRelations) {
relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo)
}
if (Array.isArray(value)) {
result = value.map((val) => {
// Handle has many
if (relatedCollection && val && (typeof val === 'string' || typeof val === 'number')) {
return convertValue({
relatedCollection,
value: val,
})
}
// Handle has many - polymorphic
if (isValidRelationObject(val)) {
const relatedCollectionForSingleValue = config.collections?.find(
({ slug }) => slug === val.relationTo,
)
if (relatedCollectionForSingleValue) {
return {
relationTo: val.relationTo,
value: convertValue({
relatedCollection: relatedCollectionForSingleValue,
value: val.value,
}),
}
}
}
return val
})
}
// Handle has one - polymorphic
if (isValidRelationObject(value)) {
relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo)
if (relatedCollection) {
result = {
relationTo: value.relationTo,
value: convertValue({ relatedCollection, value: value.value }),
}
}
}
// Handle has one
if (relatedCollection && value && (typeof value === 'string' || typeof value === 'number')) {
result = convertValue({
relatedCollection,
value,
})
}
if (locale) {
ref[locale] = result
} else {
ref[field.name] = result
}
}
export const sanitizeRelationshipIDs = ({
config,
data,
fields,
}: Args): Record<string, unknown> => {
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (field.type === 'relationship' || field.type === 'upload') {
// handle localized relationships
if (config.localization && field.localized) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
for (const { code } of locales) {
if (ref[field.name]?.[code]) {
const value = ref[field.name][code]
sanitizeRelationship({ config, field, locale: code, ref: fieldRef, value })
}
}
} else {
// handle non-localized relationships
sanitizeRelationship({
config,
field,
locale: undefined,
ref,
value: ref[field.name],
})
}
}
}
traverseFields({ callback: sanitize, fields, ref: data })
return data
}

View File

@@ -10,7 +10,7 @@ import type { MongooseAdapter } from './index.js'
export async function withSession(
db: MongooseAdapter,
req: PayloadRequest,
): Promise<{ session: ClientSession } | object> {
): Promise<{ session: ClientSession } | Record<string, never>> {
let transactionID = req.transactionID
if (transactionID instanceof Promise) {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.107",
"version": "3.0.0-beta.108",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -118,7 +118,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const versionsTableName = adapter.tableNameMap.get(
`_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`,
)
const versionFields = buildVersionCollectionFields(collection)
const versionFields = buildVersionCollectionFields(payload.config, collection)
const versionPathsToQuery: PathsToQuery = new Set()
traverseFields({
@@ -191,7 +191,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
`_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`,
)
const versionFields = buildVersionGlobalFields(global)
const versionFields = buildVersionGlobalFields(payload.config, global)
const versionPathsToQuery: PathsToQuery = new Set()

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.0.0-beta.107",
"version": "3.0.0-beta.108",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,7 +1,7 @@
import type { ChainedMethods } from '@payloadcms/drizzle/types'
import { chainMethods } from '@payloadcms/drizzle'
import { sql } from 'drizzle-orm'
import { count, sql } from 'drizzle-orm'
import type { CountDistinct, SQLiteAdapter } from './types.js'
@@ -22,8 +22,11 @@ export const countDistinct: CountDistinct = async function countDistinct(
methods: chainedMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${this.tables[tableName].id})`,
count:
joins.length > 0
? sql`count
(DISTINCT ${this.tables[tableName].id})`.mapWith(Number)
: count(),
})
.from(this.tables[tableName])
.where(where),

View File

@@ -37,6 +37,7 @@ export const init: Init = function init(this: SQLiteAdapter) {
})
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const config = this.payload.config
const baseExtraConfig: BaseExtraConfig = {}
@@ -51,6 +52,17 @@ export const init: Init = function init(this: SQLiteAdapter) {
}
}
if (collection.upload.filenameCompoundIndex) {
const indexName = `${tableName}_filename_compound_idx`
baseExtraConfig.filename_compound_index = (cols) => {
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
return cols[f]
})
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
}
}
buildTable({
adapter: this,
disableNotNull: !!collection?.versions?.drafts,
@@ -66,7 +78,7 @@ export const init: Init = function init(this: SQLiteAdapter) {
const versionsTableName = this.tableNameMap.get(
`_${toSnakeCase(collection.slug)}${this.versionsSuffix}`,
)
const versionFields = buildVersionCollectionFields(collection)
const versionFields = buildVersionCollectionFields(config, collection)
buildTable({
adapter: this,
@@ -105,7 +117,8 @@ export const init: Init = function init(this: SQLiteAdapter) {
versions: true,
versionsCustomName: true,
})
const versionFields = buildVersionGlobalFields(global)
const config = this.payload.config
const versionFields = buildVersionGlobalFields(config, global)
buildTable({
adapter: this,

View File

@@ -35,7 +35,15 @@ export type BaseExtraConfig = Record<
}) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>
export type RelationMap = Map<string, { localized: boolean; target: string; type: 'many' | 'one' }>
export type RelationMap = Map<
string,
{
localized: boolean
relationName?: string
target: string
type: 'many' | 'one'
}
>
type Args = {
adapter: SQLiteAdapter
@@ -144,9 +152,9 @@ export const buildTable = ({
const localizedRelations = new Map()
const nonLocalizedRelations = new Map()
relationsToBuild.forEach(({ type, localized, target }, key) => {
relationsToBuild.forEach(({ type, localized, relationName, target }, key) => {
const map = localized ? localizedRelations : nonLocalizedRelations
map.set(key, { type, target })
map.set(key, { type, relationName, target })
})
if (timestamps) {
@@ -458,7 +466,7 @@ export const buildTable = ({
adapter.relations[`relations_${tableName}`] = relations(table, ({ many, one }) => {
const result: Record<string, Relation<string>> = {}
nonLocalizedRelations.forEach(({ type, target }, key) => {
nonLocalizedRelations.forEach(({ type, relationName, target }, key) => {
if (type === 'one') {
result[key] = one(adapter.tables[target], {
fields: [table[key]],
@@ -467,7 +475,7 @@ export const buildTable = ({
})
}
if (type === 'many') {
result[key] = many(adapter.tables[target], { relationName: key })
result[key] = many(adapter.tables[target], { relationName: relationName || key })
}
})

View File

@@ -19,7 +19,7 @@ import {
text,
} from 'drizzle-orm/sqlite-core'
import { InvalidConfiguration } from 'payload'
import { fieldAffectsData, optionIsObject } from 'payload/shared'
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, IDType, SQLiteAdapter } from '../types.js'
@@ -113,6 +113,11 @@ export const traverseFields = ({
if ('name' in field && field.name === 'id') {
return
}
if (fieldIsVirtual(field)) {
return
}
let columnName: string
let fieldName: string
@@ -153,7 +158,7 @@ export const traverseFields = ({
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
}
targetIndexes[`${newTableName}_${field.name}Idx`] = createIndex({
name: fieldName,
name: field.localized ? [fieldName, '_locale'] : fieldName,
columnName,
tableName: newTableName,
unique,
@@ -893,6 +898,21 @@ export const traverseFields = ({
break
case 'join': {
// fieldName could be 'posts' or 'group_posts'
// using on as the key for the relation
const localized = adapter.payload.config.localization && field.localized
const target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}`
relationsToBuild.set(fieldName, {
type: 'many',
// joins are not localized on the parent table
localized: false,
relationName: toSnakeCase(field.on),
target,
})
break
}
default:
break
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.0.0-beta.107",
"version": "3.0.0-beta.108",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -118,7 +118,8 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const versionsTableName = adapter.tableNameMap.get(
`_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`,
)
const versionFields = buildVersionCollectionFields(collection)
const versionFields = buildVersionCollectionFields(payload.config, collection)
const versionPathsToQuery: PathsToQuery = new Set()
traverseFields({
@@ -191,7 +192,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
`_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`,
)
const versionFields = buildVersionGlobalFields(global)
const versionFields = buildVersionGlobalFields(payload.config, global)
const versionPathsToQuery: PathsToQuery = new Set()

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.0.0-beta.107",
"version": "3.0.0-beta.108",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -16,7 +16,7 @@ export const count: Count = async function count(
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const { joins, where } = await buildQuery({
const { joins, where } = buildQuery({
adapter: this,
fields: collectionConfig.fields,
locale,

View File

@@ -10,7 +10,14 @@ import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>(
this: DrizzleAdapter,
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
{
autosave,
globalSlug,
publishedLocale,
req = {} as PayloadRequest,
snapshot,
versionData,
}: CreateGlobalVersionArgs,
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
@@ -22,10 +29,12 @@ export async function createGlobalVersion<T extends TypeWithID>(
data: {
autosave,
latest: true,
publishedLocale,
snapshot,
version: versionData,
},
db,
fields: buildVersionGlobalFields(global),
fields: buildVersionGlobalFields(this.payload.config, global),
operation: 'create',
req,
tableName,

View File

@@ -14,7 +14,9 @@ export async function createVersion<T extends TypeWithID>(
autosave,
collectionSlug,
parent,
publishedLocale,
req = {} as PayloadRequest,
snapshot,
versionData,
}: CreateVersionArgs<T>,
) {
@@ -33,6 +35,8 @@ export async function createVersion<T extends TypeWithID>(
autosave,
latest: true,
parent,
publishedLocale,
snapshot,
version,
}
@@ -44,7 +48,7 @@ export async function createVersion<T extends TypeWithID>(
adapter: this,
data,
db,
fields: buildVersionCollectionFields(collection),
fields: buildVersionCollectionFields(this.payload.config, collection),
operation: 'create',
req,
tableName,
@@ -56,11 +60,11 @@ export async function createVersion<T extends TypeWithID>(
await this.execute({
db,
sql: sql`
UPDATE ${table}
SET latest = false
WHERE ${table.id} != ${result.id}
AND ${table.parent} = ${parent}
`,
UPDATE ${table}
SET latest = false
WHERE ${table.id} != ${result.id}
AND ${table.parent} = ${parent}
`,
})
}

View File

@@ -12,7 +12,7 @@ import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: DrizzleAdapter,
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
{ collection: collectionSlug, joins: joinQuery, req = {} as PayloadRequest, where: whereArg },
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
@@ -21,7 +21,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
let docToDelete: Record<string, unknown>
const { joins, selectFields, where } = await buildQuery({
const { joins, selectFields, where } = buildQuery({
adapter: this,
fields: collection.fields,
locale: req.locale,
@@ -48,6 +48,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
adapter: this,
depth: 0,
fields: collection.fields,
joinQuery,
tableName,
})
@@ -61,6 +62,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
config: this.payload.config,
data: docToDelete,
fields: collection.fields,
joinQuery,
})
await this.deleteWhere({

View File

@@ -19,7 +19,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionCollectionFields(collectionConfig)
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
const { docs } = await findMany({
adapter: this,

View File

@@ -10,6 +10,7 @@ export const find: Find = async function find(
this: DrizzleAdapter,
{
collection,
joins,
limit,
locale,
page = 1,
@@ -27,6 +28,7 @@ export const find: Find = async function find(
return findMany({
adapter: this,
fields: collectionConfig.fields,
joins,
limit,
locale,
page,

View File

@@ -1,7 +1,7 @@
import type { DBQueryConfig } from 'drizzle-orm'
import type { Field } from 'payload'
import type { Field, JoinQuery } from 'payload'
import type { DrizzleAdapter } from '../types.js'
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
import { traverseFields } from './traverseFields.js'
@@ -9,6 +9,12 @@ type BuildFindQueryArgs = {
adapter: DrizzleAdapter
depth: number
fields: Field[]
joinQuery?: JoinQuery
/**
* The joins array will be mutated by pushing any joins needed for the where queries of join field joins
*/
joins?: BuildQueryJoinAliases
locale?: string
tableName: string
}
@@ -24,6 +30,9 @@ export const buildFindManyArgs = ({
adapter,
depth,
fields,
joinQuery,
joins = [],
locale,
tableName,
}: BuildFindQueryArgs): Record<string, unknown> => {
const result: Result = {
@@ -79,6 +88,9 @@ export const buildFindManyArgs = ({
currentTableName: tableName,
depth,
fields,
joinQuery,
joins,
locale,
path: '',
tablePath: '',
topLevelArgs: result,

View File

@@ -19,6 +19,7 @@ type Args = {
export const findMany = async function find({
adapter,
fields,
joins: joinQuery,
limit: limitArg,
locale,
page = 1,
@@ -42,7 +43,7 @@ export const findMany = async function find({
limit = undefined
}
const { joins, orderBy, selectFields, where } = await buildQuery({
const { joins, orderBy, selectFields, where } = buildQuery({
adapter,
fields,
locale,
@@ -67,6 +68,8 @@ export const findMany = async function find({
adapter,
depth: 0,
fields,
joinQuery,
joins,
tableName,
})
@@ -151,6 +154,7 @@ export const findMany = async function find({
config: adapter.payload.config,
data,
fields,
joinQuery,
})
})

View File

@@ -1,11 +1,15 @@
import type { Field } from 'payload'
import type { DBQueryConfig } from 'drizzle-orm'
import type { Field, JoinQuery } from 'payload'
import { fieldAffectsData, tabHasName } from 'payload/shared'
import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../types.js'
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js'
import { buildOrderBy } from '../queries/buildOrderBy.js'
import buildQuery from '../queries/buildQuery.js'
type TraverseFieldArgs = {
_locales: Result
adapter: DrizzleAdapter
@@ -13,6 +17,9 @@ type TraverseFieldArgs = {
currentTableName: string
depth?: number
fields: Field[]
joinQuery: JoinQuery
joins?: BuildQueryJoinAliases
locale?: string
path: string
tablePath: string
topLevelArgs: Record<string, unknown>
@@ -26,12 +33,19 @@ export const traverseFields = ({
currentTableName,
depth,
fields,
joinQuery = {},
joins,
locale,
path,
tablePath,
topLevelArgs,
topLevelTableName,
}: TraverseFieldArgs) => {
fields.forEach((field) => {
if (fieldIsVirtual(field)) {
return
}
// handle simple relationship
if (
depth > 0 &&
@@ -54,6 +68,8 @@ export const traverseFields = ({
currentTableName,
depth,
fields: field.fields,
joinQuery,
joins,
path,
tablePath,
topLevelArgs,
@@ -75,6 +91,8 @@ export const traverseFields = ({
currentTableName,
depth,
fields: tab.fields,
joinQuery,
joins,
path: tabPath,
tablePath: tabTablePath,
topLevelArgs,
@@ -120,6 +138,7 @@ export const traverseFields = ({
currentTableName: arrayTableName,
depth,
fields: field.fields,
joinQuery,
path: '',
tablePath: '',
topLevelArgs,
@@ -177,6 +196,7 @@ export const traverseFields = ({
currentTableName: tableName,
depth,
fields: block.fields,
joinQuery,
path: '',
tablePath: '',
topLevelArgs,
@@ -195,6 +215,8 @@ export const traverseFields = ({
currentTableName,
depth,
fields: field.fields,
joinQuery,
joins,
path: `${path}${field.name}_`,
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
topLevelArgs,
@@ -204,6 +226,67 @@ export const traverseFields = ({
break
}
case 'join': {
// when `joinsQuery` is false, do not join
if (joinQuery === false) {
break
}
const {
limit: limitArg = 10,
sort,
where,
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
let limit = limitArg
if (limit !== 0) {
// get an additional document and slice it later to determine if there is a next page
limit += 1
}
const fields = adapter.payload.collections[field.collection].config.fields
const joinTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${
field.localized && adapter.payload.config.localization ? adapter.localesSuffix : ''
}`
const selectFields = {}
const orderBy = buildOrderBy({
adapter,
fields,
joins: [],
locale,
selectFields,
sort,
tableName: joinTableName,
})
const withJoin: DBQueryConfig<'many', true, any, any> = {
columns: selectFields,
orderBy: () => [orderBy.order(orderBy.column)],
}
if (limit) {
withJoin.limit = limit
}
if (field.localized) {
withJoin.columns._locale = true
withJoin.columns._parentID = true
} else {
withJoin.columns.id = true
}
if (where) {
const { where: joinWhere } = buildQuery({
adapter,
fields,
joins,
locale,
sort,
tableName: joinTableName,
where,
})
withJoin.where = () => joinWhere
}
currentArgs.with[`${path.replaceAll('.', '_')}${field.name}`] = withJoin
break
}
default: {
break
}

View File

@@ -30,7 +30,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionGlobalFields(globalConfig)
const fields = buildVersionGlobalFields(this.payload.config, globalConfig)
return findMany({
adapter: this,

View File

@@ -8,7 +8,7 @@ import { findMany } from './find/findMany.js'
export async function findOne<T extends TypeWithID>(
this: DrizzleAdapter,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
{ collection, joins, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
@@ -17,6 +17,7 @@ export async function findOne<T extends TypeWithID>(
const { docs } = await findMany({
adapter: this,
fields: collectionConfig.fields,
joins,
limit: 1,
locale,
page: 1,

View File

@@ -28,7 +28,7 @@ export const findVersions: FindVersions = async function findVersions(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionCollectionFields(collectionConfig)
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
return findMany({
adapter: this,

View File

@@ -1,4 +1,4 @@
import { sql } from 'drizzle-orm'
import { count, sql } from 'drizzle-orm'
import type { ChainedMethods, TransactionPg } from '../types.js'
import type { BasePostgresAdapter, CountDistinct } from './types.js'
@@ -22,8 +22,11 @@ export const countDistinct: CountDistinct = async function countDistinct(
methods: chainedMethods,
query: (db as TransactionPg)
.select({
count: sql<string>`count
(DISTINCT ${this.tables[tableName].id})`,
count:
joins.length > 0
? sql`count
(DISTINCT ${this.tables[tableName].id})`.mapWith(Number)
: count(),
})
.from(this.tables[tableName])
.where(where),

View File

@@ -63,7 +63,7 @@ export const init: Init = function init(this: BasePostgresAdapter) {
const versionsTableName = this.tableNameMap.get(
`_${toSnakeCase(collection.slug)}${this.versionsSuffix}`,
)
const versionFields = buildVersionCollectionFields(collection)
const versionFields = buildVersionCollectionFields(this.payload.config, collection)
buildTable({
adapter: this,
@@ -97,7 +97,7 @@ export const init: Init = function init(this: BasePostgresAdapter) {
versions: true,
versionsCustomName: true,
})
const versionFields = buildVersionGlobalFields(global)
const versionFields = buildVersionGlobalFields(this.payload.config, global)
buildTable({
adapter: this,

View File

@@ -138,9 +138,9 @@ export const buildTable = ({
const localizedRelations = new Map()
const nonLocalizedRelations = new Map()
relationsToBuild.forEach(({ type, localized, target }, key) => {
relationsToBuild.forEach(({ type, localized, relationName, target }, key) => {
const map = localized ? localizedRelations : nonLocalizedRelations
map.set(key, { type, target })
map.set(key, { type, relationName, target })
})
if (timestamps) {
@@ -444,7 +444,7 @@ export const buildTable = ({
adapter.relations[`relations_${tableName}`] = relations(table, ({ many, one }) => {
const result: Record<string, Relation<string>> = {}
nonLocalizedRelations.forEach(({ type, target }, key) => {
nonLocalizedRelations.forEach(({ type, relationName, target }, key) => {
if (type === 'one') {
result[key] = one(adapter.tables[target], {
fields: [table[key]],
@@ -453,7 +453,7 @@ export const buildTable = ({
})
}
if (type === 'many') {
result[key] = many(adapter.tables[target], { relationName: key })
result[key] = many(adapter.tables[target], { relationName: relationName || key })
}
})

View File

@@ -18,7 +18,7 @@ import {
varchar,
} from 'drizzle-orm/pg-core'
import { InvalidConfiguration } from 'payload'
import { fieldAffectsData, optionIsObject } from 'payload/shared'
import { fieldAffectsData, fieldIsVirtual, optionIsObject } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type {
@@ -119,6 +119,10 @@ export const traverseFields = ({
if ('name' in field && field.name === 'id') {
return
}
if (fieldIsVirtual(field)) {
return
}
let columnName: string
let fieldName: string
@@ -159,7 +163,7 @@ export const traverseFields = ({
adapter.fieldConstraints[rootTableName][`${columnName}_idx`] = constraintValue
}
targetIndexes[`${newTableName}_${field.name}Idx`] = createIndex({
name: fieldName,
name: field.localized ? [fieldName, '_locale'] : fieldName,
columnName,
tableName: newTableName,
unique,
@@ -902,6 +906,21 @@ export const traverseFields = ({
break
case 'join': {
// fieldName could be 'posts' or 'group_posts'
// using on as the key for the relation
const localized = adapter.payload.config.localization && field.localized
const target = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${localized ? adapter.localesSuffix : ''}`
relationsToBuild.set(fieldName, {
type: 'many',
// joins are not localized on the parent table
localized: false,
relationName: toSnakeCase(field.on),
target,
})
break
}
default:
break
}

View File

@@ -31,7 +31,15 @@ export type BaseExtraConfig = Record<
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>
export type RelationMap = Map<string, { localized: boolean; target: string; type: 'many' | 'one' }>
export type RelationMap = Map<
string,
{
localized: boolean
relationName?: string
target: string
type: 'many' | 'one'
}
>
export type GenericColumn = PgColumn<
ColumnBaseConfig<ColumnDataType, string>,

View File

@@ -6,7 +6,7 @@ import type { BuildQueryJoinAliases } from './buildQuery.js'
import { parseParams } from './parseParams.js'
export async function buildAndOrConditions({
export function buildAndOrConditions({
adapter,
fields,
joins,
@@ -24,7 +24,7 @@ export async function buildAndOrConditions({
selectFields: Record<string, GenericColumn>
tableName: string
where: Where[]
}): Promise<SQL[]> {
}): SQL[] {
const completedConditions = []
// Loop over all AND / OR operations and add them to the AND / OR query param
// Operations should come through as an array
@@ -32,7 +32,7 @@ export async function buildAndOrConditions({
for (const condition of where) {
// If the operation is properly formatted as an object
if (typeof condition === 'object') {
const result = await parseParams({
const result = parseParams({
adapter,
fields,
joins,

View File

@@ -0,0 +1,82 @@
import type { Field } from 'payload'
import { asc, desc } from 'drizzle-orm'
import type { DrizzleAdapter, GenericColumn } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryResult } from './buildQuery.js'
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
type Args = {
adapter: DrizzleAdapter
fields: Field[]
joins: BuildQueryJoinAliases
locale?: string
selectFields: Record<string, GenericColumn>
sort?: string
tableName: string
}
/**
* Gets the order by column and direction constructed from the sort argument adds the column to the select fields and joins if necessary
*/
export const buildOrderBy = ({
adapter,
fields,
joins,
locale,
selectFields,
sort,
tableName,
}: Args): BuildQueryResult['orderBy'] => {
const orderBy: BuildQueryResult['orderBy'] = {
column: null,
order: null,
}
if (sort) {
let sortPath
if (sort[0] === '-') {
sortPath = sort.substring(1)
orderBy.order = desc
} else {
sortPath = sort
orderBy.order = asc
}
try {
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
adapter,
collectionPath: sortPath,
fields,
joins,
locale,
pathSegments: sortPath.replace(/__/g, '.').split('.'),
selectFields,
tableName,
value: sortPath,
})
orderBy.column = sortTable?.[sortTableColumnName]
} catch (err) {
// continue
}
}
if (!orderBy?.column) {
orderBy.order = desc
const createdAt = adapter.tables[tableName]?.createdAt
if (createdAt) {
orderBy.column = createdAt
} else {
orderBy.column = adapter.tables[tableName].id
}
}
if (orderBy.column) {
selectFields.sort = orderBy.column
}
return orderBy
}

View File

@@ -1,12 +1,10 @@
import type { SQL } from 'drizzle-orm'
import type { asc, desc, SQL } from 'drizzle-orm'
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
import type { Field, Where } from 'payload'
import { asc, desc } from 'drizzle-orm'
import type { DrizzleAdapter, GenericColumn, GenericTable } from '../types.js'
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
import { buildOrderBy } from './buildOrderBy.js'
import { parseParams } from './parseParams.js'
export type BuildQueryJoinAliases = {
@@ -17,13 +15,14 @@ export type BuildQueryJoinAliases = {
type BuildQueryArgs = {
adapter: DrizzleAdapter
fields: Field[]
joins?: BuildQueryJoinAliases
locale?: string
sort?: string
tableName: string
where: Where
}
type Result = {
export type BuildQueryResult = {
joins: BuildQueryJoinAliases
orderBy: {
column: GenericColumn
@@ -32,72 +31,33 @@ type Result = {
selectFields: Record<string, GenericColumn>
where: SQL
}
const buildQuery = async function buildQuery({
const buildQuery = function buildQuery({
adapter,
fields,
joins = [],
locale,
sort,
tableName,
where: incomingWhere,
}: BuildQueryArgs): Promise<Result> {
}: BuildQueryArgs): BuildQueryResult {
const selectFields: Record<string, GenericColumn> = {
id: adapter.tables[tableName].id,
}
const joins: BuildQueryJoinAliases = []
const orderBy: Result['orderBy'] = {
column: null,
order: null,
}
if (sort) {
let sortPath
if (sort[0] === '-') {
sortPath = sort.substring(1)
orderBy.order = desc
} else {
sortPath = sort
orderBy.order = asc
}
try {
const { columnName: sortTableColumnName, table: sortTable } = getTableColumnFromPath({
adapter,
collectionPath: sortPath,
fields,
joins,
locale,
pathSegments: sortPath.replace(/__/g, '.').split('.'),
selectFields,
tableName,
value: sortPath,
})
orderBy.column = sortTable?.[sortTableColumnName]
} catch (err) {
// continue
}
}
if (!orderBy?.column) {
orderBy.order = desc
const createdAt = adapter.tables[tableName]?.createdAt
if (createdAt) {
orderBy.column = createdAt
} else {
orderBy.column = adapter.tables[tableName].id
}
}
if (orderBy.column) {
selectFields.sort = orderBy.column
}
const orderBy = buildOrderBy({
adapter,
fields,
joins,
locale,
selectFields,
sort,
tableName,
})
let where: SQL
if (incomingWhere && Object.keys(incomingWhere).length > 0) {
where = await parseParams({
where = parseParams({
adapter,
fields,
joins,

View File

@@ -12,6 +12,7 @@ import { validate as uuidValidate } from 'uuid'
import type { DrizzleAdapter, GenericColumn } from '../types.js'
import type { BuildQueryJoinAliases } from './buildQuery.js'
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
import { getTableAlias } from './getTableAlias.js'
type Constraint = {
@@ -23,7 +24,7 @@ type Constraint = {
type TableColumn = {
columnName?: string
columns?: {
idType: 'number' | 'text'
idType: 'number' | 'text' | 'uuid'
rawColumn: SQL<unknown>
}[]
constraints: Constraint[]
@@ -521,7 +522,8 @@ export const getTableColumnFromPath = ({
const columns: TableColumn['columns'] = field.relationTo
.map((relationTo) => {
let idType: 'number' | 'text' = adapter.idType === 'uuid' ? 'text' : 'number'
let idType: 'number' | 'text' | 'uuid' =
adapter.idType === 'uuid' ? 'uuid' : 'number'
const { customIDType } = adapter.payload.collections[relationTo]
@@ -529,9 +531,19 @@ export const getTableColumnFromPath = ({
idType = customIDType
}
const idTypeTextOrUuid = idType === 'text' || idType === 'uuid'
// Do not add the column to OR if we know that it can't match by the type
// We can't do the same with idType: 'number' because `value` can be from the REST search query params
if (typeof value === 'number' && idType === 'text') {
if (typeof value === 'number' && idTypeTextOrUuid) {
return null
}
if (
Array.isArray(value) &&
value.every((val) => typeof val === 'number') &&
idTypeTextOrUuid
) {
return null
}
@@ -540,8 +552,8 @@ export const getTableColumnFromPath = ({
// We need this because Postgres throws an error if querying by UUID column with a value that isn't a valid UUID.
if (
value &&
!customIDType &&
adapter.idType === 'uuid' &&
!Array.isArray(value) &&
idType === 'uuid' &&
hasCustomCollectionWithCustomID
) {
if (!uuidValidate(value)) {
@@ -549,6 +561,15 @@ export const getTableColumnFromPath = ({
}
}
if (
Array.isArray(value) &&
idType === 'uuid' &&
hasCustomCollectionWithCustomID &&
!value.some((val) => uuidValidate(val))
) {
return null
}
const relationTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
)
@@ -583,6 +604,19 @@ export const getTableColumnFromPath = ({
},
table: aliasRelationshipTable,
}
} else if (isPolymorphicRelationship(value)) {
const { relationTo } = value
const relationTableName = adapter.tableNameMap.get(
toSnakeCase(adapter.payload.collections[relationTo].config.slug),
)
return {
constraints,
field,
rawColumn: sql.raw(`"${aliasRelationshipTableName}"."${relationTableName}_id"`),
table: aliasRelationshipTable,
}
} else {
throw new APIError('Not supported')
}

View File

@@ -22,7 +22,7 @@ type Args = {
where: Where
}
export async function parseParams({
export function parseParams({
adapter,
fields,
joins,
@@ -30,7 +30,7 @@ export async function parseParams({
selectFields,
tableName,
where,
}: Args): Promise<SQL> {
}: Args): SQL {
let result: SQL
const constraints: SQL[] = []
@@ -46,7 +46,7 @@ export async function parseParams({
conditionOperator = or
}
if (Array.isArray(condition)) {
const builtConditions = await buildAndOrConditions({
const builtConditions = buildAndOrConditions({
adapter,
fields,
joins,

View File

@@ -2,13 +2,17 @@ import type { SQL } from 'drizzle-orm'
import { APIError, createArrayFromCommaDelineated, type Field, type TabAsField } from 'payload'
import { fieldAffectsData } from 'payload/shared'
import { validate as uuidValidate } from 'uuid'
import type { DrizzleAdapter } from '../types.js'
import { getCollectionIdType } from '../utilities/getCollectionIdType.js'
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
type SanitizeQueryValueArgs = {
adapter: DrizzleAdapter
columns?: {
idType: 'number' | 'text'
idType: 'number' | 'text' | 'uuid'
rawColumn: SQL<unknown>
}[]
field: Field | TabAsField
@@ -106,28 +110,61 @@ export const sanitizeQueryValue = ({
// convert the value to the idType of the relationship
let idType: 'number' | 'text'
if (typeof field.relationTo === 'string') {
const collection = adapter.payload.collections[field.relationTo]
const mixedType: 'number' | 'serial' | 'text' | 'uuid' =
collection.customIDType || adapter.idType
const typeMap: Record<string, 'number' | 'text'> = {
number: 'number',
serial: 'number',
text: 'text',
uuid: 'text',
}
idType = typeMap[mixedType]
idType = getCollectionIdType({
adapter,
collection: adapter.payload.collections[field.relationTo],
})
} else {
if (isPolymorphicRelationship(val)) {
if (operator !== 'equals') {
throw new APIError(
`Only 'equals' operator is supported for polymorphic relationship object notation. Given - ${operator}`,
)
}
idType = getCollectionIdType({
adapter,
collection: adapter.payload.collections[val.relationTo],
})
return {
operator,
value: idType === 'number' ? Number(val.value) : String(val.value),
}
}
formattedColumns = columns
.map(({ idType, rawColumn }) => {
let formattedValue: number | string
if (idType === 'number') {
let formattedValue: number | number[] | string | string[]
if (Array.isArray(val)) {
formattedValue = val
.map((eachVal) => {
let formattedValue: number | string
if (idType === 'number') {
formattedValue = Number(eachVal)
if (Number.isNaN(formattedValue)) {
return null
}
} else {
if (idType === 'uuid' && !uuidValidate(eachVal)) {
return null
}
formattedValue = String(eachVal)
}
return formattedValue
})
.filter(Boolean) as number[] | string[]
} else if (idType === 'number') {
formattedValue = Number(val)
if (Number.isNaN(formattedValue)) {
return null
}
}
if (idType === 'text') {
} else {
formattedValue = String(val)
}

View File

@@ -15,7 +15,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
const tableName = this.tableNameMap.get(
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
)
const fields = buildVersionCollectionFields(collectionConfig)
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -1,4 +1,4 @@
import type { Field, SanitizedConfig, TypeWithID } from 'payload'
import type { Field, JoinQuery, SanitizedConfig, TypeWithID } from 'payload'
import type { DrizzleAdapter } from '../../types.js'
@@ -12,6 +12,7 @@ type TransformArgs = {
data: Record<string, unknown>
fallbackLocale?: false | string
fields: Field[]
joinQuery?: JoinQuery
locale?: string
}
@@ -22,6 +23,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
config,
data,
fields,
joinQuery,
}: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
@@ -55,6 +57,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
deletions,
fieldPrefix: '',
fields,
joinQuery,
numbers,
path: '',
relationships,

View File

@@ -1,6 +1,6 @@
import type { Field, SanitizedConfig, TabAsField } from 'payload'
import type { Field, JoinQuery, SanitizedConfig, TabAsField } from 'payload'
import { fieldAffectsData } from 'payload/shared'
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
@@ -38,6 +38,10 @@ type TraverseFieldsArgs = {
* An array of Payload fields to traverse
*/
fields: (Field | TabAsField)[]
/**
*
*/
joinQuery?: JoinQuery
/**
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/
@@ -74,6 +78,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields,
joinQuery,
numbers,
path,
relationships,
@@ -93,6 +98,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
joinQuery,
numbers,
path,
relationships,
@@ -115,6 +121,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields: field.fields,
joinQuery,
numbers,
path,
relationships,
@@ -125,6 +132,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
if (fieldAffectsData(field)) {
if (fieldIsVirtual(field)) {
return result
}
const fieldName = `${fieldPrefix || ''}${field.name}`
const fieldData = table[fieldName]
const localizedFieldData = {}
@@ -386,6 +397,44 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
}
if (field.type === 'join') {
const { limit = 10 } = joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
let fieldResult:
| { docs: unknown[]; hasNextPage: boolean }
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
if (Array.isArray(fieldData)) {
if (field.localized) {
fieldResult = fieldData.reduce((joinResult, row) => {
if (typeof row._locale === 'string') {
if (!joinResult[row._locale]) {
joinResult[row._locale] = {
docs: [],
hasNextPage: false,
}
}
joinResult[row._locale].docs.push(row._parentID)
}
return joinResult
}, {})
Object.keys(fieldResult).forEach((locale) => {
fieldResult[locale].hasNextPage = fieldResult[locale].docs.length > limit
fieldResult[locale].docs = fieldResult[locale].docs.slice(0, limit)
})
} else {
const hasNextPage = limit !== 0 && fieldData.length > limit
fieldResult = {
docs: hasNextPage ? fieldData.slice(0, limit) : fieldData,
hasNextPage,
}
}
}
result[field.name] = fieldResult
return result
}
if (field.type === 'text' && field?.hasMany) {
const textPathMatch = texts[`${sanitizedPath}${field.name}`]
if (!textPathMatch) {

View File

@@ -1,6 +1,6 @@
import type { Field } from 'payload'
import { fieldAffectsData } from 'payload/shared'
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
@@ -93,6 +93,10 @@ export const traverseFields = ({
let fieldData: unknown
if (fieldAffectsData(field)) {
if (fieldIsVirtual(field)) {
return
}
columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}`
fieldName = `${fieldPrefix || ''}${field.name}`
fieldData = data[field.name]
@@ -264,6 +268,10 @@ export const traverseFields = ({
if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
if ('name' in tab) {
if (fieldIsVirtual(tab)) {
return
}
if (typeof data[tab.name] === 'object' && data[tab.name] !== null) {
if (tab.localized) {
Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => {

View File

@@ -10,7 +10,7 @@ import { upsertRow } from './upsertRow/index.js'
export const updateOne: UpdateOne = async function updateOne(
this: DrizzleAdapter,
{ id, collection: collectionSlug, data, draft, locale, req, where: whereArg },
{ id, collection: collectionSlug, data, draft, joins: joinQuery, locale, req, where: whereArg },
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
@@ -18,7 +18,7 @@ export const updateOne: UpdateOne = async function updateOne(
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id
const { joins, selectFields, where } = await buildQuery({
const { joins, selectFields, where } = buildQuery({
adapter: this,
fields: collection.fields,
locale,
@@ -46,6 +46,7 @@ export const updateOne: UpdateOne = async function updateOne(
data,
db,
fields: collection.fields,
joinQuery,
operation: 'update',
req,
tableName,

Some files were not shown because too many files have changed in this diff Show More