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)

View File

@@ -168,7 +168,7 @@ import * as React from 'react'
import {
CustomSaveButtonProps,
CustomSaveDraftButtonProps,
CustomPublishButtonProps,
CustomPublishButtonType,
CustomPreviewButtonProps,
} from 'payload/types'
@@ -185,7 +185,7 @@ export const CustomSaveDraftButton: CustomSaveDraftButtonProps = ({
return <DefaultButton label={label} disabled={disabled} saveDraft={saveDraft} />
}
export const CustomPublishButton: CustomPublishButtonProps = ({
export const CustomPublishButton: CustomPublishButtonType = ({
DefaultButton,
disabled,
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. |
| **`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) |
| **`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._
@@ -59,7 +60,8 @@ export const Orders: CollectionConfig = {
#### More collection config examples
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.
### 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
---
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
| Option | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`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. |
| **`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. |
| **`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) |
| **`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._
@@ -59,26 +64,30 @@ export default Nav
#### 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
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 |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
| `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). |
| `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. |
| Option | Description |
|---------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `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. |
| `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). |
| `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. |
### 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:**
@@ -113,15 +122,20 @@ export const MyGlobal: GlobalConfig = {
### 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
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
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

View File

@@ -37,12 +37,18 @@ export default buildConfig({
### Options
| Option | Description |
|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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. |
| `migrationDir` | Customize the directory that migrations are stored. |
| Option | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `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. |
| `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'. |
| `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

View File

@@ -12,22 +12,24 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
</Banner>
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/array.png"
srcDark="https://payloadcms.com/images/docs/fields/array-dark.png"
alt="Array field with two Rows in Payload admin panel"
caption="Admin panel screenshot of an Array field with two Rows"
srcLight="https://payloadcms.com/images/docs/fields/array.png"
srcDark="https://payloadcms.com/images/docs/fields/array-dark.png"
alt="Array field with two Rows in Payload admin panel"
caption="Admin panel screenshot of an Array field with two Rows"
/>
**Example uses:**
- 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)
- 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)
- 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)
- 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
| Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as the heading in the Admin panel 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. |
@@ -45,15 +47,17 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`dbName`** | Custom table name for the field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
_\* An asterisk denotes that a property is required._
### 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 |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|---------------------------|----------------------------------------------------------------------------------------------------------------------|
| **`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 |

View File

@@ -14,22 +14,23 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
</Banner>
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/blocks.png"
srcDark="https://payloadcms.com/images/docs/fields/blocks-dark.png"
alt="Admin panel screenshot of add Blocks drawer view"
caption="Admin panel screenshot of add Blocks drawer view"
srcLight="https://payloadcms.com/images/docs/fields/blocks.png"
srcDark="https://payloadcms.com/images/docs/fields/blocks-dark.png"
alt="Admin panel screenshot of add Blocks drawer view"
caption="Admin panel screenshot of add Blocks drawer view"
/>
**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`.
- Virtual event agenda "timeslots" where a timeslot could either be a `Break`, a `Presentation`, or a `BreakoutSession`.
### Field config
| Option | Description |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as the heading in the Admin panel 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. |
@@ -51,10 +52,11 @@ _\* An asterisk denotes that a property is required._
### 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 |
| ------------------- | ------------------------------- |
|---------------------|---------------------------------|
| **`initCollapsed`** | Set the initial collapsed state |
### Block configs
@@ -72,7 +74,7 @@ Blocks are defined as separate configs of their own.
</Banner>
| Option | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`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. |
| **`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. |
| **`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`. |
| **`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
@@ -92,7 +95,8 @@ The `blockType` is saved as the slug of the block that has been selected.
**`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
@@ -139,7 +143,8 @@ export const ExampleCollection: CollectionConfig = {
### 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
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. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`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._

View File

@@ -12,32 +12,34 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
</Banner>
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/select.png"
srcDark="https://payloadcms.com/images/docs/fields/select-dark.png"
alt="Shows a Select field in the Payload admin panel"
caption="Admin panel screenshot of a Select field"
srcLight="https://payloadcms.com/images/docs/fields/select.png"
srcDark="https://payloadcms.com/images/docs/fields/select-dark.png"
alt="Shows a Select field in the Payload admin panel"
caption="Admin panel screenshot of a Select field"
/>
### Config
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/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) |
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/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) |
| **`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. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
_\* An asterisk denotes that a property is required._
@@ -52,7 +54,8 @@ _\* An asterisk denotes that a property is required._
### 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`**
@@ -60,7 +63,8 @@ Set to `true` if you'd like this field to be clearable within the Admin UI.
**`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
@@ -101,7 +105,8 @@ export const ExampleCollection: CollectionConfig = {
### 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
export const CustomSelectField: Field = {
@@ -156,27 +161,33 @@ export const CustomSelectComponent: React.FC<CustomSelectProps> = ({ path, optio
return (
<div>
<label className="field-label">
Custom Select
</label>
<SelectInput
path={path}
name={path}
options={adjustedOptions}
value={value}
onChange={(e) => setValue(e.value)}
/>
</div>
)
<label className = "field-label" >
Custom
Select
< /label>
< SelectInput
path = { path }
name = { path }
options = { adjustedOptions }
value = { value }
onChange = {(e)
=>
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
id='Efn9OxSjA6Y'
label='How to Create a Custom Select Field'
drawerTitle='How to Create a Custom Select Field: A Step-by-Step Guide'
id='Efn9OxSjA6Y'
label='How to Create a Custom Select Field'
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",
"version": "1.4.3",
"version": "1.4.4",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import { withSession } from './withSession'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
{ data, req = {} as PayloadRequest, slug },
{ slug, data, req = {} as PayloadRequest },
) {
const Model = this.globals
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 toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export const create: Create = async function create(
@@ -19,8 +18,11 @@ export const create: Create = async function create(
db,
fields: collection.fields,
operation: 'create',
tableName: toSnakeCase(collectionSlug),
req,
tableName: getTableName({
adapter: this,
config: collection,
}),
})
return result

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,13 @@ import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { buildFindManyArgs } from './find/buildFindManyArgs'
import buildQuery from './queries/buildQuery'
import { selectDistinct } from './queries/selectDistinct'
import { getTableName } from './schema/getTableName'
import { transform } from './transform/read'
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 collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
let docToDelete: Record<string, unknown>
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 { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
@@ -16,7 +16,11 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
const db = this.sessions[req.transactionID]?.db || this.drizzle
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 { docs } = await findMany({

View File

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

View File

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

View File

@@ -2,11 +2,12 @@
import type { Field } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types'
import type { Result } from './buildFindManyArgs'
import { getTableName } from '../schema/getTableName'
type TraverseFieldArgs = {
_locales: Record<string, unknown>
adapter: PostgresAdapter
@@ -78,9 +79,22 @@ export const traverseFields = ({
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
traverseFields({
@@ -128,9 +142,15 @@ export const traverseFields = ({
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
traverseFields({

View File

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

View File

@@ -2,11 +2,11 @@ import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
@@ -27,7 +27,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
)
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)
return findMany({

View File

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

View File

@@ -2,11 +2,11 @@ import type { FindVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -25,7 +25,11 @@ export const findVersions: FindVersions = async function findVersions(
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
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)
return findMany({

View File

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

View File

@@ -4,11 +4,11 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build'
import { getTableName } from './schema/getTableName'
export const init: Init = async function init(this: PostgresAdapter) {
if (this.schemaName) {
@@ -25,7 +25,10 @@ export const init: Init = async function init(this: PostgresAdapter) {
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = toSnakeCase(collection.slug)
const tableName = getTableName({
adapter: this,
config: collection,
})
buildTable({
adapter: this,
@@ -37,10 +40,15 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: collection.fields,
tableName,
timestamps: collection.timestamps,
versions: false,
})
if (collection.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const versionFields = buildVersionCollectionFields(collection)
buildTable({
@@ -53,12 +61,13 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})
this.payload.config.globals.forEach((global) => {
const tableName = toSnakeCase(global.slug)
const tableName = getTableName({ adapter: this, config: global })
buildTable({
adapter: this,
@@ -70,10 +79,11 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: global.fields,
tableName,
timestamps: false,
versions: false,
})
if (global.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({ adapter: this, config: global, versions: true })
const versionFields = buildVersionGlobalFields(global)
buildTable({
@@ -86,6 +96,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
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 { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'
import { getTableName } from '../schema/getTableName'
type Constraint = {
columnName: string
table: GenericTable | PgTableWithColumns<any>
@@ -95,7 +97,7 @@ export const getTableColumnFromPath = ({
field: {
name: 'id',
type: adapter.idType === 'uuid' ? 'text' : 'number',
} as TextField | NumberField,
} as NumberField | TextField,
table: adapter.tables[newTableName],
}
}
@@ -183,7 +185,13 @@ export const getTableColumnFromPath = ({
case 'group': {
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(
adapter.tables[tableName].id,
@@ -218,7 +226,12 @@ export const getTableColumnFromPath = ({
}
case 'array': {
newTableName = `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -265,7 +278,12 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((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(
adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID,
@@ -285,7 +303,12 @@ export const getTableColumnFromPath = ({
}
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}.%.`
let result
const blockConstraints = []
@@ -351,7 +374,7 @@ export const getTableColumnFromPath = ({
case 'relationship':
case 'upload': {
let relationshipFields
const relationTableName = `${rootTableName}_rels`
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
const newCollectionPath = pathSegments.slice(1).join('.')
const aliasRelationshipTableName = uuid()
const aliasRelationshipTable = alias(
@@ -359,23 +382,45 @@ export const getTableColumnFromPath = ({
aliasRelationshipTableName,
)
// 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,
})
if (locale && field.localized && adapter.payload.config.localization) {
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
eq(aliasRelationshipTable.locale, locale),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
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
let newAliasTable
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
relationshipFields = adapter.payload.collections[field.relationTo].config.fields
relationshipFields = relationshipConfig.fields
newAliasTable = alias(adapter.tables[newTableName], toSnakeCase(uuid()))
@@ -394,7 +439,11 @@ export const getTableColumnFromPath = ({
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map(
(relationTo) => `"${aliasRelationshipTableName}"."${toSnakeCase(relationTo)}_id"`,
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
)
return {
constraints,
@@ -441,7 +490,7 @@ export const getTableColumnFromPath = ({
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// 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]

View File

@@ -66,7 +66,7 @@ export const sanitizeQueryValue = ({
formattedValue = Number(val)
}
if (field.type === 'date') {
if (field.type === 'date' && operator !== 'exists') {
if (typeof val === 'string') {
formattedValue = new Date(val)
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 { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import { findMany } from './find/findMany'
import { getTableName } from './schema/getTableName'
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
@@ -17,7 +17,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts({
where,
}) {
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 combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -11,10 +11,10 @@ import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types'
import { getTableName } from './getTableName'
import { parentIDColumnMap } from './parentIDColumnMap'
import { setColumnID } from './setColumnID'
import { traverseFields } from './traverseFields'
@@ -35,6 +35,7 @@ type Args = {
rootTableName?: string
tableName: string
timestamps?: boolean
versions: boolean
}
type Result = {
@@ -59,6 +60,7 @@ export const buildTable = ({
rootTableName: incomingRootTableName,
tableName,
timestamps,
versions,
}: Args): Result => {
const rootTableName = incomingRootTableName || tableName
const columns: Record<string, PgColumnBuilder> = baseColumns
@@ -113,6 +115,7 @@ export const buildTable = ({
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
}))
if (timestamps) {
@@ -147,7 +150,7 @@ export const buildTable = ({
adapter.tables[tableName] = table
if (hasLocalizedField) {
const localeTableName = `${tableName}_locales`
const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
@@ -288,11 +291,16 @@ export const buildTable = ({
}
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'
const relatedCollectionCustomID = adapter.payload.collections[
relationTo
].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id')
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
@@ -301,7 +309,7 @@ export const buildTable = ({
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
const relationshipsTableName = `${tableName}_rels`
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
@@ -333,7 +341,11 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = toSnakeCase(relationTo)
const relatedTableName = getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,
})
const idColumnName = `${relationTo}ID`
result[idColumnName] = one(adapter.tables[relatedTableName], {
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 { buildTable } from './build'
import { createIndex } from './createIndex'
import { getTableName } from './getTableName'
import { idToUUID } from './idToUUID'
import { parentIDColumnMap } from './parentIDColumnMap'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical'
@@ -53,6 +54,7 @@ type Args = {
rootRelationsToBuild?: Map<string, string>
rootTableIDColType: string
rootTableName: string
versions: boolean
}
type Result = {
@@ -86,7 +88,9 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
}: Args): Result => {
const throwValidationError = true
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
@@ -217,7 +221,15 @@ export const traverseFields = ({
case 'radio':
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(
enumName,
@@ -231,7 +243,14 @@ export const traverseFields = ({
)
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> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
@@ -266,6 +285,7 @@ export const traverseFields = ({
disableUnique,
fields: [],
tableName: selectTableName,
versions,
})
relationsToBuild.set(fieldName, selectTableName)
@@ -296,7 +316,13 @@ export const traverseFields = ({
case 'array': {
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> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
@@ -334,6 +360,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
versions,
})
if (subHasManyTextField) {
@@ -356,7 +383,7 @@ export const traverseFields = ({
}
if (hasLocalesTable(field.fields)) {
result._locales = many(adapter.tables[`${arrayTableName}_locales`])
result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`])
}
subRelationsToBuild.forEach((val, key) => {
@@ -375,7 +402,13 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
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]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
@@ -416,6 +449,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: blockTableName,
versions,
})
if (subHasManyTextField) {
@@ -439,7 +473,9 @@ export const traverseFields = ({
}
if (hasLocalesTable(block.fields)) {
result._locales = many(adapter.tables[`${blockTableName}_locales`])
result._locales = many(
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
)
}
subRelationsToBuild.forEach((val, key) => {
@@ -451,7 +487,7 @@ export const traverseFields = ({
)
adapter.relations[`relations_${blockTableName}`] = blockTableRelations
} else if (process.env.NODE_ENV !== 'production') {
} else if (process.env.NODE_ENV !== 'production' && !versions) {
validateExistingBlockIsIdentical({
block,
localized: field.localized,
@@ -459,7 +495,7 @@ export const traverseFields = ({
table: adapter.tables[blockTableName],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
})
@@ -498,6 +534,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -540,6 +577,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -583,6 +621,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (tabHasLocalizedField) hasLocalizedField = true
@@ -626,6 +665,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (rowHasLocalizedField) hasLocalizedField = true

View File

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

View File

@@ -7,6 +7,7 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../../types'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
import { getTableName } from '../../schema/getTableName'
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
import { transformArray } from './array'
import { transformBlocks } from './blocks'
@@ -45,7 +46,6 @@ type Args = {
locales: {
[locale: string]: Record<string, unknown>
}
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[]
/**
* This is the name of the parent table
@@ -58,6 +58,7 @@ type Args = {
selects: {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
}
export const traverseFields = ({
@@ -73,7 +74,6 @@ export const traverseFields = ({
fields,
forcedLocale,
locales,
texts,
numbers,
parentTableName,
path,
@@ -81,6 +81,7 @@ export const traverseFields = ({
relationshipsToDelete,
row,
selects,
texts,
}: Args) => {
fields.forEach((field) => {
let columnName = ''
@@ -88,7 +89,12 @@ export const traverseFields = ({
let fieldData: unknown
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}`
fieldData = data[field.name]
}
@@ -111,12 +117,12 @@ export const traverseFields = ({
data: localeData,
field,
locale: localeKey,
texts,
numbers,
path,
relationships,
relationshipsToDelete,
selects,
texts,
})
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
@@ -132,12 +138,12 @@ export const traverseFields = ({
blocksToDelete,
data: data[field.name],
field,
texts,
numbers,
path,
relationships,
relationshipsToDelete,
selects,
texts,
})
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
@@ -147,8 +153,8 @@ export const traverseFields = ({
}
if (field.type === 'blocks') {
field.blocks.forEach(({ slug }) => {
blocksToDelete.add(toSnakeCase(slug))
field.blocks.forEach((block) => {
blocksToDelete.add(getTableName({ adapter, config: block }))
})
if (field.localized) {
@@ -163,12 +169,12 @@ export const traverseFields = ({
data: localeData,
field,
locale: localeKey,
texts,
numbers,
path,
relationships,
relationshipsToDelete,
selects,
texts,
})
}
})
@@ -181,12 +187,12 @@ export const traverseFields = ({
blocksToDelete,
data: fieldData,
field,
texts,
numbers,
path,
relationships,
relationshipsToDelete,
selects,
texts,
})
}
@@ -210,7 +216,6 @@ export const traverseFields = ({
fields: field.fields,
forcedLocale: localeKey,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${field.name}.`,
@@ -218,6 +223,7 @@ export const traverseFields = ({
relationshipsToDelete,
row,
selects,
texts,
})
})
} else {
@@ -233,7 +239,6 @@ export const traverseFields = ({
fieldPrefix: `${fieldName}_`,
fields: field.fields,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${field.name}.`,
@@ -241,6 +246,7 @@ export const traverseFields = ({
relationshipsToDelete,
row,
selects,
texts,
})
}
}
@@ -267,7 +273,6 @@ export const traverseFields = ({
fields: tab.fields,
forcedLocale: localeKey,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${tab.name}.`,
@@ -275,6 +280,7 @@ export const traverseFields = ({
relationshipsToDelete,
row,
selects,
texts,
})
})
} else {
@@ -290,7 +296,6 @@ export const traverseFields = ({
fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
fields: tab.fields,
locales,
texts,
numbers,
parentTableName,
path: `${path || ''}${tab.name}.`,
@@ -298,6 +303,7 @@ export const traverseFields = ({
relationshipsToDelete,
row,
selects,
texts,
})
}
}
@@ -314,7 +320,6 @@ export const traverseFields = ({
fieldPrefix,
fields: tab.fields,
locales,
texts,
numbers,
parentTableName,
path,
@@ -322,6 +327,7 @@ export const traverseFields = ({
relationshipsToDelete,
row,
selects,
texts,
})
}
})
@@ -340,7 +346,6 @@ export const traverseFields = ({
fieldPrefix,
fields: field.fields,
locales,
texts,
numbers,
parentTableName,
path,
@@ -348,6 +353,7 @@ export const traverseFields = ({
relationshipsToDelete,
row,
selects,
texts,
})
}
@@ -488,7 +494,11 @@ export const traverseFields = ({
}
if (fieldAffectsData(field)) {
const valuesToTransform: { localeKey?: string; ref: unknown; value: unknown }[] = []
const valuesToTransform: {
localeKey?: string
ref: unknown
value: unknown
}[] = []
if (field.localized) {
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 Args = {
localesSuffix?: string
idType?: 'serial' | 'uuid'
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
push?: boolean
schemaName?: string
relationshipsSuffix?: string
versionsSuffix?: string
}
export type GenericColumn = PgColumn<
@@ -58,6 +61,10 @@ export type DrizzleTransaction = PgTransaction<
>
export type PostgresAdapter = BaseDatabaseAdapter & {
/**
* Used internally to map the block name to the table name
*/
blockTableNames: Record<string, string>
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
/**
@@ -66,12 +73,14 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
*/
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
localesSuffix?: string
logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
@@ -82,14 +91,21 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
}
}
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateUpArgs = {
payload: Payload
req?: Partial<PayloadRequest>
}
export type MigrateDownArgs = {
payload: Payload
req?: Partial<PayloadRequest>
}
declare module 'payload' {
export interface DatabaseAdapter
@@ -98,9 +114,11 @@ declare module 'payload' {
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>>
localeSuffix?: string
pool: Pool
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
sessions: {
[id: string]: {
@@ -110,5 +128,6 @@ declare module 'payload' {
}
}
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 type { ChainedMethods } from './find/chainMethods'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { selectDistinct } from './queries/selectDistinct'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
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 collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id
@@ -49,7 +54,7 @@ export const updateOne: UpdateOne = async function updateOne(
fields: collection.fields,
operation: 'update',
req,
tableName: toSnakeCase(collectionSlug),
tableName,
})
return result

View File

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

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
export async function updateGlobalVersion<T extends TypeWithID>(
@@ -25,7 +25,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global,
)
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 { where } = await buildQuery({
@@ -43,9 +47,9 @@ export async function updateGlobalVersion<T extends TypeWithID>(
db,
fields,
operation: 'update',
req,
tableName,
where,
req,
})
return result

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types'
import buildQuery from './queries/buildQuery'
import { getTableName } from './schema/getTableName'
import { upsertRow } from './upsertRow'
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 collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
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 { where } = await buildQuery({
@@ -41,9 +45,9 @@ export async function updateVersion<T extends TypeWithID>(
db,
fields,
operation: 'update',
req,
tableName,
where,
req,
})
return result

View File

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

View File

@@ -71,7 +71,7 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
}
// 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) {
row.locales = row.locales.map((localeRow, i) => {
if (typeof localeRow._getParentID === 'function') {
@@ -81,7 +81,10 @@ export const insertArrays = async ({ adapter, arrays, db, parentRows }: Args): P
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -85,15 +85,15 @@
html[data-theme='light'] {
.tooltip {
background-color: var(--theme-error-250);
color: var(--theme-error-750);
background-color: var(--theme-elevation-100);
color: var(--theme-elevation-1000);
&--position-top:after {
border-top-color: var(--theme-error-250);
border-top-color: var(--theme-elevation-100);
}
&--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 { CustomPublishButtonProps } from './Publish'
export type { CustomPublishButtonProps, CustomPublishButtonType } from './Publish'
export type { CustomSaveButtonProps } from './Save'
export type { CustomSaveDraftButtonProps } from './SaveDraft'

View File

@@ -1,9 +1,34 @@
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 = {
ar: 'ar',
az: 'az',
bg: 'bg',
cs: 'cs',
de: 'de',
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
nb: 'nb',
nl: 'nl',
pl: 'pl',
pt: 'pt',
ro: 'ro',
ru: 'ru',
sv: 'sv',
th: 'th',
tr: 'tr',
ua: 'uk',
vi: 'vi',
zh: 'zhCN',
zhTw: 'zhTW',
}
return formattedLocales[locale] || locale

View File

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

View File

@@ -6,7 +6,7 @@ import type { DeepRequired } from 'ts-essentials'
import type { GeneratedTypes } from '../../'
import type {
CustomPreviewButtonProps,
CustomPublishButtonProps,
CustomPublishButtonType,
CustomSaveButtonProps,
CustomSaveDraftButtonProps,
} from '../../admin/components/elements/types'
@@ -21,6 +21,7 @@ import type {
GeneratePreviewURL,
LivePreviewConfig,
} from '../../config/types'
import type { DBIdentifierName } from '../../database/types'
import type { PayloadRequest, RequestContext } from '../../express/types'
import type { Field } from '../../fields/config/types'
import type { IncomingUploadType, Upload } from '../../uploads/types'
@@ -105,7 +106,9 @@ export type BeforeReadHook<T extends TypeWithID = any> = (args: {
collection: SanitizedCollectionConfig
context: RequestContext
doc: T
query: { [key: string]: any }
query: {
[key: string]: any
}
req: PayloadRequest
}) => any
@@ -115,7 +118,9 @@ export type AfterReadHook<T extends TypeWithID = any> = (args: {
context: RequestContext
doc: T
findMany?: boolean
query?: { [key: string]: any }
query?: {
[key: string]: any
}
req: PayloadRequest
}) => any
@@ -146,7 +151,10 @@ export type AfterErrorHook = (
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 */
collection: SanitizedCollectionConfig | null,
) => { response: any; status: number } | void
) => {
response: any
status: number
} | void
export type BeforeLoginHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
@@ -228,7 +236,7 @@ export type CollectionAdminOptions = {
* Replaces the "Publish" button
* + drafts must be enabled
*/
PublishButton?: CustomPublishButtonProps
PublishButton?: CustomPublishButtonType
/**
* Replaces the "Save" button
* + drafts must be disabled
@@ -333,8 +341,7 @@ export type CollectionAdminOptions = {
useAsTitle?: string
}
/** Manage all aspects of a data collection */
export type CollectionConfig = {
export type BaseCollectionConfig = {
/**
* Access control
*/
@@ -359,6 +366,11 @@ export type CollectionConfig = {
auth?: IncomingAuthType | boolean
/** Extension point to add your custom data. */
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
*/
@@ -427,6 +439,7 @@ export type CollectionConfig = {
* @default false // disable uploads
*/
upload?: IncomingUploadType | boolean
/**
* Customize the handling of incoming file uploads
*
@@ -435,6 +448,9 @@ export type CollectionConfig = {
versions?: IncomingCollectionVersions | boolean
}
/** Manage all aspects of a data collection */
export type CollectionConfig = BaseCollectionConfig
export interface SanitizedCollectionConfig
extends Omit<
DeepRequired<CollectionConfig>,

View File

@@ -403,3 +403,10 @@ export type PaginatedDocs<T = any> = {
totalDocs: 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,
CreateVersion,
CreateVersionArgs,
DBIdentifierName,
DeleteMany,
DeleteManyArgs,
DeleteOne,

View File

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

View File

@@ -1,5 +1,5 @@
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 {
@@ -18,6 +18,7 @@ type Args = {
config: Config
existingFieldNames?: Set<string>
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.
* This validation will be skipped if validRelationships is null.
@@ -29,6 +30,7 @@ export const sanitizeFields = ({
config,
existingFieldNames = new Set(),
fields,
sanitizedBlocksMap = {},
validRelationships,
}: Args): Field[] => {
if (!fields) return []
@@ -96,10 +98,16 @@ export const sanitizeFields = ({
}
if (field.type === 'blocks' && field.blocks) {
field.blocks = field.blocks.map((block) => ({
...block,
fields: block.fields.concat(baseBlockFields),
}))
field.blocks = field.blocks.map((block) => {
// break recursion
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) {
@@ -113,7 +121,7 @@ export const sanitizeFields = ({
if (fieldAffectsData(field)) {
if (existingFieldNames.has(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)
}
@@ -169,16 +177,29 @@ export const sanitizeFields = ({
if ('blocks' in field && field.blocks) {
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
? formatLabels(unsanitizedBlock.slug)
: unsanitizedBlock.labels
unsanitizedBlock.fields = sanitizeFields({
config,
fields: block.fields,
validRelationships,
existingFieldNames: new Set(),
fields: block.fields,
sanitizedBlocksMap,
validRelationships,
})
return unsanitizedBlock

View File

@@ -200,9 +200,11 @@ export const select = baseField.keys({
isClearable: joi.boolean().default(false),
isSortable: joi.boolean().default(false),
}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi
.alternatives()
.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),
options: joi
.array()
@@ -232,6 +234,7 @@ export const radio = baseField.keys({
layout: joi.string().valid('vertical', 'horizontal'),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
enumName: joi.alternatives().try(joi.string(), joi.func()),
options: joi
.array()
.min(1)
@@ -309,6 +312,7 @@ export const array = baseField.keys({
.default({}),
})
.default({}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
fields: joi.array().items(joi.link('#field')).required(),
interfaceName: joi.string(),
@@ -409,6 +413,7 @@ export const blocks = baseField.keys({
joi.object({
slug: joi.string().required(),
custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
fields: joi.array().items(joi.link('#field')),
graphQL: joi.object().keys({
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 { SanitizedCollectionConfig, TypeWithID } from '../../collections/config/types'
import type { SanitizedConfig } from '../../config/types'
import type { DBIdentifierName } from '../../database/types'
import type { PayloadRequest, RequestContext } from '../../express/types'
import type { SanitizedGlobalConfig } from '../../globals/config/types'
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> = (
data: Partial<T>,
siblingData: Partial<P>,
{ user }: { user: User },
{
user,
}: {
user: User
},
) => boolean
export type FilterOptionsProps<T = any> = {
@@ -443,6 +448,14 @@ export type SelectField = FieldBase & {
isClearable?: boolean
isSortable?: boolean
}
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
/**
* Customize the DB enum name
*/
enumName?: DBIdentifierName
hasMany?: boolean
options: Option[]
type: 'select'
@@ -492,7 +505,9 @@ type RelationshipAdmin = Admin & {
}
export type PolymorphicRelationshipField = SharedRelationshipProperties & {
admin?: RelationshipAdmin & {
sortOptions?: { [collectionSlug: string]: string }
sortOptions?: {
[collectionSlug: string]: string
}
}
relationTo: string[]
}
@@ -541,6 +556,10 @@ export type ArrayField = FieldBase & {
} & Admin['components']
initCollapsed?: boolean | false
}
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
fields: Field[]
/** Customize generated GraphQL and Typescript schema names.
* By default it is bound to the collection.
@@ -563,6 +582,14 @@ export type RadioField = FieldBase & {
}
layout?: 'horizontal' | 'vertical'
}
/**
* Customize the SQL table name
*/
dbName?: DBIdentifierName
/**
* Customize the DB enum name
*/
enumName?: DBIdentifierName
options: Option[]
type: 'radio'
}

View File

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

View File

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

View File

@@ -1,6 +1,11 @@
import type { SanitizedCollectionConfig } from '../collections/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 = (
entity: SanitizedCollectionConfig | SanitizedGlobalConfig,
): string => `_${entity.slug}_versions`

View File

@@ -1,10 +1,14 @@
import type { AfterReadHook } from 'payload/dist/collections/config/types'
import { adminsOrPublished } from '../access/adminsOrPublished'
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`
// 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(
doc.layout.map(async block => {
@@ -36,6 +40,7 @@ export const populateArchiveBlock: AfterReadHook = async ({ doc, context, req: {
},
}
: {}),
...(typeof adminOrPublishedQuery === 'boolean' ? {} : adminOrPublishedQuery),
},
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 { BlockA } from '../../blockA'
import { mediaSlug } from '../Media'
export const postsSlug = 'posts'
export const PostsCollection: CollectionConfig = {
slug: postsSlug,
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'blocks',
type: 'blocks',
blocks: [BlockA],
},
{
name: 'associatedMedia',
type: 'upload',
access: {
create: () => true,
update: () => false,
},
relationTo: mediaSlug,
type: 'upload',
},
],
slug: postsSlug,
}

View File

@@ -63,7 +63,98 @@ export default buildConfigWithDefaults({
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) => {
await payload.create({
collection: 'users',

View File

@@ -3,6 +3,7 @@ import fs from 'fs'
import { GraphQLClient } from 'graphql-request'
import path from 'path'
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 { PayloadRequest } from '../../packages/payload/src/express/types'
@@ -14,6 +15,7 @@ import { initTransaction } from '../../packages/payload/src/utilities/initTransa
import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers'
import removeFiles from '../helpers/removeFiles'
import { MongooseAdapter } from '../../packages/db-mongodb/src'
describe('database', () => {
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('local api', () => {
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 slugWithLocalizedRel = 'postsLocalized'
export const relationSlug = 'relation'
export const defaultAccessRelSlug = 'strict-access'
export const chainedRelSlug = 'chained'
@@ -46,6 +47,10 @@ export const polymorphicRelationshipsSlug = 'polymorphic-relationships'
export const treeSlug = 'tree'
export default buildConfigWithDefaults({
localization: {
locales: ['en', 'de'],
defaultLocale: 'en',
},
collections: [
{
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(defaultAccessRelSlug),
@@ -276,6 +298,13 @@ export default buildConfigWithDefaults({
},
})
const rel2 = await payload.create({
collection: relationSlug,
data: {
name: 'another name',
},
})
const filteredRelation = await payload.create({
collection: relationSlug,
data: {

View File

@@ -9,6 +9,7 @@ import type {
Post,
Relation,
} from './payload-types'
import type { PostsLocalized } from './payload-types'
import payload from '../../packages/payload/src'
import { devUser } from '../credentials'
@@ -21,6 +22,7 @@ import config, {
defaultAccessRelSlug,
relationSlug,
slug,
slugWithLocalizedRel,
treeSlug,
} from './config'
@@ -409,6 +411,85 @@ describe('Relationships', () => {
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', () => {

View File

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