Compare commits

..

12 Commits

Author SHA1 Message Date
Dan Ribbens
3dd28e41d8 POC infinite recursion of blocks 2024-04-11 12:28:20 -04:00
Dan Ribbens
9bbacc4fb1 feat: custom db table and enum names (#5045)
Co-authored-by: Ritsu <enjoythepain1337@gmail.com>
2024-04-05 11:22:46 -04:00
Patrik
7df7bf448b fix: updates type name of CustomPublishButtonProps to CustomPublishButtonType (#5644)
* fix: updates type name from CustomPublishButtonProps to CustomPublishButtonType

* fix: maps old name to new name
2024-04-04 16:55:18 -04:00
Paul
44599cbc7b fix(db-postgres): issue querying by localised relationship not respecting locale as constraint (#5666)
* fix: add table join for querying on a localised field in relationship fields

* chore: add int tests in relationships
2024-04-04 16:07:11 -03:00
Patrik
373787de31 fix: duplicate document multiple times in quick succession (#5642) 2024-04-04 14:06:29 -04:00
Patrik
c1c86009a5 fix: missing date locales (#5656) 2024-04-04 13:55:07 -04:00
Paul
6cf6ca3ea8 fix(website template): archiveBlock being populated with draft items by default, causing an error for graphql in the frontend (#5643)
* fix(website template): archiveBlock being populated with draft items by default, causing an error for graphql in the frontend
2024-04-03 19:34:36 -03:00
Elliot DeNolf
6706bdb140 chore(release): db-mongodb/1.4.4 [skip ci] 2024-04-03 14:54:53 -04:00
Elliot DeNolf
5caaa032bb chore(release): payload/2.12.1 [skip ci] 2024-04-03 14:37:42 -04:00
Elliot DeNolf
4cf8c5bd78 Merge pull request #5637 from payloadcms/chore/release-2.12.0
chore(release): 2.12.0
2024-04-03 14:34:55 -04:00
Yunsup Sim
742a7af93d fix: Skip parsing if operator is 'exist' (#5404) 2024-04-03 13:50:28 -04:00
Patrik
a7e7c92768 fix: updates colors of tooltip in light mode (#5632) 2024-04-03 13:47:08 -04:00
77 changed files with 1131 additions and 281 deletions

View File

@@ -1,3 +1,11 @@
## [2.12.1](https://github.com/payloadcms/payload/compare/v2.12.0...v2.12.1) (2024-04-03)
### Bug Fixes
* Skip parsing if operator is 'exist' ([#5404](https://github.com/payloadcms/payload/issues/5404)) ([742a7af](https://github.com/payloadcms/payload/commit/742a7af93d2e9ef4d41ee093cef875322792ae72))
* updates colors of tooltip in light mode ([#5632](https://github.com/payloadcms/payload/issues/5632)) ([a7e7c92](https://github.com/payloadcms/payload/commit/a7e7c9276835e0a35a18daccb218c901ca2fdd8c))
## [2.12.0](https://github.com/payloadcms/payload/compare/v2.11.2...v2.12.0) (2024-04-03) ## [2.12.0](https://github.com/payloadcms/payload/compare/v2.11.2...v2.12.0) (2024-04-03)

View File

@@ -168,7 +168,7 @@ import * as React from 'react'
import { import {
CustomSaveButtonProps, CustomSaveButtonProps,
CustomSaveDraftButtonProps, CustomSaveDraftButtonProps,
CustomPublishButtonProps, CustomPublishButtonType,
CustomPreviewButtonProps, CustomPreviewButtonProps,
} from 'payload/types' } from 'payload/types'
@@ -185,7 +185,7 @@ export const CustomSaveDraftButton: CustomSaveDraftButtonProps = ({
return <DefaultButton label={label} disabled={disabled} saveDraft={saveDraft} /> return <DefaultButton label={label} disabled={disabled} saveDraft={saveDraft} />
} }
export const CustomPublishButton: CustomPublishButtonProps = ({ export const CustomPublishButton: CustomPublishButtonType = ({
DefaultButton, DefaultButton,
disabled, disabled,
label, label,

View File

@@ -30,6 +30,7 @@ It's often best practice to write your Collections in separate files and then im
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | | **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`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. | | **`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. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined.
_\* An asterisk denotes that a property is required._ _\* An asterisk denotes that a property is required._
@@ -59,7 +60,8 @@ export const Orders: CollectionConfig = {
#### More collection config examples #### More collection config examples
You can find an assortment You can find an assortment
of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/collections) in the Public of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/collections) in the
Public
Demo source code on GitHub. Demo source code on GitHub.
### Admin options ### Admin options

View File

@@ -6,14 +6,18 @@ desc: Set up your Global config for your needs by defining fields, adding slugs
keywords: globals, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express keywords: globals, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
--- ---
Global configs are in many ways similar to [Collections](/docs/configuration/collections). The big difference is that Collections will potentially contain _many_ documents, while a Global is a "one-off". Globals are perfect for things like header nav, site-wide banner alerts, app-wide localized strings, and other "global" data that your site or app might rely on. Global configs are in many ways similar to [Collections](/docs/configuration/collections). The big difference is that
Collections will potentially contain _many_ documents, while a Global is a "one-off". Globals are perfect for things
like header nav, site-wide banner alerts, app-wide localized strings, and other "global" data that your site or app
might rely on.
As with Collection configs, it's often best practice to write your Globals in separate files and then import them into the main Payload config. As with Collection configs, it's often best practice to write your Globals in separate files and then import them into
the main Payload config.
## Options ## Options
| Option | Description | | Option | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. | | **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Global. |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. | | **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`label`** | Text for the name in the Admin panel or an object with keys for each language. Auto-generated from slug if not defined. | | **`label`** | Text for the name in the Admin panel or an object with keys for each language. Auto-generated from slug if not defined. |
@@ -26,6 +30,7 @@ As with Collection configs, it's often best practice to write your Globals in se
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. | | **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | | **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`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.
_\* An asterisk denotes that a property is required._ _\* An asterisk denotes that a property is required._
@@ -59,26 +64,30 @@ export default Nav
#### Global config example #### Global config example
You can find a few [example Global configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/globals) in the Public Demo source code on GitHub. You can find a few [example Global configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/globals)
in the Public Demo source code on GitHub.
### Admin options ### Admin options
You can customize the way that the Admin panel behaves on a Global-by-Global basis by defining the `admin` property on a Global's config. You can customize the way that the Admin panel behaves on a Global-by-Global basis by defining the `admin` property on a
Global's config.
| Option | Description | | Option | Description |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | |---------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `group` | Text used as a label for grouping collection and global links together in the navigation. | | `group` | Text used as a label for grouping collection and global links together in the navigation. |
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this global from navigation and admin routing. | | `hidden` | Set to true or a function, called with the current user, returning true to exclude this global from navigation and admin routing. |
| `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) | | `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) |
| `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). | | `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). |
| `livePreview`| Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). | | `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. | | `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
### Preview ### Preview
Global `admin` options can accept a `preview` function that will be used to generate a link pointing to the frontend of your app to preview data. Global `admin` options can accept a `preview` function that will be used to generate a link pointing to the frontend of
your app to preview data.
If the function is specified, a Preview button will automatically appear in the corresponding global's Edit view. Clicking the Preview button will link to the URL that is generated by the function. If the function is specified, a Preview button will automatically appear in the corresponding global's Edit view.
Clicking the Preview button will link to the URL that is generated by the function.
**The preview function accepts two arguments:** **The preview function accepts two arguments:**
@@ -113,15 +122,20 @@ export const MyGlobal: GlobalConfig = {
### Access control ### Access control
As with Collections, you can specify extremely granular access control (what users can do with this Global) on a Global-by-Global basis. However, Globals only have `update` and `read` access control due to their nature of only having one document. To learn more, go to the [Access Control](/docs/access-control/overview) docs. As with Collections, you can specify extremely granular access control (what users can do with this Global) on a
Global-by-Global basis. However, Globals only have `update` and `read` access control due to their nature of only having
one document. To learn more, go to the [Access Control](/docs/access-control/overview) docs.
### Hooks ### Hooks
Globals also fully support a smaller subset of Hooks. To learn more, go to the [Hooks](/docs/hooks/overview) documentation. Globals also fully support a smaller subset of Hooks. To learn more, go to the [Hooks](/docs/hooks/overview)
documentation.
### Field types ### Field types
Globals support all field types that Payload has to offer—including simple fields like text and checkboxes all the way to more complicated layout-building field groups like Blocks. [Click here](/docs/fields/overview) to learn more about field types. Globals support all field types that Payload has to offer—including simple fields like text and checkboxes all the way
to more complicated layout-building field groups like Blocks. [Click here](/docs/fields/overview) to learn more about
field types.
### TypeScript ### TypeScript

View File

@@ -37,12 +37,18 @@ export default buildConfig({
### Options ### Options
| Option | Description | | Option | Description |
|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `pool` | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. | | `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. | | `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. | | `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. | | `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
### Access to Drizzle ### Access to Drizzle

View File

@@ -12,22 +12,24 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
</Banner> </Banner>
<LightDarkImage <LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/array.png" srcLight="https://payloadcms.com/images/docs/fields/array.png"
srcDark="https://payloadcms.com/images/docs/fields/array-dark.png" srcDark="https://payloadcms.com/images/docs/fields/array-dark.png"
alt="Array field with two Rows in Payload admin panel" alt="Array field with two Rows in Payload admin panel"
caption="Admin panel screenshot of an Array field with two Rows" caption="Admin panel screenshot of an Array field with two Rows"
/> />
**Example uses:** **Example uses:**
- A "slider" with an image ([upload field](/docs/fields/upload)) and a caption ([text field](/docs/fields/text)) - A "slider" with an image ([upload field](/docs/fields/upload)) and a caption ([text field](/docs/fields/text))
- Navigational structures where editors can specify nav items containing pages ([relationship field](/docs/fields/relationship)), an "open in new tab" [checkbox field](/docs/fields/checkbox) - Navigational structures where editors can specify nav items containing
- Event agenda "timeslots" where you need to specify start & end time ([date field](/docs/fields/date)), label ([text field](/docs/fields/text)), and Learn More page [relationship](/docs/fields/relationship) pages ([relationship field](/docs/fields/relationship)), an "open in new tab" [checkbox field](/docs/fields/checkbox)
- Event agenda "timeslots" where you need to specify start & end time ([date field](/docs/fields/date)),
label ([text field](/docs/fields/text)), and Learn More page [relationship](/docs/fields/relationship)
### Config ### Config
| Option | Description | | Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | | **`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 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 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. | | **`fields`** \* | Array of field types to correspond to each row of the Array. |
@@ -45,15 +47,17 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`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). | | **`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. |
_\* An asterisk denotes that a property is required._ _\* An asterisk denotes that a property is required._
### Admin Config ### Admin Config
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties: In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following
properties:
| Option | Description | | Option | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- | |---------------------------|----------------------------------------------------------------------------------------------------------------------|
| **`initCollapsed`** | Set the initial collapsed state | | **`initCollapsed`** | Set the initial collapsed state |
| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args | | **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |

View File

@@ -14,22 +14,23 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
</Banner> </Banner>
<LightDarkImage <LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/blocks.png" srcLight="https://payloadcms.com/images/docs/fields/blocks.png"
srcDark="https://payloadcms.com/images/docs/fields/blocks-dark.png" srcDark="https://payloadcms.com/images/docs/fields/blocks-dark.png"
alt="Admin panel screenshot of add Blocks drawer view" alt="Admin panel screenshot of add Blocks drawer view"
caption="Admin panel screenshot of add Blocks drawer view" caption="Admin panel screenshot of add Blocks drawer view"
/> />
**Example uses:** **Example uses:**
- A layout builder tool that grants editors to design highly customizable page or post layouts. Blocks could include configs such as `Quote`, `CallToAction`, `Slider`, `Content`, `Gallery`, or others. - A layout builder tool that grants editors to design highly customizable page or post layouts. Blocks could include
configs such as `Quote`, `CallToAction`, `Slider`, `Content`, `Gallery`, or others.
- A form builder tool where available block configs might be `Text`, `Select`, or `Checkbox`. - A form builder tool where available block configs might be `Text`, `Select`, or `Checkbox`.
- Virtual event agenda "timeslots" where a timeslot could either be a `Break`, a `Presentation`, or a `BreakoutSession`. - Virtual event agenda "timeslots" where a timeslot could either be a `Break`, a `Presentation`, or a `BreakoutSession`.
### Field config ### Field config
| Option | Description | | Option | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | | **`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 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 or an object with keys for each language. Auto-generated from name if not defined. |
| **`blocks`** \* | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. | | **`blocks`** \* | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
@@ -51,10 +52,11 @@ _\* An asterisk denotes that a property is required._
### Admin Config ### Admin Config
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties: In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following
properties:
| Option | Description | | Option | Description |
| ------------------- | ------------------------------- | |---------------------|---------------------------------|
| **`initCollapsed`** | Set the initial collapsed state | | **`initCollapsed`** | Set the initial collapsed state |
### Block configs ### Block configs
@@ -72,7 +74,7 @@ Blocks are defined as separate configs of their own.
</Banner> </Banner>
| Option | Description | | Option | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`slug`** \* | Identifier for this block type. Will be saved on each block as the `blockType` property. | | **`slug`** \* | Identifier for this block type. Will be saved on each block as the `blockType` property. |
| **`fields`** \* | Array of fields to be stored in this block. | | **`fields`** \* | Array of fields to be stored in this block. |
| **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. | | **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. |
@@ -80,7 +82,8 @@ Blocks are defined as separate configs of their own.
| **`imageAltText`** | Customize this block's image thumbnail alt text. | | **`imageAltText`** | Customize this block's image thumbnail alt text. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). | | **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. | | **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`dbName`** | Custom table name for this block type when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined.
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
#### Auto-generated data per block #### Auto-generated data per block
@@ -92,7 +95,8 @@ The `blockType` is saved as the slug of the block that has been selected.
**`blockName`** **`blockName`**
The Admin panel provides each block with a `blockName` field which optionally allows editors to label their blocks for better editability and readability. The Admin panel provides each block with a `blockName` field which optionally allows editors to label their blocks for
better editability and readability.
### Example ### Example
@@ -139,7 +143,8 @@ export const ExampleCollection: CollectionConfig = {
### TypeScript ### TypeScript
As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type: As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do
so, you can import and use Payload's `Block` type:
```ts ```ts
import type { Block } from 'payload/types' import type { Block } from 'payload/types'

View File

@@ -36,6 +36,7 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
| **`required`** | Require this field to have a value. | | **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`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.
_\* An asterisk denotes that a property is required._ _\* An asterisk denotes that a property is required._

View File

@@ -12,32 +12,34 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
</Banner> </Banner>
<LightDarkImage <LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/select.png" srcLight="https://payloadcms.com/images/docs/fields/select.png"
srcDark="https://payloadcms.com/images/docs/fields/select-dark.png" srcDark="https://payloadcms.com/images/docs/fields/select-dark.png"
alt="Shows a Select field in the Payload admin panel" alt="Shows a Select field in the Payload admin panel"
caption="Admin panel screenshot of a Select field" caption="Admin panel screenshot of a Select field"
/> />
### Config ### Config
| Option | Description | | Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) | | **`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. | | **`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. | | **`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. | | **`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. | | **`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) | | **`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. | | **`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/config), include its data in the user JWT. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) | | **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) | | **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
| **`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. | | **`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) | | **`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. | | **`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. | | **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. | | **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`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. |
_\* An asterisk denotes that a property is required._ _\* An asterisk denotes that a property is required._
@@ -52,7 +54,8 @@ _\* An asterisk denotes that a property is required._
### Admin config ### Admin config
In addition to the default [field admin config](/docs/fields/overview#admin-config), the Select field type also allows for the following admin-specific properties: In addition to the default [field admin config](/docs/fields/overview#admin-config), the Select field type also allows
for the following admin-specific properties:
**`isClearable`** **`isClearable`**
@@ -60,7 +63,8 @@ Set to `true` if you'd like this field to be clearable within the Admin UI.
**`isSortable`** **`isSortable`**
Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop. (Only works when `hasMany` is set to `true`) Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop. (Only works
when `hasMany` is set to `true`)
### Example ### Example
@@ -101,7 +105,8 @@ export const ExampleCollection: CollectionConfig = {
### Customization ### Customization
The Select field UI component can be customized by providing a custom React component to the `components` object in the Base config. The Select field UI component can be customized by providing a custom React component to the `components` object in the
Base config.
```ts ```ts
export const CustomSelectField: Field = { export const CustomSelectField: Field = {
@@ -156,27 +161,33 @@ export const CustomSelectComponent: React.FC<CustomSelectProps> = ({ path, optio
return ( return (
<div> <div>
<label className="field-label"> <label className = "field-label" >
Custom Select Custom
</label> Select
<SelectInput < /label>
path={path} < SelectInput
name={path} path = { path }
options={adjustedOptions} name = { path }
value={value} options = { adjustedOptions }
onChange={(e) => setValue(e.value)} value = { value }
/> onChange = {(e)
</div> =>
) setValue(e.value)
}
/>
< /div>
)
} }
``` ```
If you are looking to create a dynamic select field, the following tutorial will walk you through the process of creating a custom select field that fetches its options from an external API. If you are looking to create a dynamic select field, the following tutorial will walk you through the process of
creating a custom select field that fetches its options from an external API.
<VideoDrawer <VideoDrawer
id='Efn9OxSjA6Y' id='Efn9OxSjA6Y'
label='How to Create a Custom Select Field' label='How to Create a Custom Select Field'
drawerTitle='How to Create a Custom Select Field: A Step-by-Step Guide' drawerTitle='How to Create a Custom Select Field: A Step-by-Step Guide'
/> />
If you want to learn more about custom components check out the [Admin > Custom Component](/docs/admin/components#field-component) docs. If you want to learn more about custom components check out
the [Admin > Custom Component](/docs/admin/components#field-component) docs.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/db-mongodb", "name": "@payloadcms/db-mongodb",
"version": "1.4.3", "version": "1.4.4",
"description": "The officially supported MongoDB database adapter for Payload", "description": "The officially supported MongoDB database adapter for Payload",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -8,7 +8,7 @@ import { withSession } from './withSession'
export const createGlobal: CreateGlobal = async function createGlobal( export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter, this: MongooseAdapter,
{ data, req = {} as PayloadRequest, slug }, { slug, data, req = {} as PayloadRequest },
) { ) {
const Model = this.globals const Model = this.globals
const global = { const global = {

View File

@@ -10,7 +10,7 @@ import { withSession } from './withSession'
export const findGlobal: FindGlobal = async function findGlobal( export const findGlobal: FindGlobal = async function findGlobal(
this: MongooseAdapter, this: MongooseAdapter,
{ locale, req = {} as PayloadRequest, slug, where }, { slug, locale, req = {} as PayloadRequest, where },
) { ) {
const Model = this.globals const Model = this.globals
const options = { const options = {

View File

@@ -19,13 +19,14 @@ import buildCollectionSchema from './models/buildCollectionSchema'
import { buildGlobalModel } from './models/buildGlobalModel' import { buildGlobalModel } from './models/buildGlobalModel'
import buildSchema from './models/buildSchema' import buildSchema from './models/buildSchema'
import getBuildQueryPlugin from './queries/buildQuery' import getBuildQueryPlugin from './queries/buildQuery'
import { getDBName } from './utilities/getDBName'
export const init: Init = async function init(this: MongooseAdapter) { export const init: Init = async function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => { this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schema = buildCollectionSchema(collection, this.payload.config) const schema = buildCollectionSchema(collection, this.payload.config)
if (collection.versions) { if (collection.versions) {
const versionModelName = getVersionsModelName(collection) const versionModelName = getDBName({ config: collection, versions: true })
const versionCollectionFields = buildVersionCollectionFields(collection) const versionCollectionFields = buildVersionCollectionFields(collection)
@@ -54,12 +55,11 @@ export const init: Init = async function init(this: MongooseAdapter) {
versionSchema, versionSchema,
this.autoPluralization === true ? undefined : versionModelName, this.autoPluralization === true ? undefined : versionModelName,
) as CollectionModel ) as CollectionModel
// this.payload.versions[collection.slug] = model;
this.versions[collection.slug] = model this.versions[collection.slug] = model
} }
const model = mongoose.model( const model = mongoose.model(
collection.slug, getDBName({ config: collection }),
schema, schema,
this.autoPluralization === true ? undefined : collection.slug, this.autoPluralization === true ? undefined : collection.slug,
) as CollectionModel ) as CollectionModel
@@ -77,7 +77,7 @@ export const init: Init = async function init(this: MongooseAdapter) {
this.payload.config.globals.forEach((global) => { this.payload.config.globals.forEach((global) => {
if (global.versions) { if (global.versions) {
const versionModelName = getVersionsModelName(global) const versionModelName = getDBName({ config: global, versions: true })
const versionGlobalFields = buildVersionGlobalFields(global) const versionGlobalFields = buildVersionGlobalFields(global)

View File

@@ -361,7 +361,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
} }
if (field.localized && config.localization) { if (field.localized && config.localization) {
config.localization.locales.forEach((locale) => { config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale}`]: '2dsphere' }, indexOptions) schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
}) })
} else { } else {
schema.index({ [field.name]: '2dsphere' }, indexOptions) schema.index({ [field.name]: '2dsphere' }, indexOptions)

View File

@@ -62,7 +62,7 @@ export const sanitizeQueryValue = ({
formattedValue = Number(val) formattedValue = Number(val)
} }
if (field.type === 'date' && typeof val === 'string') { if (field.type === 'date' && typeof val === 'string' && operator !== 'exists') {
formattedValue = new Date(val) formattedValue = new Date(val)
if (Number.isNaN(Date.parse(formattedValue))) { if (Number.isNaN(Date.parse(formattedValue))) {
return undefined return undefined

View File

@@ -8,7 +8,7 @@ import { withSession } from './withSession'
export const updateGlobal: UpdateGlobal = async function updateGlobal( export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter, this: MongooseAdapter,
{ data, req = {} as PayloadRequest, slug }, { slug, data, req = {} as PayloadRequest },
) { ) {
const Model = this.globals const Model = this.globals
const options = { const options = {

View File

@@ -0,0 +1,41 @@
import type { DBIdentifierName } from 'payload/database'
type Args = {
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
locales?: boolean
target?: 'dbName' | 'enumName'
versions?: boolean
}
/**
* Used to name database enums and collections
* Returns the collection or enum name for a given entity
*/
export const getDBName = ({
config: { name, slug },
config,
target = 'dbName',
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({}) : custom
} else {
result = name ?? slug
}
if (versions) result = `_${result}_versions`
return result
}

View File

@@ -1,9 +1,8 @@
import type { Create } from 'payload/database' import type { Create } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export const create: Create = async function create( export const create: Create = async function create(
@@ -19,8 +18,11 @@ export const create: Create = async function create(
db, db,
fields: collection.fields, fields: collection.fields,
operation: 'create', operation: 'create',
tableName: toSnakeCase(collectionSlug),
req, req,
tableName: getTableName({
adapter: this,
config: collection,
}),
}) })
return result return result

View File

@@ -1,15 +1,14 @@
import type { CreateGlobalArgs } from 'payload/database' import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types' import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export async function createGlobal<T extends TypeWithID>( export async function createGlobal<T extends TypeWithID>(
this: PostgresAdapter, this: PostgresAdapter,
{ data, req = {} as PayloadRequest, slug }: CreateGlobalArgs, { slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
): Promise<T> { ): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
@@ -20,8 +19,11 @@ export async function createGlobal<T extends TypeWithID>(
db, db,
fields: globalConfig.fields, fields: globalConfig.fields,
operation: 'create', operation: 'create',
tableName: toSnakeCase(slug),
req, req,
tableName: getTableName({
adapter: this,
config: globalConfig,
}),
}) })
return result return result

View File

@@ -1,13 +1,13 @@
import type { TypeWithVersion } from 'payload/database' import type { TypeWithVersion } from 'payload/database'
import { type CreateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types' import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
import { type CreateGlobalVersionArgs } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions' import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export async function createGlobalVersion<T extends TypeWithID>( export async function createGlobalVersion<T extends TypeWithID>(
@@ -16,8 +16,11 @@ export async function createGlobalVersion<T extends TypeWithID>(
) { ) {
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug) const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const globalTableName = toSnakeCase(globalSlug) const tableName = getTableName({
const tableName = `_${globalTableName}_v` adapter: this,
config: global,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({ const result = await upsertRow<TypeWithVersion<T>>({
adapter: this, adapter: this,
@@ -29,17 +32,17 @@ export async function createGlobalVersion<T extends TypeWithID>(
db, db,
fields: buildVersionGlobalFields(global), fields: buildVersionGlobalFields(global),
operation: 'create', operation: 'create',
tableName,
req, req,
tableName,
}) })
const table = this.tables[tableName] const table = this.tables[tableName]
if (global.versions.drafts) { if (global.versions.drafts) {
await db.execute(sql` await db.execute(sql`
UPDATE ${table} UPDATE ${table}
SET latest = false SET latest = false
WHERE ${table.id} != ${result.id}; WHERE ${table.id} != ${result.id};
`) `)
} }

View File

@@ -3,10 +3,10 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions' import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export async function createVersion<T extends TypeWithID>( export async function createVersion<T extends TypeWithID>(
@@ -21,8 +21,11 @@ export async function createVersion<T extends TypeWithID>(
) { ) {
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config const collection = this.payload.collections[collectionSlug].config
const collectionTableName = toSnakeCase(collectionSlug) const tableName = getTableName({
const tableName = `_${collectionTableName}_v` adapter: this,
config: collection,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({ const result = await upsertRow<TypeWithVersion<T>>({
adapter: this, adapter: this,
@@ -35,22 +38,30 @@ export async function createVersion<T extends TypeWithID>(
db, db,
fields: buildVersionCollectionFields(collection), fields: buildVersionCollectionFields(collection),
operation: 'create', operation: 'create',
tableName,
req, req,
tableName,
}) })
const table = this.tables[tableName] const table = this.tables[tableName]
const relationshipsTable = this.tables[`${tableName}_rels`] const relationshipsTable =
this.tables[
getTableName({
adapter: this,
config: collection,
relationships: true,
versions: true,
})
]
if (collection.versions.drafts) { if (collection.versions.drafts) {
await db.execute(sql` await db.execute(sql`
UPDATE ${table} UPDATE ${table}
SET latest = false SET latest = false
FROM ${relationshipsTable} FROM ${relationshipsTable}
WHERE ${table.id} = ${relationshipsTable.parent} WHERE ${table.id} = ${relationshipsTable.parent}
AND ${relationshipsTable.path} = ${'parent'} AND ${relationshipsTable.path} = ${'parent'}
AND ${relationshipsTable[`${collectionSlug}ID`]} = ${parent} AND ${relationshipsTable[`${collectionSlug}ID`]} = ${parent}
AND ${table.id} != ${result.id}; AND ${table.id} != ${result.id};
`) `)
} }

View File

@@ -2,11 +2,11 @@ import type { DeleteMany } from 'payload/database'
import type { PayloadRequest } from 'payload/types' import type { PayloadRequest } from 'payload/types'
import { inArray } from 'drizzle-orm' import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const deleteMany: DeleteMany = async function deleteMany( export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter, this: PostgresAdapter,
@@ -14,7 +14,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
) { ) {
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config const collectionConfig = this.payload.collections[collection].config
const tableName = toSnakeCase(collection) const tableName = getTableName({ adapter: this, config: collectionConfig })
const result = await findMany({ const result = await findMany({
adapter: this, adapter: this,

View File

@@ -2,13 +2,13 @@ import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types' import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { buildFindManyArgs } from './find/buildFindManyArgs' import { buildFindManyArgs } from './find/buildFindManyArgs'
import buildQuery from './queries/buildQuery' import buildQuery from './queries/buildQuery'
import { selectDistinct } from './queries/selectDistinct' import { selectDistinct } from './queries/selectDistinct'
import { getTableName } from './schema/getTableName'
import { transform } from './transform/read' import { transform } from './transform/read'
export const deleteOne: DeleteOne = async function deleteOne( export const deleteOne: DeleteOne = async function deleteOne(
@@ -17,7 +17,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
) { ) {
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug) const tableName = getTableName({
adapter: this,
config: collection,
})
let docToDelete: Record<string, unknown> let docToDelete: Record<string, unknown>
const { joinAliases, joins, selectFields, where } = await buildQuery({ const { joinAliases, joins, selectFields, where } = await buildQuery({

View File

@@ -3,11 +3,11 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { inArray } from 'drizzle-orm' import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions' import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const deleteVersions: DeleteVersions = async function deleteVersion( export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter, this: PostgresAdapter,
@@ -16,7 +16,11 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${toSnakeCase(collection)}_v` const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig) const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({ const { docs } = await findMany({

View File

@@ -1,38 +1,41 @@
import type { Find } from 'payload/database' import type { Find } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types' import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const find: Find = async function find( export const find: Find = async function find(
this: PostgresAdapter, this: PostgresAdapter,
{ {
collection, collection,
limit: limitArg, limit,
locale, locale,
page = 1, page = 1,
pagination, pagination,
req = {} as PayloadRequest, req = {} as PayloadRequest,
sort: sortArg, sort: sortArg,
where: whereArg, where,
}, },
) { ) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
return findMany({ return findMany({
adapter: this, adapter: this,
fields: collectionConfig.fields, fields: collectionConfig.fields,
limit: limitArg, limit,
locale, locale,
page, page,
pagination, pagination,
req, req,
sort, sort,
tableName: toSnakeCase(collection), tableName,
where: whereArg, where,
}) })
} }

View File

@@ -53,7 +53,7 @@ export const buildFindManyArgs = ({
} }
} }
if (adapter.tables[`${tableName}_rels`]) { if (adapter.tables[`${tableName}${adapter.relationshipsSuffix}`]) {
result.with._rels = { result.with._rels = {
columns: { columns: {
id: false, id: false,
@@ -63,7 +63,7 @@ export const buildFindManyArgs = ({
} }
} }
if (adapter.tables[`${tableName}_locales`]) { if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
result.with._locales = _locales result.with._locales = _locales
} }

View File

@@ -2,11 +2,12 @@
import type { Field } from 'payload/types' import type { Field } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types' import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types' import type { PostgresAdapter } from '../types'
import type { Result } from './buildFindManyArgs' import type { Result } from './buildFindManyArgs'
import { getTableName } from '../schema/getTableName'
type TraverseFieldArgs = { type TraverseFieldArgs = {
_locales: Record<string, unknown> _locales: Record<string, unknown>
adapter: PostgresAdapter adapter: PostgresAdapter
@@ -78,9 +79,22 @@ export const traverseFields = ({
with: {}, with: {},
} }
const arrayTableName = `${currentTableName}_${path}${toSnakeCase(field.name)}` const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales const arrayTableNameWithLocales = getTableName({
adapter,
config: field,
locales: true,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[arrayTableNameWithLocales]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray currentArgs.with[`${path}${field.name}`] = withArray
traverseFields({ traverseFields({
@@ -128,9 +142,15 @@ export const traverseFields = ({
with: {}, with: {},
} }
const tableName = `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}` const tableName = getTableName({
adapter,
config: block,
parentTableName: topLevelTableName,
prefix: `${topLevelTableName}_blocks_`,
})
if (adapter.tables[`${tableName}_locales`]) withBlock.with._locales = _locales if (adapter.tables[`${tableName}${adapter.localesSuffix}`])
withBlock.with._locales = _locales
topLevelArgs.with[blockKey] = withBlock topLevelArgs.with[blockKey] = withBlock
traverseFields({ traverseFields({

View File

@@ -1,17 +1,19 @@
import type { FindGlobal } from 'payload/database' import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findGlobal: FindGlobal = async function findGlobal( export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter, this: PostgresAdapter,
{ locale, req, slug, where }, { slug, locale, req, where },
) { ) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = toSnakeCase(slug) const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const { const {
docs: [doc], docs: [doc],

View File

@@ -2,11 +2,11 @@ import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types' import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions' import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter, this: PostgresAdapter,
@@ -27,7 +27,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
) )
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt' const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const tableName = `_${toSnakeCase(global)}_v` const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig) const fields = buildVersionGlobalFields(globalConfig)
return findMany({ return findMany({

View File

@@ -1,17 +1,20 @@
import type { FindOneArgs } from 'payload/database' import type { FindOneArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types' import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export async function findOne<T extends TypeWithID>( export async function findOne<T extends TypeWithID>(
this: PostgresAdapter, this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where: incomingWhere }: FindOneArgs, { collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> { ): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
const { docs } = await findMany({ const { docs } = await findMany({
adapter: this, adapter: this,
@@ -22,8 +25,8 @@ export async function findOne<T extends TypeWithID>(
pagination: false, pagination: false,
req, req,
sort: undefined, sort: undefined,
tableName: toSnakeCase(collection), tableName,
where: incomingWhere, where,
}) })
return docs?.[0] || null return docs?.[0] || null

View File

@@ -2,11 +2,11 @@ import type { FindVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types' import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions' import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findVersions: FindVersions = async function findVersions( export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter, this: PostgresAdapter,
@@ -25,7 +25,11 @@ export const findVersions: FindVersions = async function findVersions(
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = `_${toSnakeCase(collection)}_v` const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig) const fields = buildVersionCollectionFields(collectionConfig)
return findMany({ return findMany({

View File

@@ -47,20 +47,24 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
name: 'postgres', name: 'postgres',
// Postgres-specific // Postgres-specific
blockTableNames: {},
drizzle: undefined, drizzle: undefined,
enums: {}, enums: {},
fieldConstraints: {}, fieldConstraints: {},
idType, idType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger, logger: args.logger,
pgSchema: undefined, pgSchema: undefined,
pool: undefined, pool: undefined,
poolOptions: args.pool, poolOptions: args.pool,
push: args.push, push: args.push,
relations: {}, relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',
schema: {}, schema: {},
schemaName: args.schemaName, schemaName: args.schemaName,
sessions: {}, sessions: {},
tables: {}, tables: {},
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter // DatabaseAdapter
beginTransaction, beginTransaction,

View File

@@ -4,11 +4,11 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core' import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions' import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build' import { buildTable } from './schema/build'
import { getTableName } from './schema/getTableName'
export const init: Init = async function init(this: PostgresAdapter) { export const init: Init = async function init(this: PostgresAdapter) {
if (this.schemaName) { if (this.schemaName) {
@@ -25,7 +25,10 @@ export const init: Init = async function init(this: PostgresAdapter) {
} }
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => { this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = toSnakeCase(collection.slug) const tableName = getTableName({
adapter: this,
config: collection,
})
buildTable({ buildTable({
adapter: this, adapter: this,
@@ -37,10 +40,15 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: collection.fields, fields: collection.fields,
tableName, tableName,
timestamps: collection.timestamps, timestamps: collection.timestamps,
versions: false,
}) })
if (collection.versions) { if (collection.versions) {
const versionsTableName = `_${tableName}_v` const versionsTableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const versionFields = buildVersionCollectionFields(collection) const versionFields = buildVersionCollectionFields(collection)
buildTable({ buildTable({
@@ -53,12 +61,13 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: versionFields, fields: versionFields,
tableName: versionsTableName, tableName: versionsTableName,
timestamps: true, timestamps: true,
versions: true,
}) })
} }
}) })
this.payload.config.globals.forEach((global) => { this.payload.config.globals.forEach((global) => {
const tableName = toSnakeCase(global.slug) const tableName = getTableName({ adapter: this, config: global })
buildTable({ buildTable({
adapter: this, adapter: this,
@@ -70,10 +79,11 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: global.fields, fields: global.fields,
tableName, tableName,
timestamps: false, timestamps: false,
versions: false,
}) })
if (global.versions) { if (global.versions) {
const versionsTableName = `_${tableName}_v` const versionsTableName = getTableName({ adapter: this, config: global, versions: true })
const versionFields = buildVersionGlobalFields(global) const versionFields = buildVersionGlobalFields(global)
buildTable({ buildTable({
@@ -86,6 +96,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: versionFields, fields: versionFields,
tableName: versionsTableName, tableName: versionsTableName,
timestamps: true, timestamps: true,
versions: true,
}) })
} }
}) })

View File

@@ -14,6 +14,8 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types' import type { GenericColumn, GenericTable, PostgresAdapter } from '../types'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery' import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'
import { getTableName } from '../schema/getTableName'
type Constraint = { type Constraint = {
columnName: string columnName: string
table: GenericTable | PgTableWithColumns<any> table: GenericTable | PgTableWithColumns<any>
@@ -95,7 +97,7 @@ export const getTableColumnFromPath = ({
field: { field: {
name: 'id', name: 'id',
type: adapter.idType === 'uuid' ? 'text' : 'number', type: adapter.idType === 'uuid' ? 'text' : 'number',
} as TextField | NumberField, } as NumberField | TextField,
table: adapter.tables[newTableName], table: adapter.tables[newTableName],
} }
} }
@@ -183,7 +185,13 @@ export const getTableColumnFromPath = ({
case 'group': { case 'group': {
if (locale && field.localized && adapter.payload.config.localization) { if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}_locales` newTableName = getTableName({
adapter,
config: field,
locales: true,
parentTableName: tableName,
prefix: `${tableName}_`,
})
joins[tableName] = eq( joins[tableName] = eq(
adapter.tables[tableName].id, adapter.tables[tableName].id,
@@ -218,7 +226,12 @@ export const getTableColumnFromPath = ({
} }
case 'array': { case 'array': {
newTableName = `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}` newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
constraintPath = `${constraintPath}${field.name}.%.` constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) { if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and( joins[newTableName] = and(
@@ -265,7 +278,12 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value] const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => { blockTypes.forEach((blockType) => {
const block = field.blocks.find((block) => block.slug === blockType) const block = field.blocks.find((block) => block.slug === blockType)
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}` newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
joins[newTableName] = eq( joins[newTableName] = eq(
adapter.tables[tableName].id, adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID, adapter.tables[newTableName]._parentID,
@@ -285,7 +303,12 @@ export const getTableColumnFromPath = ({
} }
const hasBlockField = field.blocks.some((block) => { const hasBlockField = field.blocks.some((block) => {
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}` newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
constraintPath = `${constraintPath}${field.name}.%.` constraintPath = `${constraintPath}${field.name}.%.`
let result let result
const blockConstraints = [] const blockConstraints = []
@@ -351,7 +374,7 @@ export const getTableColumnFromPath = ({
case 'relationship': case 'relationship':
case 'upload': { case 'upload': {
let relationshipFields let relationshipFields
const relationTableName = `${rootTableName}_rels` const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
const newCollectionPath = pathSegments.slice(1).join('.') const newCollectionPath = pathSegments.slice(1).join('.')
const aliasRelationshipTableName = uuid() const aliasRelationshipTableName = uuid()
const aliasRelationshipTable = alias( const aliasRelationshipTable = alias(
@@ -359,23 +382,45 @@ export const getTableColumnFromPath = ({
aliasRelationshipTableName, aliasRelationshipTableName,
) )
// Join in the relationships table if (locale && field.localized && adapter.payload.config.localization) {
joinAliases.push({ joinAliases.push({
condition: and( condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent), eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`), eq(aliasRelationshipTable.locale, locale),
), like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
table: aliasRelationshipTable, ),
}) table: aliasRelationshipTable,
})
if (locale !== 'all') {
constraints.push({
columnName: 'locale',
table: aliasRelationshipTable,
value: locale,
})
}
} else {
// Join in the relationships table
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
}
selectFields[`${relationTableName}.path`] = aliasRelationshipTable.path selectFields[`${relationTableName}.path`] = aliasRelationshipTable.path
let newAliasTable let newAliasTable
if (typeof field.relationTo === 'string') { if (typeof field.relationTo === 'string') {
newTableName = `${toSnakeCase(field.relationTo)}` const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = getTableName({
adapter,
config: relationshipConfig,
})
// parent to relationship join table // parent to relationship join table
relationshipFields = adapter.payload.collections[field.relationTo].config.fields relationshipFields = relationshipConfig.fields
newAliasTable = alias(adapter.tables[newTableName], toSnakeCase(uuid())) newAliasTable = alias(adapter.tables[newTableName], toSnakeCase(uuid()))
@@ -394,7 +439,11 @@ export const getTableColumnFromPath = ({
} }
} else if (newCollectionPath === 'value') { } else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map( const tableColumnsNames = field.relationTo.map(
(relationTo) => `"${aliasRelationshipTableName}"."${toSnakeCase(relationTo)}_id"`, (relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
) )
return { return {
constraints, constraints,
@@ -441,7 +490,7 @@ export const getTableColumnFromPath = ({
if (field.localized && adapter.payload.config.localization) { if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined // If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable // so it is not picked up below to be used as targetTable
newTableName = `${tableName}_locales` newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName] const parentTable = aliasTable || adapter.tables[tableName]

View File

@@ -66,7 +66,7 @@ export const sanitizeQueryValue = ({
formattedValue = Number(val) formattedValue = Number(val)
} }
if (field.type === 'date') { if (field.type === 'date' && operator !== 'exists') {
if (typeof val === 'string') { if (typeof val === 'string') {
formattedValue = new Date(val) formattedValue = new Date(val)
if (Number.isNaN(Date.parse(formattedValue))) { if (Number.isNaN(Date.parse(formattedValue))) {

View File

@@ -2,9 +2,9 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { type QueryDrafts, combineQueries } from 'payload/database' import { type QueryDrafts, combineQueries } from 'payload/database'
import { buildVersionCollectionFields } from 'payload/versions' import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import { findMany } from './find/findMany' import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const queryDrafts: QueryDrafts = async function queryDrafts({ export const queryDrafts: QueryDrafts = async function queryDrafts({
collection, collection,
@@ -17,7 +17,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts({
where, where,
}) { }) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${toSnakeCase(collection)}_v` const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig) const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where) const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -11,10 +11,10 @@ import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm' import { relations } from 'drizzle-orm'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core' import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types' import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types' import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types'
import { getTableName } from './getTableName'
import { parentIDColumnMap } from './parentIDColumnMap' import { parentIDColumnMap } from './parentIDColumnMap'
import { setColumnID } from './setColumnID' import { setColumnID } from './setColumnID'
import { traverseFields } from './traverseFields' import { traverseFields } from './traverseFields'
@@ -35,6 +35,7 @@ type Args = {
rootTableName?: string rootTableName?: string
tableName: string tableName: string
timestamps?: boolean timestamps?: boolean
versions: boolean
} }
type Result = { type Result = {
@@ -59,6 +60,7 @@ export const buildTable = ({
rootTableName: incomingRootTableName, rootTableName: incomingRootTableName,
tableName, tableName,
timestamps, timestamps,
versions,
}: Args): Result => { }: Args): Result => {
const rootTableName = incomingRootTableName || tableName const rootTableName = incomingRootTableName || tableName
const columns: Record<string, PgColumnBuilder> = baseColumns const columns: Record<string, PgColumnBuilder> = baseColumns
@@ -113,6 +115,7 @@ export const buildTable = ({
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild, rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType, rootTableIDColType: rootTableIDColType || idColType,
rootTableName, rootTableName,
versions,
})) }))
if (timestamps) { if (timestamps) {
@@ -147,7 +150,7 @@ export const buildTable = ({
adapter.tables[tableName] = table adapter.tables[tableName] = table
if (hasLocalizedField) { if (hasLocalizedField) {
const localeTableName = `${tableName}_locales` const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey() localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull() localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id') localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
@@ -288,11 +291,16 @@ export const buildTable = ({
} }
relationships.forEach((relationTo) => { relationships.forEach((relationTo) => {
const formattedRelationTo = toSnakeCase(relationTo) const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = getTableName({
adapter,
config: relationshipConfig,
throwValidationError: true,
})
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer' let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
const relatedCollectionCustomID = adapter.payload.collections[ const relatedCollectionCustomID = relationshipConfig.fields.find(
relationTo (field) => fieldAffectsData(field) && field.name === 'id',
].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id') )
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric' if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar' if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
@@ -301,7 +309,7 @@ export const buildTable = ({
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' }) ).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
}) })
const relationshipsTableName = `${tableName}_rels` const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationshipsTable = adapter.pgSchema.table( relationshipsTable = adapter.pgSchema.table(
relationshipsTableName, relationshipsTableName,
@@ -333,7 +341,11 @@ export const buildTable = ({
} }
relationships.forEach((relationTo) => { relationships.forEach((relationTo) => {
const relatedTableName = toSnakeCase(relationTo) const relatedTableName = getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,
})
const idColumnName = `${relationTo}ID` const idColumnName = `${relationTo}ID`
result[idColumnName] = one(adapter.tables[relatedTableName], { result[idColumnName] = one(adapter.tables[relatedTableName], {
fields: [relationshipsTable[idColumnName]], fields: [relationshipsTable[idColumnName]],

View File

@@ -0,0 +1,75 @@
import type { DBIdentifierName } from 'payload/database'
import { APIError } from 'payload/errors'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types'
type Args = {
adapter: PostgresAdapter
/** The collection, global or field config **/
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
/** Localized tables need to be given the locales suffix */
locales?: boolean
/** For nested tables passed for the user custom dbName functions to handle their own iterations */
parentTableName?: string
/** For sub tables (array for example) this needs to include the parentTableName */
prefix?: string
/** Adds the relationships suffix */
relationships?: boolean
/** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */
target?: 'dbName' | 'enumName'
throwValidationError?: boolean
/** Adds the versions suffix, should only be used on the base collection to duplicate suffixing */
versions?: boolean
}
/**
* Used to name database enums and tables
* Returns the table or enum name for a given entity
*/
export const getTableName = ({
adapter,
config: { name, slug },
config,
locales = false,
parentTableName,
prefix = '',
relationships = false,
target = 'dbName',
throwValidationError = false,
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({ tableName: parentTableName }) : custom
} else {
result = `${prefix}${toSnakeCase(name ?? slug)}`
}
if (locales) result = `${result}${adapter.localesSuffix}`
if (versions) result = `_${result}${adapter.versionsSuffix}`
if (relationships) result = `${result}${adapter.relationshipsSuffix}`
if (!throwValidationError) {
return result
}
if (result.length > 63) {
throw new APIError(
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
)
}
return result
}

View File

@@ -27,6 +27,7 @@ import type { GenericColumns, IDType, PostgresAdapter } from '../types'
import { hasLocalesTable } from '../utilities/hasLocalesTable' import { hasLocalesTable } from '../utilities/hasLocalesTable'
import { buildTable } from './build' import { buildTable } from './build'
import { createIndex } from './createIndex' import { createIndex } from './createIndex'
import { getTableName } from './getTableName'
import { idToUUID } from './idToUUID' import { idToUUID } from './idToUUID'
import { parentIDColumnMap } from './parentIDColumnMap' import { parentIDColumnMap } from './parentIDColumnMap'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical' import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical'
@@ -53,6 +54,7 @@ type Args = {
rootRelationsToBuild?: Map<string, string> rootRelationsToBuild?: Map<string, string>
rootTableIDColType: string rootTableIDColType: string
rootTableName: string rootTableName: string
versions: boolean
} }
type Result = { type Result = {
@@ -86,7 +88,9 @@ export const traverseFields = ({
rootRelationsToBuild, rootRelationsToBuild,
rootTableIDColType, rootTableIDColType,
rootTableName, rootTableName,
versions,
}: Args): Result => { }: Args): Result => {
const throwValidationError = true
let hasLocalizedField = false let hasLocalizedField = false
let hasLocalizedRelationshipField = false let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false let hasManyTextField: 'index' | boolean = false
@@ -217,7 +221,15 @@ export const traverseFields = ({
case 'radio': case 'radio':
case 'select': { case 'select': {
const enumName = `enum_${newTableName}_${toSnakeCase(field.name)}` const enumName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum( adapter.enums[enumName] = pgEnum(
enumName, enumName,
@@ -231,7 +243,14 @@ export const traverseFields = ({
) )
if (field.type === 'select' && field.hasMany) { if (field.type === 'select' && field.hasMany) {
const selectTableName = `${newTableName}_${toSnakeCase(field.name)}` const selectTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = { const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(), order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id') parent: parentIDColumnMap[parentIDColType]('parent_id')
@@ -266,6 +285,7 @@ export const traverseFields = ({
disableUnique, disableUnique,
fields: [], fields: [],
tableName: selectTableName, tableName: selectTableName,
versions,
}) })
relationsToBuild.set(fieldName, selectTableName) relationsToBuild.set(fieldName, selectTableName)
@@ -296,7 +316,13 @@ export const traverseFields = ({
case 'array': { case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}` const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
})
const baseColumns: Record<string, PgColumnBuilder> = { const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(), _order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id') _parentID: parentIDColumnMap[parentIDColType]('_parent_id')
@@ -334,6 +360,7 @@ export const traverseFields = ({
rootTableIDColType, rootTableIDColType,
rootTableName, rootTableName,
tableName: arrayTableName, tableName: arrayTableName,
versions,
}) })
if (subHasManyTextField) { if (subHasManyTextField) {
@@ -356,7 +383,7 @@ export const traverseFields = ({
} }
if (hasLocalesTable(field.fields)) { if (hasLocalesTable(field.fields)) {
result._locales = many(adapter.tables[`${arrayTableName}_locales`]) result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`])
} }
subRelationsToBuild.forEach((val, key) => { subRelationsToBuild.forEach((val, key) => {
@@ -375,7 +402,13 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => { field.blocks.forEach((block) => {
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}` const blockTableName = getTableName({
adapter,
config: block,
parentTableName: rootTableName,
prefix: `${rootTableName}_blocks_`,
throwValidationError,
})
if (!adapter.tables[blockTableName]) { if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = { const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(), _order: integer('_order').notNull(),
@@ -416,6 +449,7 @@ export const traverseFields = ({
rootTableIDColType, rootTableIDColType,
rootTableName, rootTableName,
tableName: blockTableName, tableName: blockTableName,
versions,
}) })
if (subHasManyTextField) { if (subHasManyTextField) {
@@ -439,7 +473,9 @@ export const traverseFields = ({
} }
if (hasLocalesTable(block.fields)) { if (hasLocalesTable(block.fields)) {
result._locales = many(adapter.tables[`${blockTableName}_locales`]) result._locales = many(
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
)
} }
subRelationsToBuild.forEach((val, key) => { subRelationsToBuild.forEach((val, key) => {
@@ -451,7 +487,7 @@ export const traverseFields = ({
) )
adapter.relations[`relations_${blockTableName}`] = blockTableRelations adapter.relations[`relations_${blockTableName}`] = blockTableRelations
} else if (process.env.NODE_ENV !== 'production') { } else if (process.env.NODE_ENV !== 'production' && !versions) {
validateExistingBlockIsIdentical({ validateExistingBlockIsIdentical({
block, block,
localized: field.localized, localized: field.localized,
@@ -459,7 +495,7 @@ export const traverseFields = ({
table: adapter.tables[blockTableName], table: adapter.tables[blockTableName],
}) })
} }
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName) rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
}) })
@@ -498,6 +534,7 @@ export const traverseFields = ({
rootRelationsToBuild, rootRelationsToBuild,
rootTableIDColType, rootTableIDColType,
rootTableName, rootTableName,
versions,
}) })
if (groupHasLocalizedField) hasLocalizedField = true if (groupHasLocalizedField) hasLocalizedField = true
@@ -540,6 +577,7 @@ export const traverseFields = ({
rootRelationsToBuild, rootRelationsToBuild,
rootTableIDColType, rootTableIDColType,
rootTableName, rootTableName,
versions,
}) })
if (groupHasLocalizedField) hasLocalizedField = true if (groupHasLocalizedField) hasLocalizedField = true
@@ -583,6 +621,7 @@ export const traverseFields = ({
rootRelationsToBuild, rootRelationsToBuild,
rootTableIDColType, rootTableIDColType,
rootTableName, rootTableName,
versions,
}) })
if (tabHasLocalizedField) hasLocalizedField = true if (tabHasLocalizedField) hasLocalizedField = true
@@ -626,6 +665,7 @@ export const traverseFields = ({
rootRelationsToBuild, rootRelationsToBuild,
rootTableIDColType, rootTableIDColType,
rootTableName, rootTableName,
versions,
}) })
if (rowHasLocalizedField) hasLocalizedField = true if (rowHasLocalizedField) hasLocalizedField = true

View File

@@ -18,7 +18,6 @@ type Args = {
data: Record<string, unknown>[] data: Record<string, unknown>[]
field: BlockField field: BlockField
locale?: string locale?: string
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[] numbers: Record<string, unknown>[]
path: string path: string
relationships: Record<string, unknown>[] relationships: Record<string, unknown>[]
@@ -26,6 +25,7 @@ type Args = {
selects: { selects: {
[tableName: string]: Record<string, unknown>[] [tableName: string]: Record<string, unknown>[]
} }
texts: Record<string, unknown>[]
} }
export const transformBlocks = ({ export const transformBlocks = ({
adapter, adapter,
@@ -35,12 +35,12 @@ export const transformBlocks = ({
data, data,
field, field,
locale, locale,
texts,
numbers, numbers,
path, path,
relationships, relationships,
relationshipsToDelete, relationshipsToDelete,
selects, selects,
texts,
}: Args) => { }: Args) => {
data.forEach((blockRow, i) => { data.forEach((blockRow, i) => {
if (typeof blockRow.blockType !== 'string') return if (typeof blockRow.blockType !== 'string') return
@@ -86,7 +86,6 @@ export const transformBlocks = ({
fieldPrefix: '', fieldPrefix: '',
fields: matchedBlock.fields, fields: matchedBlock.fields,
locales: newRow.locales, locales: newRow.locales,
texts,
numbers, numbers,
parentTableName: blockTableName, parentTableName: blockTableName,
path: `${path || ''}${field.name}.${i}.`, path: `${path || ''}${field.name}.${i}.`,
@@ -94,6 +93,7 @@ export const transformBlocks = ({
relationshipsToDelete, relationshipsToDelete,
row: newRow.row, row: newRow.row,
selects, selects,
texts,
}) })
blocks[blockType].push(newRow) blocks[blockType].push(newRow)

View File

@@ -7,6 +7,7 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../../types' import type { PostgresAdapter } from '../../types'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types' import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
import { getTableName } from '../../schema/getTableName'
import { isArrayOfRows } from '../../utilities/isArrayOfRows' import { isArrayOfRows } from '../../utilities/isArrayOfRows'
import { transformArray } from './array' import { transformArray } from './array'
import { transformBlocks } from './blocks' import { transformBlocks } from './blocks'
@@ -45,7 +46,6 @@ type Args = {
locales: { locales: {
[locale: string]: Record<string, unknown> [locale: string]: Record<string, unknown>
} }
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[] numbers: Record<string, unknown>[]
/** /**
* This is the name of the parent table * This is the name of the parent table
@@ -58,6 +58,7 @@ type Args = {
selects: { selects: {
[tableName: string]: Record<string, unknown>[] [tableName: string]: Record<string, unknown>[]
} }
texts: Record<string, unknown>[]
} }
export const traverseFields = ({ export const traverseFields = ({
@@ -73,7 +74,6 @@ export const traverseFields = ({
fields, fields,
forcedLocale, forcedLocale,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path, path,
@@ -81,6 +81,7 @@ export const traverseFields = ({
relationshipsToDelete, relationshipsToDelete,
row, row,
selects, selects,
texts,
}: Args) => { }: Args) => {
fields.forEach((field) => { fields.forEach((field) => {
let columnName = '' let columnName = ''
@@ -88,7 +89,12 @@ export const traverseFields = ({
let fieldData: unknown let fieldData: unknown
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
columnName = `${columnPrefix || ''}${toSnakeCase(field.name)}` columnName = `${columnPrefix || ''}${getTableName({
adapter,
config: field,
// do not pass columnPrefix here because it is required and custom dbNames also need it
prefix: '',
})}`
fieldName = `${fieldPrefix || ''}${field.name}` fieldName = `${fieldPrefix || ''}${field.name}`
fieldData = data[field.name] fieldData = data[field.name]
} }
@@ -111,12 +117,12 @@ export const traverseFields = ({
data: localeData, data: localeData,
field, field,
locale: localeKey, locale: localeKey,
texts,
numbers, numbers,
path, path,
relationships, relationships,
relationshipsToDelete, relationshipsToDelete,
selects, selects,
texts,
}) })
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows) arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
@@ -132,12 +138,12 @@ export const traverseFields = ({
blocksToDelete, blocksToDelete,
data: data[field.name], data: data[field.name],
field, field,
texts,
numbers, numbers,
path, path,
relationships, relationships,
relationshipsToDelete, relationshipsToDelete,
selects, selects,
texts,
}) })
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows) arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
@@ -147,8 +153,8 @@ export const traverseFields = ({
} }
if (field.type === 'blocks') { if (field.type === 'blocks') {
field.blocks.forEach(({ slug }) => { field.blocks.forEach((block) => {
blocksToDelete.add(toSnakeCase(slug)) blocksToDelete.add(getTableName({ adapter, config: block }))
}) })
if (field.localized) { if (field.localized) {
@@ -163,12 +169,12 @@ export const traverseFields = ({
data: localeData, data: localeData,
field, field,
locale: localeKey, locale: localeKey,
texts,
numbers, numbers,
path, path,
relationships, relationships,
relationshipsToDelete, relationshipsToDelete,
selects, selects,
texts,
}) })
} }
}) })
@@ -181,12 +187,12 @@ export const traverseFields = ({
blocksToDelete, blocksToDelete,
data: fieldData, data: fieldData,
field, field,
texts,
numbers, numbers,
path, path,
relationships, relationships,
relationshipsToDelete, relationshipsToDelete,
selects, selects,
texts,
}) })
} }
@@ -210,7 +216,6 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forcedLocale: localeKey, forcedLocale: localeKey,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${field.name}.`, path: `${path || ''}${field.name}.`,
@@ -218,6 +223,7 @@ export const traverseFields = ({
relationshipsToDelete, relationshipsToDelete,
row, row,
selects, selects,
texts,
}) })
}) })
} else { } else {
@@ -233,7 +239,6 @@ export const traverseFields = ({
fieldPrefix: `${fieldName}_`, fieldPrefix: `${fieldName}_`,
fields: field.fields, fields: field.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${field.name}.`, path: `${path || ''}${field.name}.`,
@@ -241,6 +246,7 @@ export const traverseFields = ({
relationshipsToDelete, relationshipsToDelete,
row, row,
selects, selects,
texts,
}) })
} }
} }
@@ -267,7 +273,6 @@ export const traverseFields = ({
fields: tab.fields, fields: tab.fields,
forcedLocale: localeKey, forcedLocale: localeKey,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${tab.name}.`, path: `${path || ''}${tab.name}.`,
@@ -275,6 +280,7 @@ export const traverseFields = ({
relationshipsToDelete, relationshipsToDelete,
row, row,
selects, selects,
texts,
}) })
}) })
} else { } else {
@@ -290,7 +296,6 @@ export const traverseFields = ({
fieldPrefix: `${fieldPrefix || ''}${tab.name}_`, fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
fields: tab.fields, fields: tab.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${tab.name}.`, path: `${path || ''}${tab.name}.`,
@@ -298,6 +303,7 @@ export const traverseFields = ({
relationshipsToDelete, relationshipsToDelete,
row, row,
selects, selects,
texts,
}) })
} }
} }
@@ -314,7 +320,6 @@ export const traverseFields = ({
fieldPrefix, fieldPrefix,
fields: tab.fields, fields: tab.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path, path,
@@ -322,6 +327,7 @@ export const traverseFields = ({
relationshipsToDelete, relationshipsToDelete,
row, row,
selects, selects,
texts,
}) })
} }
}) })
@@ -340,7 +346,6 @@ export const traverseFields = ({
fieldPrefix, fieldPrefix,
fields: field.fields, fields: field.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path, path,
@@ -348,6 +353,7 @@ export const traverseFields = ({
relationshipsToDelete, relationshipsToDelete,
row, row,
selects, selects,
texts,
}) })
} }
@@ -488,7 +494,11 @@ export const traverseFields = ({
} }
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = [] const valuesToTransform: {
localeKey?: string
ref: unknown
value: unknown
}[] = []
if (field.localized) { if (field.localized) {
if (typeof fieldData === 'object' && fieldData !== null) { if (typeof fieldData === 'object' && fieldData !== null) {

View File

@@ -23,12 +23,15 @@ import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>> export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = { export type Args = {
localesSuffix?: string
idType?: 'serial' | 'uuid' idType?: 'serial' | 'uuid'
logger?: DrizzleConfig['logger'] logger?: DrizzleConfig['logger']
migrationDir?: string migrationDir?: string
pool: PoolConfig pool: PoolConfig
push?: boolean push?: boolean
schemaName?: string schemaName?: string
relationshipsSuffix?: string
versionsSuffix?: string
} }
export type GenericColumn = PgColumn< export type GenericColumn = PgColumn<
@@ -58,6 +61,10 @@ export type DrizzleTransaction = PgTransaction<
> >
export type PostgresAdapter = BaseDatabaseAdapter & { export type PostgresAdapter = BaseDatabaseAdapter & {
/**
* Used internally to map the block name to the table name
*/
blockTableNames: Record<string, string>
drizzle: DrizzleDB drizzle: DrizzleDB
enums: Record<string, GenericEnum> enums: Record<string, GenericEnum>
/** /**
@@ -66,12 +73,14 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
*/ */
fieldConstraints: Record<string, Record<string, string>> fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType'] idType: Args['idType']
localesSuffix?: string
logger: DrizzleConfig['logger'] logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool pool: Pool
poolOptions: Args['pool'] poolOptions: Args['pool']
push: boolean push: boolean
relations: Record<string, GenericRelation> relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable> schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName'] schemaName?: Args['schemaName']
sessions: { sessions: {
@@ -82,14 +91,21 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
} }
} }
tables: Record<string, GenericTable | PgTableWithColumns<any>> tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
} }
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar' export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> } export type MigrateUpArgs = {
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> } payload: Payload
req?: Partial<PayloadRequest>
}
export type MigrateDownArgs = {
payload: Payload
req?: Partial<PayloadRequest>
}
declare module 'payload' { declare module 'payload' {
export interface DatabaseAdapter export interface DatabaseAdapter
@@ -98,9 +114,11 @@ declare module 'payload' {
drizzle: DrizzleDB drizzle: DrizzleDB
enums: Record<string, GenericEnum> enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>> fieldConstraints: Record<string, Record<string, string>>
localeSuffix?: string
pool: Pool pool: Pool
push: boolean push: boolean
relations: Record<string, GenericRelation> relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable> schema: Record<string, GenericEnum | GenericRelation | GenericTable>
sessions: { sessions: {
[id: string]: { [id: string]: {
@@ -110,5 +128,6 @@ declare module 'payload' {
} }
} }
tables: Record<string, GenericTable> tables: Record<string, GenericTable>
versionsSuffix?: string
} }
} }

View File

@@ -2,10 +2,12 @@ import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case' import toSnakeCase from 'to-snake-case'
import type { ChainedMethods } from './find/chainMethods'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery' import buildQuery from './queries/buildQuery'
import { selectDistinct } from './queries/selectDistinct' import { selectDistinct } from './queries/selectDistinct'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export const updateOne: UpdateOne = async function updateOne( export const updateOne: UpdateOne = async function updateOne(
@@ -14,7 +16,10 @@ export const updateOne: UpdateOne = async function updateOne(
) { ) {
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug) const tableName = getTableName({
adapter: this,
config: collection,
})
const whereToUse = whereArg || { id: { equals: id } } const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id let idToUpdate = id
@@ -49,7 +54,7 @@ export const updateOne: UpdateOne = async function updateOne(
fields: collection.fields, fields: collection.fields,
operation: 'update', operation: 'update',
req, req,
tableName: toSnakeCase(collectionSlug), tableName,
}) })
return result return result

View File

@@ -1,19 +1,21 @@
import type { UpdateGlobalArgs } from 'payload/database' import type { UpdateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types' import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export async function updateGlobal<T extends TypeWithID>( export async function updateGlobal<T extends TypeWithID>(
this: PostgresAdapter, this: PostgresAdapter,
{ data, req = {} as PayloadRequest, slug }: UpdateGlobalArgs, { slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs,
): Promise<T> { ): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = toSnakeCase(slug) const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const existingGlobal = await db.query[tableName].findFirst({}) const existingGlobal = await db.query[tableName].findFirst({})
@@ -23,8 +25,8 @@ export async function updateGlobal<T extends TypeWithID>(
data, data,
db, db,
fields: globalConfig.fields, fields: globalConfig.fields,
tableName,
req, req,
tableName,
}) })
return result return result

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types' import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions' import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery' import buildQuery from './queries/buildQuery'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export async function updateGlobalVersion<T extends TypeWithID>( export async function updateGlobalVersion<T extends TypeWithID>(
@@ -25,7 +25,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global, ({ slug }) => slug === global,
) )
const whereToUse = whereArg || { id: { equals: id } } const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${toSnakeCase(global)}_v` const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig) const fields = buildVersionGlobalFields(globalConfig)
const { where } = await buildQuery({ const { where } = await buildQuery({
@@ -43,9 +47,9 @@ export async function updateGlobalVersion<T extends TypeWithID>(
db, db,
fields, fields,
operation: 'update', operation: 'update',
req,
tableName, tableName,
where, where,
req,
}) })
return result return result

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types' import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions' import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery' import buildQuery from './queries/buildQuery'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow' import { upsertRow } from './upsertRow'
export async function updateVersion<T extends TypeWithID>( export async function updateVersion<T extends TypeWithID>(
@@ -23,7 +23,11 @@ export async function updateVersion<T extends TypeWithID>(
const db = this.sessions[req.transactionID]?.db || this.drizzle const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } } const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${toSnakeCase(collection)}_v` const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig) const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({ const { where } = await buildQuery({
@@ -41,9 +45,9 @@ export async function updateVersion<T extends TypeWithID>(
db, db,
fields, fields,
operation: 'update', operation: 'update',
req,
tableName, tableName,
where, where,
req,
}) })
return result return result

View File

@@ -138,7 +138,7 @@ export const upsertRow = async <T extends TypeWithID>({
// ////////////////////////////////// // //////////////////////////////////
if (localesToInsert.length > 0) { if (localesToInsert.length > 0) {
const localeTable = adapter.tables[`${tableName}_locales`] const localeTable = adapter.tables[`${tableName}${adapter.localesSuffix}`]
if (operation === 'update') { if (operation === 'update') {
await db.delete(localeTable).where(eq(localeTable._parentID, insertedRow.id)) await db.delete(localeTable).where(eq(localeTable._parentID, insertedRow.id))
@@ -151,7 +151,7 @@ export const upsertRow = async <T extends TypeWithID>({
// INSERT RELATIONSHIPS // INSERT RELATIONSHIPS
// ////////////////////////////////// // //////////////////////////////////
const relationshipsTableName = `${tableName}_rels` const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
if (operation === 'update') { if (operation === 'update') {
await deleteExistingRowsByPath({ await deleteExistingRowsByPath({
@@ -224,15 +224,16 @@ export const upsertRow = async <T extends TypeWithID>({
if (operation === 'update') { if (operation === 'update') {
for (const blockName of rowToInsert.blocksToDelete) { for (const blockName of rowToInsert.blocksToDelete) {
const blockTableName = `${tableName}_blocks_${blockName}` const blockTableName = adapter.blockTableNames[`${tableName}.${blockName}`]
const blockTable = adapter.tables[blockTableName] const blockTable = adapter.tables[blockTableName]
await db.delete(blockTable).where(eq(blockTable._parentID, insertedRow.id)) await db.delete(blockTable).where(eq(blockTable._parentID, insertedRow.id))
} }
} }
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) { for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
const blockTableName = adapter.blockTableNames[`${tableName}.${blockName}`]
insertedBlockRows[blockName] = await db insertedBlockRows[blockName] = await db
.insert(adapter.tables[`${tableName}_blocks_${blockName}`]) .insert(adapter.tables[blockTableName])
.values(blockRows.map(({ row }) => row)) .values(blockRows.map(({ row }) => row))
.returning() .returning()
@@ -259,7 +260,7 @@ export const upsertRow = async <T extends TypeWithID>({
if (blockLocaleRowsToInsert.length > 0) { if (blockLocaleRowsToInsert.length > 0) {
await db await db
.insert(adapter.tables[`${tableName}_blocks_${blockName}_locales`]) .insert(adapter.tables[`${blockTableName}${adapter.localesSuffix}`])
.values(blockLocaleRowsToInsert) .values(blockLocaleRowsToInsert)
.returning() .returning()
} }

View File

@@ -71,7 +71,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
} }
// Insert locale rows // Insert locale rows
if (adapter.tables[`${tableName}_locales`] && row.locales.length > 0) { if (adapter.tables[`${tableName}${adapter.localesSuffix}`] && row.locales.length > 0) {
if (!row.locales[0]._parentID) { if (!row.locales[0]._parentID) {
row.locales = row.locales.map((localeRow, i) => { row.locales = row.locales.map((localeRow, i) => {
if (typeof localeRow._getParentID === 'function') { if (typeof localeRow._getParentID === 'function') {
@@ -81,7 +81,10 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
return localeRow return localeRow
}) })
} }
await db.insert(adapter.tables[`${tableName}_locales`]).values(row.locales).returning() await db
.insert(adapter.tables[`${tableName}${adapter.localesSuffix}`])
.values(row.locales)
.returning()
} }
// If there are sub arrays, call this function recursively // If there are sub arrays, call this function recursively

View File

@@ -9,8 +9,8 @@ type BaseArgs = {
db: DrizzleDB db: DrizzleDB
fields: Field[] fields: Field[]
path?: string path?: string
tableName: string
req: PayloadRequest req: PayloadRequest
tableName: string
} }
type CreateArgs = BaseArgs & { type CreateArgs = BaseArgs & {

View File

@@ -1,6 +1,6 @@
{ {
"name": "payload", "name": "payload",
"version": "2.12.0", "version": "2.12.1",
"description": "Node, React and MongoDB Headless CMS and Application Framework", "description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -17,7 +17,7 @@ import './index.scss'
const baseClass = 'duplicate' const baseClass = 'duplicate'
const Duplicate: React.FC<Props> = ({ id, collection, slug }) => { const Duplicate: React.FC<Props> = ({ id, slug, collection }) => {
const { push } = useHistory() const { push } = useHistory()
const modified = useFormModified() const modified = useFormModified()
const { toggleModal } = useModal() const { toggleModal } = useModal()
@@ -31,12 +31,15 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
routes: { admin }, routes: { admin },
} = useConfig() } = useConfig()
const [hasClicked, setHasClicked] = useState<boolean>(false) const [hasClicked, setHasClicked] = useState<boolean>(false)
const [isSubmitting, setIsSubmitting] = useState<boolean>(false)
const { i18n, t } = useTranslation('general') const { i18n, t } = useTranslation('general')
const modalSlug = `duplicate-${id}` const modalSlug = `duplicate-${id}`
const handleClick = useCallback( const handleClick = useCallback(
async (override = false) => { async (override = false) => {
if (isSubmitting) return
setIsSubmitting(true)
setHasClicked(true) setHasClicked(true)
if (modified && !override) { if (modified && !override) {
@@ -144,6 +147,7 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
} }
setModified(false) setModified(false)
setIsSubmitting(false)
setTimeout(() => { setTimeout(() => {
push({ push({
@@ -170,13 +174,17 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
) )
const confirm = useCallback(async () => { const confirm = useCallback(async () => {
setHasClicked(false)
await handleClick(true) await handleClick(true)
setHasClicked(false)
}, [handleClick]) }, [handleClick])
return ( return (
<React.Fragment> <React.Fragment>
<PopupList.Button id="action-duplicate" onClick={() => handleClick(false)}> <PopupList.Button
disabled={isSubmitting}
id="action-duplicate"
onClick={() => handleClick(false)}
>
{t('duplicate')} {t('duplicate')}
</PopupList.Button> </PopupList.Button>
{modified && hasClicked && ( {modified && hasClicked && (

View File

@@ -27,6 +27,7 @@ type MenuButtonProps = {
active?: boolean active?: boolean
children: React.ReactNode children: React.ReactNode
className?: string className?: string
disabled?: boolean
id?: string id?: string
onClick?: () => void onClick?: () => void
to?: LinkProps['to'] to?: LinkProps['to']
@@ -36,6 +37,7 @@ export const Button: React.FC<MenuButtonProps> = ({
active, active,
children, children,
className, className,
disabled,
onClick, onClick,
to, to,
}) => { }) => {
@@ -64,6 +66,7 @@ export const Button: React.FC<MenuButtonProps> = ({
return ( return (
<button <button
className={classes} className={classes}
disabled={disabled}
id={id} id={id}
onClick={() => { onClick={() => {
if (onClick) { if (onClick) {

View File

@@ -9,11 +9,16 @@ import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale' import { useLocale } from '../../utilities/Locale'
import RenderCustomComponent from '../../utilities/RenderCustomComponent' import RenderCustomComponent from '../../utilities/RenderCustomComponent'
export type CustomPublishButtonProps = React.ComponentType< export type CustomPublishButtonType = React.ComponentType<
DefaultPublishButtonProps & { DefaultPublishButtonProps & {
DefaultButton: React.ComponentType<DefaultPublishButtonProps> DefaultButton: React.ComponentType<DefaultPublishButtonProps>
} }
> >
/**
* @deprecated Use `CustomPublishButtonType` instead - renamed from `CustomPublishButtonProps`
*/
export type CustomPublishButtonProps = CustomPublishButtonType
export type DefaultPublishButtonProps = { export type DefaultPublishButtonProps = {
canPublish: boolean canPublish: boolean
disabled: boolean disabled: boolean
@@ -38,7 +43,7 @@ const DefaultPublishButton: React.FC<DefaultPublishButtonProps> = ({
} }
type Props = { type Props = {
CustomComponent?: CustomPublishButtonProps CustomComponent?: CustomPublishButtonType
} }
export const Publish: React.FC<Props> = ({ CustomComponent }) => { export const Publish: React.FC<Props> = ({ CustomComponent }) => {

View File

@@ -85,15 +85,15 @@
html[data-theme='light'] { html[data-theme='light'] {
.tooltip { .tooltip {
background-color: var(--theme-error-250); background-color: var(--theme-elevation-100);
color: var(--theme-error-750); color: var(--theme-elevation-1000);
&--position-top:after { &--position-top:after {
border-top-color: var(--theme-error-250); border-top-color: var(--theme-elevation-100);
} }
&--position-bottom:after { &--position-bottom:after {
border-bottom-color: var(--theme-error-250); border-bottom-color: var(--theme-elevation-100);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
export type { CustomPreviewButtonProps } from './PreviewButton' export type { CustomPreviewButtonProps } from './PreviewButton'
export type { CustomPublishButtonProps } from './Publish' export type { CustomPublishButtonProps, CustomPublishButtonType } from './Publish'
export type { CustomSaveButtonProps } from './Save' export type { CustomSaveButtonProps } from './Save'
export type { CustomSaveDraftButtonProps } from './SaveDraft' export type { CustomSaveDraftButtonProps } from './SaveDraft'

View File

@@ -1,9 +1,34 @@
export const getSupportedDateLocale = (locale = 'enUS'): string => { export const getSupportedDateLocale = (locale = 'enUS'): string => {
// Need to match our translation locales with the local codes of 'date-fns/locale to support date locales
const formattedLocales = { const formattedLocales = {
ar: 'ar',
az: 'az',
bg: 'bg',
cs: 'cs',
de: 'de',
en: 'enUS', en: 'enUS',
es: 'es',
fa: 'faIR',
fr: 'fr',
hr: 'hr',
hu: 'hu',
it: 'it',
ja: 'ja',
ko: 'ko',
my: 'enUS', // Burmese is not currently supported my: 'enUS', // Burmese is not currently supported
nb: 'nb',
nl: 'nl',
pl: 'pl',
pt: 'pt',
ro: 'ro',
ru: 'ru',
sv: 'sv',
th: 'th',
tr: 'tr',
ua: 'uk', ua: 'uk',
vi: 'vi',
zh: 'zhCN', zh: 'zhCN',
zhTw: 'zhTW',
} }
return formattedLocales[locale] || locale return formattedLocales[locale] || locale

View File

@@ -13,6 +13,7 @@ const strategyBaseSchema = joi.object().keys({
}) })
const collectionSchema = joi.object().keys({ const collectionSchema = joi.object().keys({
slug: joi.string().required(),
access: joi.object({ access: joi.object({
admin: joi.func(), admin: joi.func(),
create: joi.func(), create: joi.func(),
@@ -117,6 +118,7 @@ const collectionSchema = joi.object().keys({
joi.boolean(), joi.boolean(),
), ),
custom: joi.object().pattern(joi.string(), joi.any()), custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultSort: joi.string(), defaultSort: joi.string(),
endpoints: endpointsSchema, endpoints: endpointsSchema,
fields: joi.array(), fields: joi.array(),
@@ -158,7 +160,6 @@ const collectionSchema = joi.object().keys({
.alternatives() .alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])), .try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
}), }),
slug: joi.string().required(),
timestamps: joi.boolean(), timestamps: joi.boolean(),
typescript: joi.object().keys({ typescript: joi.object().keys({
interface: joi.string(), interface: joi.string(),

View File

@@ -6,7 +6,7 @@ import type { DeepRequired } from 'ts-essentials'
import type { GeneratedTypes } from '../../' import type { GeneratedTypes } from '../../'
import type { import type {
CustomPreviewButtonProps, CustomPreviewButtonProps,
CustomPublishButtonProps, CustomPublishButtonType,
CustomSaveButtonProps, CustomSaveButtonProps,
CustomSaveDraftButtonProps, CustomSaveDraftButtonProps,
} from '../../admin/components/elements/types' } from '../../admin/components/elements/types'
@@ -21,6 +21,7 @@ import type {
GeneratePreviewURL, GeneratePreviewURL,
LivePreviewConfig, LivePreviewConfig,
} from '../../config/types' } from '../../config/types'
import type { DBIdentifierName } from '../../database/types'
import type { PayloadRequest, RequestContext } from '../../express/types' import type { PayloadRequest, RequestContext } from '../../express/types'
import type { Field } from '../../fields/config/types' import type { Field } from '../../fields/config/types'
import type { IncomingUploadType, Upload } from '../../uploads/types' import type { IncomingUploadType, Upload } from '../../uploads/types'
@@ -105,7 +106,9 @@ export type BeforeReadHook<T extends TypeWithID = any> = (args: {
collection: SanitizedCollectionConfig collection: SanitizedCollectionConfig
context: RequestContext context: RequestContext
doc: T doc: T
query: { [key: string]: any } query: {
[key: string]: any
}
req: PayloadRequest req: PayloadRequest
}) => any }) => any
@@ -115,7 +118,9 @@ export type AfterReadHook<T extends TypeWithID = any> = (args: {
context: RequestContext context: RequestContext
doc: T doc: T
findMany?: boolean findMany?: boolean
query?: { [key: string]: any } query?: {
[key: string]: any
}
req: PayloadRequest req: PayloadRequest
}) => any }) => any
@@ -146,7 +151,10 @@ export type AfterErrorHook = (
context: RequestContext, context: RequestContext,
/** The collection which this hook is being run on. This is null if the AfterError hook was be added to the payload-wide config */ /** The collection which this hook is being run on. This is null if the AfterError hook was be added to the payload-wide config */
collection: SanitizedCollectionConfig | null, collection: SanitizedCollectionConfig | null,
) => { response: any; status: number } | void ) => {
response: any
status: number
} | void
export type BeforeLoginHook<T extends TypeWithID = any> = (args: { export type BeforeLoginHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */ /** The collection which this hook is being run on */
@@ -228,7 +236,7 @@ export type CollectionAdminOptions = {
* Replaces the "Publish" button * Replaces the "Publish" button
* + drafts must be enabled * + drafts must be enabled
*/ */
PublishButton?: CustomPublishButtonProps PublishButton?: CustomPublishButtonType
/** /**
* Replaces the "Save" button * Replaces the "Save" button
* + drafts must be disabled * + drafts must be disabled
@@ -333,8 +341,7 @@ export type CollectionAdminOptions = {
useAsTitle?: string useAsTitle?: string
} }
/** Manage all aspects of a data collection */ export type BaseCollectionConfig = {
export type CollectionConfig = {
/** /**
* Access control * Access control
*/ */
@@ -359,6 +366,11 @@ export type CollectionConfig = {
auth?: IncomingAuthType | boolean auth?: IncomingAuthType | boolean
/** Extension point to add your custom data. */ /** Extension point to add your custom data. */
custom?: Record<string, any> custom?: Record<string, any>
/**
* Used to override the default naming of the database table or collection with your using a function or string
* @WARNING: If you change this property with existing data, you will need to handle the renaming of the table in your database or by using migrations
*/
dbName?: DBIdentifierName
/** /**
* Default field to sort by in collection list view * Default field to sort by in collection list view
*/ */
@@ -427,6 +439,7 @@ export type CollectionConfig = {
* @default false // disable uploads * @default false // disable uploads
*/ */
upload?: IncomingUploadType | boolean upload?: IncomingUploadType | boolean
/** /**
* Customize the handling of incoming file uploads * Customize the handling of incoming file uploads
* *
@@ -435,6 +448,9 @@ export type CollectionConfig = {
versions?: IncomingCollectionVersions | boolean versions?: IncomingCollectionVersions | boolean
} }
/** Manage all aspects of a data collection */
export type CollectionConfig = BaseCollectionConfig
export interface SanitizedCollectionConfig export interface SanitizedCollectionConfig
extends Omit< extends Omit<
DeepRequired<CollectionConfig>, DeepRequired<CollectionConfig>,

View File

@@ -403,3 +403,10 @@ export type PaginatedDocs<T = any> = {
totalDocs: number totalDocs: number
totalPages: number totalPages: number
} }
export type DBIdentifierName =
| ((Args: {
/** The name of the parent table when using relational DBs */
tableName?: string
}) => string)
| string

View File

@@ -12,6 +12,7 @@ export {
CreateMigration, CreateMigration,
CreateVersion, CreateVersion,
CreateVersionArgs, CreateVersionArgs,
DBIdentifierName,
DeleteMany, DeleteMany,
DeleteManyArgs, DeleteManyArgs,
DeleteOne, DeleteOne,

View File

@@ -19,6 +19,7 @@ export { FileData, ImageSize, IncomingUploadType } from '../uploads/types'
export type { export type {
CustomPublishButtonProps, CustomPublishButtonProps,
CustomPublishButtonType,
CustomSaveButtonProps, CustomSaveButtonProps,
CustomSaveDraftButtonProps, CustomSaveDraftButtonProps,
} from './../admin/components/elements/types' } from './../admin/components/elements/types'

View File

@@ -1,5 +1,5 @@
import type { Config } from '../../config/types' import type { Config } from '../../config/types'
import type { Field } from './types' import type { Block, Field } from './types'
import withCondition from '../../admin/components/forms/withCondition' import withCondition from '../../admin/components/forms/withCondition'
import { import {
@@ -18,6 +18,7 @@ type Args = {
config: Config config: Config
existingFieldNames?: Set<string> existingFieldNames?: Set<string>
fields: Field[] fields: Field[]
sanitizedBlocksMap?: Record<string, Block>
/** /**
* If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array. * If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array.
* This validation will be skipped if validRelationships is null. * This validation will be skipped if validRelationships is null.
@@ -29,6 +30,7 @@ export const sanitizeFields = ({
config, config,
existingFieldNames = new Set(), existingFieldNames = new Set(),
fields, fields,
sanitizedBlocksMap = {},
validRelationships, validRelationships,
}: Args): Field[] => { }: Args): Field[] => {
if (!fields) return [] if (!fields) return []
@@ -96,10 +98,16 @@ export const sanitizeFields = ({
} }
if (field.type === 'blocks' && field.blocks) { if (field.type === 'blocks' && field.blocks) {
field.blocks = field.blocks.map((block) => ({ field.blocks = field.blocks.map((block) => {
...block, // break recursion
fields: block.fields.concat(baseBlockFields), if (!block.slug && Object.keys(block)[0]) {
})) return sanitizedBlocksMap[Object.keys(block)[0]]
}
return {
...block,
fields: block.fields.concat(baseBlockFields),
}
})
} }
if (field.type === 'array' && field.fields) { if (field.type === 'array' && field.fields) {
@@ -113,7 +121,7 @@ export const sanitizeFields = ({
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
if (existingFieldNames.has(field.name)) { if (existingFieldNames.has(field.name)) {
throw new DuplicateFieldName(field.name) throw new DuplicateFieldName(field.name)
} else if (!['id', 'blockName'].includes(field.name)) { } else if (!['blockName', 'id'].includes(field.name)) {
existingFieldNames.add(field.name) existingFieldNames.add(field.name)
} }
@@ -169,16 +177,29 @@ export const sanitizeFields = ({
if ('blocks' in field && field.blocks) { if ('blocks' in field && field.blocks) {
field.blocks = field.blocks.map((block) => { field.blocks = field.blocks.map((block) => {
const unsanitizedBlock = { ...block } if (sanitizedBlocksMap[block?.interfaceName || block.slug]) {
return sanitizedBlocksMap[block?.interfaceName || block.slug]
}
// break recursion
if (!block.slug && Object.keys(block)[0]) {
return sanitizedBlocksMap[Object.keys(block)[0]]
}
sanitizedBlocksMap[block?.interfaceName || block.slug] = { ...block }
const unsanitizedBlock = sanitizedBlocksMap[block?.interfaceName || block.slug]
unsanitizedBlock.labels = !unsanitizedBlock.labels unsanitizedBlock.labels = !unsanitizedBlock.labels
? formatLabels(unsanitizedBlock.slug) ? formatLabels(unsanitizedBlock.slug)
: unsanitizedBlock.labels : unsanitizedBlock.labels
unsanitizedBlock.fields = sanitizeFields({ unsanitizedBlock.fields = sanitizeFields({
config, config,
fields: block.fields,
validRelationships,
existingFieldNames: new Set(), existingFieldNames: new Set(),
fields: block.fields,
sanitizedBlocksMap,
validRelationships,
}) })
return unsanitizedBlock return unsanitizedBlock

View File

@@ -200,9 +200,11 @@ export const select = baseField.keys({
isClearable: joi.boolean().default(false), isClearable: joi.boolean().default(false),
isSortable: joi.boolean().default(false), isSortable: joi.boolean().default(false),
}), }),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi defaultValue: joi
.alternatives() .alternatives()
.try(joi.string().allow(''), joi.array().items(joi.string().allow('')), joi.func()), .try(joi.string().allow(''), joi.array().items(joi.string().allow('')), joi.func()),
enumName: joi.alternatives().try(joi.string(), joi.func()),
hasMany: joi.boolean().default(false), hasMany: joi.boolean().default(false),
options: joi options: joi
.array() .array()
@@ -232,6 +234,7 @@ export const radio = baseField.keys({
layout: joi.string().valid('vertical', 'horizontal'), layout: joi.string().valid('vertical', 'horizontal'),
}), }),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()), defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
enumName: joi.alternatives().try(joi.string(), joi.func()),
options: joi options: joi
.array() .array()
.min(1) .min(1)
@@ -309,6 +312,7 @@ export const array = baseField.keys({
.default({}), .default({}),
}) })
.default({}), .default({}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()), defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
fields: joi.array().items(joi.link('#field')).required(), fields: joi.array().items(joi.link('#field')).required(),
interfaceName: joi.string(), interfaceName: joi.string(),
@@ -409,6 +413,7 @@ export const blocks = baseField.keys({
joi.object({ joi.object({
slug: joi.string().required(), slug: joi.string().required(),
custom: joi.object().pattern(joi.string(), joi.any()), custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
fields: joi.array().items(joi.link('#field')), fields: joi.array().items(joi.link('#field')),
graphQL: joi.object().keys({ graphQL: joi.object().keys({
singularName: joi.string(), singularName: joi.string(),

View File

@@ -15,6 +15,7 @@ import type { RichTextAdapter } from '../../admin/components/forms/field-types/R
import type { User } from '../../auth' import type { User } from '../../auth'
import type { SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types' import type { SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types'
import type { SanitizedConfig } from '../../config/types' import type { SanitizedConfig } from '../../config/types'
import type { DBIdentifierName } from '../../database/types'
import type { PayloadRequest, RequestContext } from '../../express/types' import type { PayloadRequest, RequestContext } from '../../express/types'
import type { SanitizedGlobalConfig } from '../../globals/config/types' import type { SanitizedGlobalConfig } from '../../globals/config/types'
import type { Payload } from '../../payload' import type { Payload } from '../../payload'
@@ -78,7 +79,11 @@ export type FieldAccess<T extends TypeWithID = any, P = any, U = any> = (args: {
export type Condition<T extends TypeWithID = any, P = any> = ( export type Condition<T extends TypeWithID = any, P = any> = (
data: Partial<T>, data: Partial<T>,
siblingData: Partial<P>, siblingData: Partial<P>,
{ user }: { user: User }, {
user,
}: {
user: User
},
) => boolean ) => boolean
export type FilterOptionsProps<T = any> = { export type FilterOptionsProps<T = any> = {
@@ -443,6 +448,14 @@ export type SelectField = FieldBase & {
isClearable?: boolean isClearable?: boolean
isSortable?: boolean isSortable?: boolean
} }
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
/**
* Customize the DB enum name
*/
enumName?: DBIdentifierName
hasMany?: boolean hasMany?: boolean
options: Option[] options: Option[]
type: 'select' type: 'select'
@@ -492,7 +505,9 @@ type RelationshipAdmin = Admin & {
} }
export type PolymorphicRelationshipField = SharedRelationshipProperties & { export type PolymorphicRelationshipField = SharedRelationshipProperties & {
admin?: RelationshipAdmin & { admin?: RelationshipAdmin & {
sortOptions?: { [collectionSlug: string]: string } sortOptions?: {
[collectionSlug: string]: string
}
} }
relationTo: string[] relationTo: string[]
} }
@@ -541,6 +556,10 @@ export type ArrayField = FieldBase & {
} & Admin['components'] } & Admin['components']
initCollapsed?: boolean | false initCollapsed?: boolean | false
} }
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
fields: Field[] fields: Field[]
/** Customize generated GraphQL and Typescript schema names. /** Customize generated GraphQL and Typescript schema names.
* By default it is bound to the collection. * By default it is bound to the collection.
@@ -563,6 +582,14 @@ export type RadioField = FieldBase & {
} }
layout?: 'horizontal' | 'vertical' layout?: 'horizontal' | 'vertical'
} }
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
/**
* Customize the DB enum name
*/
enumName?: DBIdentifierName
options: Option[] options: Option[]
type: 'radio' type: 'radio'
} }

View File

@@ -10,6 +10,7 @@ import {
const globalSchema = joi const globalSchema = joi
.object() .object()
.keys({ .keys({
slug: joi.string().required(),
access: joi.object({ access: joi.object({
read: joi.func(), read: joi.func(),
readVersions: joi.func(), readVersions: joi.func(),
@@ -48,6 +49,7 @@ const globalSchema = joi
preview: joi.func(), preview: joi.func(),
}), }),
custom: joi.object().pattern(joi.string(), joi.any()), custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
endpoints: endpointsSchema, endpoints: endpointsSchema,
fields: joi.array(), fields: joi.array(),
graphQL: joi.alternatives().try( graphQL: joi.alternatives().try(
@@ -64,7 +66,6 @@ const globalSchema = joi
beforeValidate: joi.array().items(joi.func()), beforeValidate: joi.array().items(joi.func()),
}), }),
label: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])), label: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
slug: joi.string().required(),
typescript: joi.object().keys({ typescript: joi.object().keys({
interface: joi.string(), interface: joi.string(),
}), }),

View File

@@ -3,7 +3,7 @@ import type { DeepRequired } from 'ts-essentials'
import type { import type {
CustomPreviewButtonProps, CustomPreviewButtonProps,
CustomPublishButtonProps, CustomPublishButtonType,
CustomSaveButtonProps, CustomSaveButtonProps,
CustomSaveDraftButtonProps, CustomSaveDraftButtonProps,
} from '../../admin/components/elements/types' } from '../../admin/components/elements/types'
@@ -17,8 +17,8 @@ import type {
GeneratePreviewURL, GeneratePreviewURL,
LivePreviewConfig, LivePreviewConfig,
} from '../../config/types' } from '../../config/types'
import type { PayloadRequest } from '../../express/types' import type { DBIdentifierName } from '../../database/types'
import type { RequestContext } from '../../express/types' import type { PayloadRequest, RequestContext } from '../../express/types'
import type { Field } from '../../fields/config/types' import type { Field } from '../../fields/config/types'
import type { Where } from '../../types' import type { Where } from '../../types'
import type { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types' import type { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types'
@@ -86,7 +86,7 @@ export type GlobalAdminOptions = {
* Replaces the "Publish" button * Replaces the "Publish" button
* + drafts must be enabled * + drafts must be enabled
*/ */
PublishButton?: CustomPublishButtonProps PublishButton?: CustomPublishButtonType
/** /**
* Replaces the "Save" button * Replaces the "Save" button
* + drafts must be disabled * + drafts must be disabled
@@ -170,6 +170,10 @@ export type GlobalConfig = {
admin?: GlobalAdminOptions admin?: GlobalAdminOptions
/** Extension point to add your custom data. */ /** Extension point to add your custom data. */
custom?: Record<string, any> custom?: Record<string, any>
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
endpoints?: Omit<Endpoint, 'root'>[] | false endpoints?: Omit<Endpoint, 'root'>[] | false
fields: Field[] fields: Field[]
graphQL?: graphQL?:

View File

@@ -1,6 +1,11 @@
import type { SanitizedCollectionConfig } from '../collections/config/types' import type { SanitizedCollectionConfig } from '../collections/config/types'
import type { SanitizedGlobalConfig } from '../globals/config/types' import type { SanitizedGlobalConfig } from '../globals/config/types'
/**
* This function is not being used and will no longer be exported in the future
* @deprecated
* @param entity
*/
export const getVersionsModelName = ( export const getVersionsModelName = (
entity: SanitizedCollectionConfig | SanitizedGlobalConfig, entity: SanitizedCollectionConfig | SanitizedGlobalConfig,
): string => `_${entity.slug}_versions` ): string => `_${entity.slug}_versions`

View File

@@ -1,10 +1,14 @@
import type { AfterReadHook } from 'payload/dist/collections/config/types' import type { AfterReadHook } from 'payload/dist/collections/config/types'
import { adminsOrPublished } from '../access/adminsOrPublished'
import type { Page, Post } from '../payload-types' import type { Page, Post } from '../payload-types'
export const populateArchiveBlock: AfterReadHook = async ({ doc, context, req: { payload } }) => { export const populateArchiveBlock: AfterReadHook = async ({ doc, context, req }) => {
// pre-populate the archive block if `populateBy` is `collection` // pre-populate the archive block if `populateBy` is `collection`
// then hydrate it on your front-end // then hydrate it on your front-end
const payload = req.payload
const adminOrPublishedResult = await adminsOrPublished({ req: req })
const adminOrPublishedQuery = adminOrPublishedResult
const layoutWithArchive = await Promise.all( const layoutWithArchive = await Promise.all(
doc.layout.map(async block => { doc.layout.map(async block => {
@@ -36,6 +40,7 @@ export const populateArchiveBlock: AfterReadHook = async ({ doc, context, req: {
}, },
} }
: {}), : {}),
...(typeof adminOrPublishedQuery === 'boolean' ? {} : adminOrPublishedQuery),
}, },
sort: '-publishedAt', sort: '-publishedAt',
}) })

19
test/_community/blockA.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { Block } from 'payload/dist/fields/config/types'
import { BlockB } from './blockB'
export const BlockA: Block = {
slug: 'block-a',
fields: [
{
name: 'nestedBlocks',
type: 'blocks',
blocks: [BlockB],
},
{
name: 'title',
type: 'text',
},
],
interfaceName: 'BlockA',
}

15
test/_community/blockB.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { Block } from 'payload/dist/fields/config/types'
import { BlockC } from './blockC'
export const BlockB: Block = {
slug: 'block-b',
fields: [
{
name: 'nestedBlocks',
type: 'blocks',
blocks: [BlockC],
},
],
interfaceName: 'BlockB',
}

15
test/_community/blockC.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { Block } from 'payload/dist/fields/config/types'
const Recurse = require('./blockA')
export const BlockC: Block = {
slug: 'block-C',
fields: [
{
name: 'nestedBlocks',
type: 'blocks',
blocks: [Recurse],
},
],
interfaceName: 'BlockC',
}

View File

@@ -1,24 +1,30 @@
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
import { BlockA } from '../../blockA'
import { mediaSlug } from '../Media' import { mediaSlug } from '../Media'
export const postsSlug = 'posts' export const postsSlug = 'posts'
export const PostsCollection: CollectionConfig = { export const PostsCollection: CollectionConfig = {
slug: postsSlug,
fields: [ fields: [
{ {
name: 'text', name: 'text',
type: 'text', type: 'text',
}, },
{
name: 'blocks',
type: 'blocks',
blocks: [BlockA],
},
{ {
name: 'associatedMedia', name: 'associatedMedia',
type: 'upload',
access: { access: {
create: () => true, create: () => true,
update: () => false, update: () => false,
}, },
relationTo: mediaSlug, relationTo: mediaSlug,
type: 'upload',
}, },
], ],
slug: postsSlug,
} }

View File

@@ -63,7 +63,98 @@ export default buildConfigWithDefaults({
singular: 'Relation B', singular: 'Relation B',
}, },
}, },
{
slug: 'custom-schema',
dbName: 'customs',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
type: 'text',
localized: true,
},
{
name: 'relationship',
type: 'relationship',
hasMany: true,
relationTo: 'relation-a',
},
{
name: 'select',
type: 'select',
dbName: ({ tableName }) => `${tableName}_customSelect`,
enumName: 'selectEnum',
hasMany: true,
options: ['a', 'b', 'c'],
},
{
name: 'radio',
type: 'select',
enumName: 'radioEnum',
options: ['a', 'b', 'c'],
},
{
name: 'array',
type: 'array',
dbName: 'customArrays',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
type: 'text',
localized: true,
},
],
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'block',
dbName: 'customBlocks',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'localizedText',
type: 'text',
localized: true,
},
],
},
],
},
],
versions: true,
},
], ],
globals: [
{
slug: 'global',
// @ts-expect-error
dbName: 'customGlobal',
fields: [
{
name: 'text',
type: 'text',
},
],
versions: true,
},
],
localization: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
onInit: async (payload) => { onInit: async (payload) => {
await payload.create({ await payload.create({
collection: 'users', collection: 'users',

View File

@@ -3,6 +3,7 @@ import fs from 'fs'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import path from 'path' import path from 'path'
import type { PostgresAdapter } from '../../packages/db-postgres/src/types'
import type { PostgresAdapter } from '../../packages/db-postgres/src/types' import type { PostgresAdapter } from '../../packages/db-postgres/src/types'
import type { TypeWithID } from '../../packages/payload/src/collections/config/types' import type { TypeWithID } from '../../packages/payload/src/collections/config/types'
import type { PayloadRequest } from '../../packages/payload/src/express/types' import type { PayloadRequest } from '../../packages/payload/src/express/types'
@@ -14,6 +15,7 @@ import { initTransaction } from '../../packages/payload/src/utilities/initTransa
import { devUser } from '../credentials' import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers' import { initPayloadTest } from '../helpers/configHelpers'
import removeFiles from '../helpers/removeFiles' import removeFiles from '../helpers/removeFiles'
import { MongooseAdapter } from '../../packages/db-mongodb/src'
describe('database', () => { describe('database', () => {
let serverURL let serverURL
@@ -140,6 +142,59 @@ describe('database', () => {
}) })
}) })
describe('schema', () => {
it('should use custom dbNames', () => {
expect(payload.db).toBeDefined()
if (payload.db.name === 'mongoose') {
// @ts-expect-error
const db: MongooseAdapter = payload.db
expect(db.collections['custom-schema'].modelName).toStrictEqual('customs')
expect(db.versions['custom-schema'].modelName).toStrictEqual('_customs_versions')
expect(db.versions.global.modelName).toStrictEqual('_customGlobal_versions')
} else {
// @ts-expect-error
const db: PostgresAdapter = payload.db
// collection
expect(db.tables.customs).toBeDefined()
// collection versions
expect(db.tables._customs_v).toBeDefined()
// collection relationships
expect(db.tables.customs_rels).toBeDefined()
// collection localized
expect(db.tables.customs_locales).toBeDefined()
// global
expect(db.tables.customGlobal).toBeDefined()
expect(db.tables._customGlobal_v).toBeDefined()
// select
expect(db.tables.customs_customSelect).toBeDefined()
// array
expect(db.tables.customArrays).toBeDefined()
// array localized
expect(db.tables.customArrays_locales).toBeDefined()
// blocks
expect(db.tables.customBlocks).toBeDefined()
// localized blocks
expect(db.tables.customBlocks_locales).toBeDefined()
// enum names
expect(db.enums.selectEnum).toBeDefined()
expect(db.enums.radioEnum).toBeDefined()
}
})
})
describe('transactions', () => { describe('transactions', () => {
describe('local api', () => { describe('local api', () => {
it('should commit multiple operations in isolation', async () => { it('should commit multiple operations in isolation', async () => {

View File

@@ -37,6 +37,7 @@ const collectionWithName = (collectionSlug: string): CollectionConfig => {
} }
export const slug = 'posts' export const slug = 'posts'
export const slugWithLocalizedRel = 'postsLocalized'
export const relationSlug = 'relation' export const relationSlug = 'relation'
export const defaultAccessRelSlug = 'strict-access' export const defaultAccessRelSlug = 'strict-access'
export const chainedRelSlug = 'chained' export const chainedRelSlug = 'chained'
@@ -46,6 +47,10 @@ export const polymorphicRelationshipsSlug = 'polymorphic-relationships'
export const treeSlug = 'tree' export const treeSlug = 'tree'
export default buildConfigWithDefaults({ export default buildConfigWithDefaults({
localization: {
locales: ['en', 'de'],
defaultLocale: 'en',
},
collections: [ collections: [
{ {
slug, slug,
@@ -108,6 +113,23 @@ export default buildConfigWithDefaults({
}, },
], ],
}, },
{
slug: slugWithLocalizedRel,
access: openAccess,
fields: [
{
name: 'title',
type: 'text',
},
// Relationship
{
name: 'relationField',
type: 'relationship',
relationTo: relationSlug,
localized: true,
},
],
},
collectionWithName(relationSlug), collectionWithName(relationSlug),
{ {
...collectionWithName(defaultAccessRelSlug), ...collectionWithName(defaultAccessRelSlug),
@@ -276,6 +298,13 @@ export default buildConfigWithDefaults({
}, },
}) })
const rel2 = await payload.create({
collection: relationSlug,
data: {
name: 'another name',
},
})
const filteredRelation = await payload.create({ const filteredRelation = await payload.create({
collection: relationSlug, collection: relationSlug,
data: { data: {

View File

@@ -9,6 +9,7 @@ import type {
Post, Post,
Relation, Relation,
} from './payload-types' } from './payload-types'
import type { PostsLocalized } from './payload-types'
import payload from '../../packages/payload/src' import payload from '../../packages/payload/src'
import { devUser } from '../credentials' import { devUser } from '../credentials'
@@ -21,6 +22,7 @@ import config, {
defaultAccessRelSlug, defaultAccessRelSlug,
relationSlug, relationSlug,
slug, slug,
slugWithLocalizedRel,
treeSlug, treeSlug,
} from './config' } from './config'
@@ -409,6 +411,85 @@ describe('Relationships', () => {
expect(doc?.relationField).toMatchObject({ id: relation.id, name: relation.name }) expect(doc?.relationField).toMatchObject({ id: relation.id, name: relation.name })
}) })
}) })
describe('with localization', () => {
let relation1: Relation
let relation2: Relation
let localizedPost1: PostsLocalized
let localizedPost2: PostsLocalized
beforeAll(async () => {
relation1 = await payload.create<Relation>({
collection: relationSlug,
data: {
name: 'english',
},
})
relation2 = await payload.create<Relation>({
collection: relationSlug,
data: {
name: 'german',
},
})
localizedPost1 = await payload.create<'postsLocalized'>({
collection: slugWithLocalizedRel,
data: {
title: 'english',
relationField: relation1.id,
},
locale: 'en',
})
await payload.update({
id: localizedPost1.id,
collection: slugWithLocalizedRel,
locale: 'de',
data: {
relationField: relation2.id,
},
})
localizedPost2 = await payload.create({
collection: slugWithLocalizedRel,
data: {
title: 'german',
relationField: relation2.id,
},
locale: 'de',
})
})
it('should find two docs for german locale', async () => {
const { docs } = await payload.find<PostsLocalized>({
collection: slugWithLocalizedRel,
locale: 'de',
where: {
relationField: {
equals: relation2.id,
},
},
})
const mappedIds = docs.map((doc) => doc?.id)
expect(mappedIds).toContain(localizedPost1.id)
expect(mappedIds).toContain(localizedPost2.id)
})
it("shouldn't find a relationship query outside of the specified locale", async () => {
const { docs } = await payload.find<PostsLocalized>({
collection: slugWithLocalizedRel,
locale: 'en',
where: {
relationField: {
equals: relation2.id,
},
},
})
expect(docs.map((doc) => doc?.id)).not.toContain(localizedPost2.id)
})
})
}) })
describe('Nested Querying', () => { describe('Nested Querying', () => {

View File

@@ -9,6 +9,7 @@ export interface Config {
collections: { collections: {
posts: Post posts: Post
relation: Relation relation: Relation
postsLocalized: PostsLocalized
'strict-access': StrictAccess 'strict-access': StrictAccess
'chained-relation': ChainedRelation 'chained-relation': ChainedRelation
'custom-id-relation': CustomIdRelation 'custom-id-relation': CustomIdRelation
@@ -35,6 +36,13 @@ export interface Post {
updatedAt: string updatedAt: string
createdAt: string createdAt: string
} }
export interface PostsLocalized {
id: string
title?: string | null
relationField?: (string | null) | Relation
updatedAt: string
createdAt: string
}
export interface Relation { export interface Relation {
id: string id: string
name?: string name?: string