Compare commits
75 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8b752ef2 | ||
|
|
3e5c31a024 | ||
|
|
631431e006 | ||
|
|
492d920133 | ||
|
|
f754edc375 | ||
|
|
d2571e10d6 | ||
|
|
a687cb9c5b | ||
|
|
cf6634111f | ||
|
|
1ee19d3016 | ||
|
|
9beaa281dc | ||
|
|
5174c7092f | ||
|
|
d894ac75f0 | ||
|
|
af0105ced5 | ||
|
|
93e81314df | ||
|
|
163d1c85da | ||
|
|
cb9b80aaf9 | ||
|
|
cad1906725 | ||
|
|
988c8848e9 | ||
|
|
95a8bb0d27 | ||
|
|
9c2ccbf61a | ||
|
|
3ee0e842a5 | ||
|
|
6ec982022e | ||
|
|
4f71df79fc | ||
|
|
227d2e0502 | ||
|
|
3a91deb0a4 | ||
|
|
9e6e8357b8 | ||
|
|
0dd17e6347 | ||
|
|
17312d9f90 | ||
|
|
0c36cbde73 | ||
|
|
ebd43c7763 | ||
|
|
adf2f31178 | ||
|
|
beadc0158e | ||
|
|
bb09da08c2 | ||
|
|
ab09f2aff5 | ||
|
|
2f3829083d | ||
|
|
a526c7becd | ||
|
|
2835e1d709 | ||
|
|
4808e31276 | ||
|
|
bd51fd1390 | ||
|
|
b3b1cd2c23 | ||
|
|
d67f674160 | ||
|
|
6eb4438dc8 | ||
|
|
2d6e7f8a37 | ||
|
|
3d37d74c6e | ||
|
|
de19822ed4 | ||
|
|
2b2bcb5264 | ||
|
|
e9b01e6d9f | ||
|
|
b0a760193e | ||
|
|
95569e44e4 | ||
|
|
11816080a6 | ||
|
|
3a86822f0a | ||
|
|
6f8604e18c | ||
|
|
aec3f5e308 | ||
|
|
e0a5de6730 | ||
|
|
5eee49da9a | ||
|
|
b7d01dec70 | ||
|
|
0618130fe3 | ||
|
|
cd245793fc | ||
|
|
3a6c75a1a3 | ||
|
|
5a683b6947 | ||
|
|
9b27f03e61 | ||
|
|
89746ebe09 | ||
|
|
eacf2030cd | ||
|
|
86428539f5 | ||
|
|
a7f519c53a | ||
|
|
49a2d70fbb | ||
|
|
cb7fa00a6f | ||
|
|
a212cdef3f | ||
|
|
4f323a3754 | ||
|
|
f5e7578b41 | ||
|
|
0bf27b117a | ||
|
|
806c22e6bd | ||
|
|
39d7b717a9 | ||
|
|
9d1997e6a0 | ||
|
|
c65f5027d6 |
21
.github/CODEOWNERS
vendored
21
.github/CODEOWNERS
vendored
@@ -1,24 +1,23 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
# Approvals are not required currently but may be enabled in the future.
|
||||
|
||||
### Package Exports ###
|
||||
/**/exports/ @denolfe @jmikrut
|
||||
/**/exports/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Packages ###
|
||||
/packages/richtext-*/ @AlessioGr
|
||||
/packages/plugin-cloud*/ @denolfe
|
||||
/packages/email-*/ @denolfe
|
||||
/packages/storage-*/ @denolfe
|
||||
/packages/create-payload-app/ @denolfe
|
||||
/packages/eslint-*/ @denolfe
|
||||
/packages/plugin-cloud*/src/ @denolfe
|
||||
/packages/email-*/src/ @denolfe
|
||||
/packages/storage-*/src/ @denolfe
|
||||
/packages/create-payload-app/src/ @denolfe
|
||||
/packages/eslint-*/ @denolfe @AlessioGr
|
||||
|
||||
### Templates ###
|
||||
/templates/ @jacobsfletch @denolfe
|
||||
/templates/_data/ @denolfe
|
||||
/templates/_template/ @denolfe
|
||||
|
||||
### Build Files ###
|
||||
/**/package.json @denolfe
|
||||
/tsconfig.json @denolfe
|
||||
/**/tsconfig*.json @denolfe
|
||||
|
||||
/jest.config.js @denolfe
|
||||
/**/jest.config.js @denolfe
|
||||
|
||||
@@ -26,5 +25,5 @@
|
||||
/package.json @denolfe
|
||||
/scripts/ @denolfe
|
||||
/.husky/ @denolfe
|
||||
/.vscode/ @denolfe
|
||||
/.vscode/ @denolfe @AlessioGr
|
||||
/.github/ @denolfe
|
||||
|
||||
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -25,7 +25,7 @@ runs:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
34
.github/workflows/main.yml
vendored
34
.github/workflows/main.yml
vendored
@@ -18,7 +18,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 9.7.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
@@ -207,6 +207,9 @@ jobs:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
@@ -217,17 +220,12 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
- run: pnpm install
|
||||
|
||||
- name: Start LocalStack
|
||||
run: pnpm docker:start
|
||||
@@ -332,7 +330,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
@@ -371,7 +369,7 @@ jobs:
|
||||
run: pnpm exec playwright install-deps chromium
|
||||
|
||||
- name: E2E Tests
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e ${{ matrix.suite }}
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e:prod:ci ${{ matrix.suite }}
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
@@ -407,7 +405,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
@@ -420,7 +418,7 @@ jobs:
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
@@ -451,7 +449,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
@@ -492,7 +490,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
@@ -520,7 +518,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
2
.github/workflows/release-canary.yml
vendored
2
.github/workflows/release-canary.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 9.7.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,7 +5,7 @@ dist
|
||||
!/.idea/runConfigurations
|
||||
!/.idea/payload.iml
|
||||
|
||||
|
||||
test/packed
|
||||
test-results
|
||||
.devcontainer
|
||||
.localstack
|
||||
@@ -306,3 +306,6 @@ test/live-preview/app/(payload)/admin/importMap.js
|
||||
/test/live-preview/app/(payload)/admin/importMap.js
|
||||
test/admin-root/app/(payload)/admin/importMap.js
|
||||
/test/admin-root/app/(payload)/admin/importMap.js
|
||||
test/app/(payload)/admin/importMap.js
|
||||
/test/app/(payload)/admin/importMap.js
|
||||
test/pnpm-lock.yaml
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pnpm run lint-staged --quiet
|
||||
|
||||
@@ -31,12 +31,12 @@ The following options are available:
|
||||
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
|
||||
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
|
||||
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. |
|
||||
| **`description`** | Text or React component to display below the Collection label in the List View to give editors more information. |
|
||||
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#components). |
|
||||
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
|
||||
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this Collection. |
|
||||
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| **`enableRichTextRelationship`** | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| **`meta`** | Metadata overrides to apply to the Admin Panel. Included properties are `description` and `openGraph`. |
|
||||
| **`meta`** | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](./metadata). |
|
||||
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](#preview). |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#components). |
|
||||
@@ -69,7 +69,8 @@ The following options are available:
|
||||
| **`beforeList`** | An array of components to inject _before_ the built-in List View |
|
||||
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
|
||||
| **`afterList`** | An array of components to inject _after_ the built-in List View |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table
|
||||
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
|
||||
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
|
||||
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
|
||||
|
||||
@@ -145,7 +145,7 @@ Instead, we utilize component paths to reference React Components. This method e
|
||||
|
||||
When constructing the `ClientConfig`, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the `PayloadComponent` with a `MappedComponent`. A `MappedComponent` includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the `<RenderComponent />` component within the Payload Admin Panel.
|
||||
|
||||
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map.
|
||||
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map. If you encounter any errors running this command, see the [Troubleshooting](/docs/beta/local-api/outside-nextjs#troubleshooting) section.
|
||||
|
||||
### Component paths in external packages
|
||||
|
||||
|
||||
@@ -347,31 +347,13 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Label Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview). The convention is to append `LabelComponent` to the type of field, i.e. `TextFieldLabelComponent`.
|
||||
When building Custom Label Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `LabelServerComponent` or `LabelClientComponent` to the type of field, i.e. `TextFieldLabelClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
ArrayFieldLabelComponent,
|
||||
BlocksFieldLabelComponent,
|
||||
CheckboxFieldLabelComponent,
|
||||
CodeFieldLabelComponent,
|
||||
CollapsibleFieldLabelComponent,
|
||||
DateFieldLabelComponent,
|
||||
EmailFieldLabelComponent,
|
||||
GroupFieldLabelComponent,
|
||||
HiddenFieldLabelComponent,
|
||||
JSONFieldLabelComponent,
|
||||
NumberFieldLabelComponent,
|
||||
PointFieldLabelComponent,
|
||||
RadioFieldLabelComponent,
|
||||
RelationshipFieldLabelComponent,
|
||||
RichTextFieldLabelComponent,
|
||||
RowFieldLabelComponent,
|
||||
SelectFieldLabelComponent,
|
||||
TabsFieldLabelComponent,
|
||||
TextFieldLabelComponent,
|
||||
TextareaFieldLabelComponent,
|
||||
UploadFieldLabelComponent
|
||||
TextFieldLabelServerComponent,
|
||||
TextFieldLabelClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
@@ -410,31 +392,13 @@ Custom Error Components receive all [Field Component](#the-field-component) prop
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview). The convention is to append `ErrorComponent` to the type of field, i.e. `TextFieldErrorComponent`.
|
||||
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `ErrorServerComponent` or `ErrorClientComponent` to the type of field, i.e. `TextFieldErrorClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
ArrayFieldErrorComponent,
|
||||
BlocksFieldErrorComponent,
|
||||
CheckboxFieldErrorComponent,
|
||||
CodeFieldErrorComponent,
|
||||
CollapsibleFieldErrorComponent,
|
||||
DateFieldErrorComponent,
|
||||
EmailFieldErrorComponent,
|
||||
GroupFieldErrorComponent,
|
||||
HiddenFieldErrorComponent,
|
||||
JSONFieldErrorComponent,
|
||||
NumberFieldErrorComponent,
|
||||
PointFieldErrorComponent,
|
||||
RadioFieldErrorComponent,
|
||||
RelationshipFieldErrorComponent,
|
||||
RichTextFieldErrorComponent,
|
||||
RowFieldErrorComponent,
|
||||
SelectFieldErrorComponent,
|
||||
TabsFieldErrorComponent,
|
||||
TextFieldErrorComponent,
|
||||
TextareaFieldErrorComponent,
|
||||
UploadFieldErrorComponent
|
||||
TextFieldErrorServerComponent,
|
||||
TextFieldErrorClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
@@ -544,31 +508,13 @@ Custom Description Components receive all [Field Component](#the-field-component
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every [Field Type](../fields/overview). The convention is to append `DescriptionComponent` to the type of field, i.e. `TextFieldDescriptionComponent`.
|
||||
When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `DescriptionServerComponent` or `DescriptionClientComponent` to the type of field, i.e. `TextFieldDescriptionClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
ArrayFieldDescriptionComponent,
|
||||
BlocksFieldDescriptionComponent,
|
||||
CheckboxFieldDescriptionComponent,
|
||||
CodeFieldDescriptionComponent,
|
||||
CollapsibleFieldDescriptionComponent,
|
||||
DateFieldDescriptionComponent,
|
||||
EmailFieldDescriptionComponent,
|
||||
GroupFieldDescriptionComponent,
|
||||
HiddenFieldDescriptionComponent,
|
||||
JSONFieldDescriptionComponent,
|
||||
NumberFieldDescriptionComponent,
|
||||
PointFieldDescriptionComponent,
|
||||
RadioFieldDescriptionComponent,
|
||||
RelationshipFieldDescriptionComponent,
|
||||
RichTextFieldDescriptionComponent,
|
||||
RowFieldDescriptionComponent,
|
||||
SelectFieldDescriptionComponent,
|
||||
TabsFieldDescriptionComponent,
|
||||
TextFieldDescriptionComponent,
|
||||
TextareaFieldDescriptionComponent,
|
||||
UploadFieldDescriptionComponent
|
||||
TextFieldDescriptionServerComponent,
|
||||
TextFieldDescriptionClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ The following options are available:
|
||||
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](#preview). |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
| **`meta`** | Metadata overrides to apply to the Admin Panel. Included properties are `description` and `openGraph`. |
|
||||
| **`meta`** | Page metadata overrides to apply to this Global within the Admin Panel. [More details](./metadata). |
|
||||
|
||||
### Components
|
||||
|
||||
|
||||
216
docs/admin/metadata.mdx
Normal file
216
docs/admin/metadata.mdx
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
title: Page Metadata
|
||||
label: Metadata
|
||||
order: 70
|
||||
desc: Customize the metadata of your pages within the Admin Panel
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more, without any additional configuration. This includes the page title, description, og:image and everything in between. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views, allowing for the ability to control metadata on any page with high precision.
|
||||
|
||||
Within the Admin Panel, metadata can be customized at the following levels:
|
||||
|
||||
- [Root Metadata](#root-metadata)
|
||||
- [Collection Metadata](#collection-metadata)
|
||||
- [Global Metadata](#global-metadata)
|
||||
- [View Metadata](#view-metadata)
|
||||
|
||||
All of these types of metadata share a similar structure, with a few key differences on the Root level. To customize metadata, consult the list of available scopes. Determine the scope that corresponds to what you are trying to accomplish, then author your metadata within the Payload Config accordingly.
|
||||
|
||||
## Root Metadata
|
||||
|
||||
Root Metadata is the metadata that is applied to all pages within the Admin Panel. This is where you can control things like the suffix appended onto each page's title, the favicon displayed in the browser's tab, and the Open Graph data that is used when sharing the Admin Panel on social media.
|
||||
|
||||
To customize Root Metadata, use the `admin.meta` key in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Admin Panel',
|
||||
description: 'The best admin panel in the world',
|
||||
icons: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: '/favicon.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Root Metadata:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`defaultOGImageType`** | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
|
||||
| **`icons`** | `IconConfig[]` | An array of icon objects. [More details](#icons) |
|
||||
| **`keywords`** | `string` | A comma-separated list of keywords to include in the metadata of the Admin Panel. |
|
||||
| **`openGraph`** | `OpenGraphConfig` | An object containing Open Graph metadata. [More details](#open-graph) |
|
||||
| **`titleSuffix`** | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
These are the _root-level_ options for the Admin Panel. You can also customize [Collection Metadata](./collections), [Global Metadata](./globals), and [Document Metadata](./documents) in their respective configs.
|
||||
</Banner>
|
||||
|
||||
### Icons
|
||||
|
||||
The Icons Config corresponds to the `<link>` tags that are used to specify icons for the Admin Panel. The `icons` key is an array of objects, each of which represents an individual icon. Icons are differentiated from one another by their `rel` attribute, which specifies the relationship between the document and the icon.
|
||||
|
||||
The most common icon type is the favicon, which is displayed in the browser tab. This is specified by the `rel` attribute `icon`. Other common icon types include `apple-touch-icon`, which is used by Apple devices when the Admin Panel is saved to the home screen, and `mask-icon`, which is used by Safari to mask the Admin Panel icon.
|
||||
|
||||
To customize icons, use the `icons` key within the `admin.meta` object in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
meta: {
|
||||
// highlight-start
|
||||
icons: [
|
||||
// highlight-end
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: '/favicon.png',
|
||||
},
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
type: 'image/png',
|
||||
href: '/apple-touch-icon.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Icons:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`rel`** | `string` | The HTML `rel` attribute of the icon. |
|
||||
| **`type`** | `string` | The MIME type of the icon. |
|
||||
| **`color`** | `string` | The color of the icon. |
|
||||
| **`fetchPriority`** | `string` | The [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) of the icon. |
|
||||
| **`media`** | `string` | The [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) of the icon. |
|
||||
| **`sizes`** | `string` | The [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) of the icon. |
|
||||
| **`url`** | `string` | The URL pointing the resource of the icon. |
|
||||
|
||||
### Open Graph
|
||||
|
||||
Open Graph metadata is a set of tags that are used to control how URLs are displayed when shared on social media platforms. Open Graph metadata is automatically generated by Payload, but can be customized at the Root level.
|
||||
|
||||
To customize Open Graph metadata, use the `openGraph` key within the `admin.meta` object in your Payload Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
meta: {
|
||||
// highlight-start
|
||||
openGraph: {
|
||||
// highlight-end
|
||||
description: 'The best admin panel in the world',
|
||||
images: [
|
||||
{
|
||||
url: 'https://example.com/image.jpg',
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
],
|
||||
siteName: 'Payload',
|
||||
title: 'My Admin Panel',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available for Open Graph Metadata:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| **`description`** | `string` | The description of the Admin Panel. |
|
||||
| **`images`** | `OGImageConfig | OGImageConfig[]` | An array of image objects. |
|
||||
| **`siteName`** | `string` | The name of the site. |
|
||||
| **`title`** | `string` | The title of the Admin Panel. |
|
||||
|
||||
## Collection Metadata
|
||||
|
||||
Collection Metadata is the metadata that is applied to all pages within any given Collection within the Admin Panel. This metadata is used to customize the title and description of all views within any given Collection, unless overridden by the view itself.
|
||||
|
||||
To customize Collection Metadata, use the `admin.meta` key within your Collection Config:
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Collection',
|
||||
description: 'The best collection in the world',
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The Collection Meta config has the same options as the [Root Metadata](#root-metadata) config.
|
||||
|
||||
## Global Metadata
|
||||
|
||||
Global Metadata is the metadata that is applied to all pages within any given Global within the Admin Panel. This metadata is used to customize the title and description of all views within any given Global, unless overridden by the view itself.
|
||||
|
||||
To customize Global Metadata, use the `admin.meta` key within your Global Config:
|
||||
|
||||
```ts
|
||||
import { GlobalConfig } from 'payload'
|
||||
|
||||
export const MyGlobal: GlobalConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Global',
|
||||
description: 'The best
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The Global Meta config has the same options as the [Root Metadata](#root-metadata) config.
|
||||
|
||||
## View Metadata
|
||||
|
||||
View Metadata is the metadata that is applied to specific [Views](./views) within the Admin Panel. This metadata is used to customize the title and description of a specific view, overriding any metadata set at the [Root](#root-metadata), [Collection](#collection-metadata), or [Global](#global-metadata) level.
|
||||
|
||||
To customize View Metadata, use the `meta` key within your View Config:
|
||||
|
||||
```ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
views: {
|
||||
dashboard: {
|
||||
// highlight-start
|
||||
meta: {
|
||||
// highlight-end
|
||||
title: 'My Dashboard',
|
||||
description: 'The best dashboard in the world',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -88,17 +88,17 @@ The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](./components). |
|
||||
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `disable` | If set to `true`, the entire Admin Panel will be disabled. |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `meta` | Base metadata to use for the Admin Panel. Included properties are `titleSuffix`, `icons`, and `openGraph`. Can be overridden on a per Collection or per Global basis. |
|
||||
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](./components). |
|
||||
| **`custom`** | Any custom properties you wish to pass to the Admin Panel. |
|
||||
| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| **`disable`** | If set to `true`, the entire Admin Panel will be disabled. |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
|
||||
@@ -53,11 +53,12 @@ For more granular control, pass a configuration object instead. Payload exposes
|
||||
|
||||
| Property | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`Component`** \* | Pass in the component that should be rendered when a user navigates to this route. |
|
||||
| **`Component`** \* | Pass in the component path that should be rendered when a user navigates to this route. |
|
||||
| **`path`** \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
|
||||
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||
| **`sensitive`** | When true, will match if the path is case sensitive. |
|
||||
| **`sensitive`** | When true, will match if the path is case sensitive.
|
||||
| **`meta`** | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -111,7 +112,20 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
components: {
|
||||
views: {
|
||||
edit: {
|
||||
Component: '/path/to/MyCustomEditView', // highlight-line
|
||||
root: {
|
||||
Component: '/path/to/MyCustomEditView', // highlight-line
|
||||
}
|
||||
// other options include:
|
||||
// default
|
||||
// versions
|
||||
// version
|
||||
// api
|
||||
// livePreview
|
||||
// [key: string]
|
||||
// See "Document Views" for more details
|
||||
},
|
||||
list: {
|
||||
Component: '/path/to/MyCustomListView',
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -123,7 +137,7 @@ _For details on how to build Custom Views, see [Building Custom Views](#building
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
The `Edit` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `Edit.Default` key instead.
|
||||
The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
@@ -152,18 +166,29 @@ export const MyGlobalConfig: SanitizedGlobalConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
edit: '/path/to/MyCustomEditView', // highlight-line
|
||||
edit: {
|
||||
root: {
|
||||
Component: '/path/to/MyCustomEditView', // highlight-line
|
||||
}
|
||||
// other options include:
|
||||
// default
|
||||
// versions
|
||||
// version
|
||||
// api
|
||||
// livePreview
|
||||
// [key: string]
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
The `Edit` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `Edit.Default` key instead.
|
||||
The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
@@ -199,25 +224,26 @@ export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Views, see [Building Custom Views](#building-custom-views)._
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
If you need to replace the _entire_ Edit View, including _all_ nested Document Views, use the `Edit` key itself. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information.
|
||||
If you need to replace the _entire_ Edit View, including _all_ nested Document Views, use the `root` key. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information.
|
||||
</Banner>
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`default`** | The Default view is the primary view in which your document is edited. |
|
||||
| **`versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). |
|
||||
| **`version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
|
||||
| **`api`** | The API view is used to display the REST API JSON response for a given document. |
|
||||
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
|
||||
| **`root`** | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. |
|
||||
| **`default`** | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. |
|
||||
| **`versions`** | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](../versions). |
|
||||
| **`version`** | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions). |
|
||||
| **`api`** | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. |
|
||||
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [More details](../live-preview). |
|
||||
|
||||
### Document Tabs
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ IMPORTANT: This will overwrite all slate data. We recommend doing the following
|
||||
1. Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support.
|
||||
2. Make every richText field a lexical editor. This script will only convert lexical richText fields with old Slate data
|
||||
3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the Admin Panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated.
|
||||
4. If this works as expected, add the `disableHooks: true` prop everywhere you're initializing `SlateToLexicalFeature`. Example: `SlateToLexicalFeature({ disableHooks: true })`. Once you did that, you're ready to run the migration script.
|
||||
|
||||
```ts
|
||||
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical/migrate'
|
||||
|
||||
@@ -61,14 +61,27 @@ payload run src/seed.ts
|
||||
The `payload run` command does two things for you:
|
||||
|
||||
1. It loads the environment variables the same way Next.js loads them, eliminating the need for additional dependencies like `dotenv`. The usage of `dotenv` is not recommended, as Next.js loads environment variables differently. By using `payload run`, you ensure consistent environment variable handling across your Payload and Next.js setup.
|
||||
2. It initializes swc, allowing direct execution of TypeScript files without requiring tools like tsx or ts-node.
|
||||
2. It initializes tsx, allowing direct execution of TypeScript files manually installing tools like tsx or ts-node.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you encounter import-related errors, try running the script in TSX mode:
|
||||
If you encounter import-related errors, you have 2 options:
|
||||
|
||||
#### Option 1: enable swc mode by appending `--use-swc` to the `payload` command:
|
||||
|
||||
Example:
|
||||
```sh
|
||||
payload run src/seed.ts --use-tsx
|
||||
payload run src/seed.ts --use-swc
|
||||
```
|
||||
|
||||
Note: Install tsx in your project first. Be aware that TSX mode is slower than the default swc mode, so only use it if necessary.
|
||||
Note: Install @swc-node/register in your project first. While swc mode is faster than the default tsx mode, it might break for some imports.
|
||||
|
||||
#### Option 2: use an alternative runtime like bun
|
||||
|
||||
While we do not guarantee support for alternative runtimes, you are free to use them and disable payloads own transpilation by appending the `--disable-transpilation` flag to the `payload` command:
|
||||
|
||||
```sh
|
||||
bunx --bun payload run src/seed.ts --disable-transpile
|
||||
```
|
||||
|
||||
You will need to have bun installed on your system for this to work.
|
||||
|
||||
@@ -467,10 +467,10 @@ export const ServerRenderedDescription = () => <ClientRenderedDescription />
|
||||
// file: components/ClientRenderedDescription.tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { DescriptionComponent } from 'payload'
|
||||
import type { TextFieldDescriptionClientComponent } from 'payload'
|
||||
import { useFieldProps, useFormFields } from '@payloadcms/ui'
|
||||
|
||||
export const ClientRenderedDescription: DescriptionComponent = () ={
|
||||
export const ClientRenderedDescription: TextFieldDescriptionClientComponent = () ={
|
||||
const { path } = useFieldProps()
|
||||
const { value } = useFormFields(([fields]) => fields[path])
|
||||
const customDescription = `Component description: ${path} - ${value}`
|
||||
@@ -659,8 +659,8 @@ export const ClientArrayRowLabel = () => {
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
Tab: {
|
||||
edit: {
|
||||
tab: {
|
||||
pillLabel: '',
|
||||
},
|
||||
},
|
||||
@@ -675,9 +675,11 @@ export const ClientArrayRowLabel = () => {
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
Tab: {
|
||||
Pill: MyPill,
|
||||
edit: {
|
||||
tab: {
|
||||
pill: {
|
||||
Component: './path/to/CustomPill.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -66,6 +66,8 @@ export default config
|
||||
| ------------- | ---------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `collections` | `string[]` | An array of collection slugs to populate in the `to` field of each redirect. |
|
||||
| `overrides` | `object` | A partial collection config that allows you to override anything on the `redirects` collection. |
|
||||
| `redirectTypes` | `string[]` | Provide an array of redirects if you want to provide options for the type of redirects to be supported. |
|
||||
| `redirectTypeFieldOverride` | `Field` | A partial Field config that allows you to override the Redirect Type field if enabled above. |
|
||||
|
||||
Note that the fields in overrides take a function that receives the default fields and returns an array of fields. This allows you to add fields to the collection.
|
||||
|
||||
@@ -83,6 +85,10 @@ redirectsPlugin({
|
||||
]
|
||||
},
|
||||
},
|
||||
redirectTypes: ['301', '302'],
|
||||
redirectTypeFieldOverride: {
|
||||
label: 'Redirect Type (Overridden)',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ A function that allows you to return any meta title, including from document's c
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateTitle: ({ ...docInfo, doc, locale }) => `Website.com — ${doc?.title}`,
|
||||
generateTitle: ({ ...docInfo, doc, locale, req }) => `Website.com — ${doc?.title}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -133,7 +133,7 @@ A function that allows you to return any meta description, including from docume
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateDescription: ({ ...docInfo, doc, locale }) => doc?.excerpt,
|
||||
generateDescription: ({ ...docInfo, doc, locale, req }) => doc?.excerpt,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -147,7 +147,7 @@ A function that allows you to return any meta image, including from document's c
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateImage: ({ ...docInfo, doc, locale }) => doc?.featuredImage,
|
||||
generateImage: ({ ...docInfo, doc, locale, req }) => doc?.featuredImage,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -161,7 +161,7 @@ A function called by the search preview component to display the actual URL of y
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateURL: ({ ...docInfo, doc, locale }) =>
|
||||
generateURL: ({ ...docInfo, doc, locale, req }) =>
|
||||
`https://yoursite.com/${collection?.slug}/${doc?.slug}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ _An asterisk denotes that an option is required._
|
||||
| Option | Description |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
||||
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
|
||||
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
||||
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
|
||||
|
||||
@@ -62,6 +62,7 @@ export const rootEslintConfig = [
|
||||
'payload/no-jsx-import-statements': 'warn',
|
||||
'payload/no-relative-monorepo-imports': 'error',
|
||||
'payload/no-imports-from-exports-dir': 'error',
|
||||
'payload/no-imports-from-self': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ export default withBundleAnalyzer(
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
PAYLOAD_DISABLE_DEPENDENCY_CHECKER: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -16,6 +16,7 @@
|
||||
"build:db-mongodb": "turbo build --filter db-mongodb",
|
||||
"build:db-postgres": "turbo build --filter db-postgres",
|
||||
"build:db-sqlite": "turbo build --filter db-sqlite",
|
||||
"build:db-vercel-postgres": "turbo build --filter db-vercel-postgres",
|
||||
"build:drizzle": "turbo build --filter drizzle",
|
||||
"build:email-nodemailer": "turbo build --filter email-nodemailer",
|
||||
"build:email-resend": "turbo build --filter email-resend",
|
||||
@@ -53,12 +54,12 @@
|
||||
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media/*' '**/dist/' '**/.cache/*' '**/.next/*' '**/.turbo/*' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
|
||||
"dev": "pnpm runts ./test/dev.ts",
|
||||
"runts": "node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"dev": "tsx ./test/dev.ts",
|
||||
"dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
|
||||
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
||||
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
||||
"dev:postgres": "pnpm runts ./test/dev.ts",
|
||||
"dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts",
|
||||
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
|
||||
@@ -72,6 +73,7 @@
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
||||
"runts": "node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"script:gen-templates": "pnpm runts ./scripts/generate-template-variations.ts",
|
||||
"script:list-published": "pnpm runts scripts/lib/getPackageRegistryVersions.ts",
|
||||
"script:pack": "pnpm runts scripts/pack-all-to-dest.ts",
|
||||
@@ -81,6 +83,8 @@
|
||||
"test:e2e": "pnpm runts ./test/runE2E.ts",
|
||||
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd .. && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:e2e:prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd .. && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
|
||||
@@ -93,13 +97,14 @@
|
||||
"prettier --write",
|
||||
"eslint --cache --fix"
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace --frozen-lockfile; pnpm run lint --fix\"",
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --ignore-workspace; pnpm run lint --fix\"",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.6.2",
|
||||
"@next/bundle-analyzer": "15.0.0-canary.104",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
"@payloadcms/live-preview-react": "workspace:*",
|
||||
@@ -123,6 +128,7 @@
|
||||
"create-payload-app": "workspace:*",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-kit": "0.23.2-df9e596",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"execa": "5.1.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -182,6 +182,7 @@ export function mongooseAdapter({
|
||||
init,
|
||||
migrateFresh,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-mongodb',
|
||||
payload,
|
||||
prodMigrations,
|
||||
queryDrafts,
|
||||
|
||||
@@ -595,14 +595,77 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
config: SanitizedConfig,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
const hasManyRelations = Array.isArray(field.relationTo)
|
||||
let schemaToReturn: { [key: string]: any } = {}
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
schemaToReturn = {
|
||||
type: config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
if (hasManyRelations) {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
refPath: `${field.name}.${locale}.relationTo`,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
localeSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...locales,
|
||||
[locale]: field.hasMany
|
||||
? { type: [localeSchema], default: formatDefaultValue(field) }
|
||||
: localeSchema,
|
||||
}
|
||||
}, {}),
|
||||
localized: true,
|
||||
}
|
||||
} else if (hasManyRelations) {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
_id: false,
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
relationTo: { type: String, enum: field.relationTo },
|
||||
value: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
refPath: `${field.name}.relationTo`,
|
||||
},
|
||||
}
|
||||
|
||||
if (field.hasMany) {
|
||||
schemaToReturn = {
|
||||
type: [schemaToReturn],
|
||||
default: formatDefaultValue(field),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
schemaToReturn = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
ref: field.relationTo,
|
||||
}
|
||||
|
||||
if (field.hasMany) {
|
||||
schemaToReturn = {
|
||||
type: [schemaToReturn],
|
||||
default: formatDefaultValue(field),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: schemaToReturn,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -32,28 +32,26 @@ import {
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
convertPathToJSONTraversal,
|
||||
countDistinct,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
defaultDrizzleSnapshot,
|
||||
deleteWhere,
|
||||
dropDatabase,
|
||||
execute,
|
||||
getMigrationTemplate,
|
||||
init,
|
||||
insert,
|
||||
requireDrizzleKit,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createDatabaseAdapter } from 'payload'
|
||||
|
||||
import type { Args, PostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
import { countDistinct } from './countDistinct.js'
|
||||
import { convertPathToJSONTraversal } from './createJSONQuery/convertPathToJSONTraversal.js'
|
||||
import { createJSONQuery } from './createJSONQuery/index.js'
|
||||
import { createMigration } from './createMigration.js'
|
||||
import { defaultDrizzleSnapshot } from './defaultSnapshot.js'
|
||||
import { deleteWhere } from './deleteWhere.js'
|
||||
import { dropDatabase } from './dropDatabase.js'
|
||||
import { execute } from './execute.js'
|
||||
import { getMigrationTemplate } from './getMigrationTemplate.js'
|
||||
import { init } from './init.js'
|
||||
import { insert } from './insert.js'
|
||||
import { requireDrizzleKit } from './requireDrizzleKit.js'
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
|
||||
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
@@ -141,6 +139,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-postgres',
|
||||
payload,
|
||||
queryDrafts,
|
||||
rejectInitializing,
|
||||
@@ -159,3 +158,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
@@ -1,31 +1,14 @@
|
||||
import type { Operators } from '@payloadcms/drizzle'
|
||||
import type {
|
||||
BuildQueryJoinAliases,
|
||||
DrizzleAdapter,
|
||||
TransactionPg,
|
||||
} from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type {
|
||||
ColumnBaseConfig,
|
||||
ColumnDataType,
|
||||
DrizzleConfig,
|
||||
Relation,
|
||||
Relations,
|
||||
SQL,
|
||||
} from 'drizzle-orm'
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
import type {
|
||||
PgColumn,
|
||||
PgEnum,
|
||||
PgInsertOnConflictDoUpdateConfig,
|
||||
PgSchema,
|
||||
PgTableWithColumns,
|
||||
PgTransactionConfig,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
import type { Pool, PoolConfig, QueryResult } from 'pg'
|
||||
BasePostgresAdapter,
|
||||
GenericEnum,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
PostgresDB,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleConfig } from 'drizzle-orm'
|
||||
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
|
||||
import type { Pool, PoolConfig } from 'pg'
|
||||
|
||||
export type Args = {
|
||||
idType?: 'serial' | 'uuid'
|
||||
@@ -49,125 +32,10 @@ export type Args = {
|
||||
versionsSuffix?: string
|
||||
}
|
||||
|
||||
export type GenericColumn = PgColumn<
|
||||
ColumnBaseConfig<ColumnDataType, string>,
|
||||
Record<string, unknown>
|
||||
>
|
||||
|
||||
export type GenericColumns = {
|
||||
[x: string]: GenericColumn
|
||||
}
|
||||
|
||||
export type GenericTable = PgTableWithColumns<{
|
||||
columns: GenericColumns
|
||||
dialect: string
|
||||
name: string
|
||||
schema: string
|
||||
}>
|
||||
|
||||
export type GenericEnum = PgEnum<[string, ...string[]]>
|
||||
|
||||
export type GenericRelation = Relations<string, Record<string, Relation<string>>>
|
||||
|
||||
export type PostgresDB = NodePgDatabase<Record<string, unknown>>
|
||||
|
||||
export type CountDistinct = (args: {
|
||||
db: PostgresDB | TransactionPg
|
||||
joins: BuildQueryJoinAliases
|
||||
tableName: string
|
||||
where: SQL
|
||||
}) => Promise<number>
|
||||
|
||||
export type DeleteWhere = (args: {
|
||||
db: PostgresDB | TransactionPg
|
||||
tableName: string
|
||||
where: SQL
|
||||
}) => Promise<void>
|
||||
|
||||
export type DropDatabase = (args: { adapter: PostgresAdapter }) => Promise<void>
|
||||
|
||||
export type Execute<T> = (args: {
|
||||
db?: PostgresDB | TransactionPg
|
||||
drizzle?: PostgresDB
|
||||
raw?: string
|
||||
sql?: SQL<unknown>
|
||||
}) => Promise<QueryResult<Record<string, T>>>
|
||||
|
||||
export type Insert = (args: {
|
||||
db: PostgresDB | TransactionPg
|
||||
onConflictDoUpdate?: PgInsertOnConflictDoUpdateConfig<any>
|
||||
tableName: string
|
||||
values: Record<string, unknown> | Record<string, unknown>[]
|
||||
}) => Promise<Record<string, unknown>[]>
|
||||
|
||||
type PostgresDrizzleAdapter = Omit<
|
||||
DrizzleAdapter,
|
||||
| 'countDistinct'
|
||||
| 'deleteWhere'
|
||||
| 'drizzle'
|
||||
| 'dropDatabase'
|
||||
| 'execute'
|
||||
| 'insert'
|
||||
| 'operators'
|
||||
| 'relations'
|
||||
>
|
||||
|
||||
type Schema =
|
||||
| {
|
||||
enum: typeof pgEnum
|
||||
table: PgTableFn
|
||||
}
|
||||
| PgSchema
|
||||
|
||||
export type PostgresAdapter = {
|
||||
countDistinct: CountDistinct
|
||||
defaultDrizzleSnapshot: DrizzleSnapshotJSON
|
||||
deleteWhere: DeleteWhere
|
||||
drizzle: PostgresDB
|
||||
dropDatabase: DropDatabase
|
||||
enums: Record<string, GenericEnum>
|
||||
execute: Execute<unknown>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
*/
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
idType: Args['idType']
|
||||
initializing: Promise<void>
|
||||
insert: Insert
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
operators: Operators
|
||||
pgSchema?: Schema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
resolveInitializing: () => void
|
||||
schemaName?: Args['schemaName']
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
db: PostgresDB | TransactionPg
|
||||
reject: () => Promise<void>
|
||||
resolve: () => Promise<void>
|
||||
}
|
||||
}
|
||||
tableNameMap: Map<string, string>
|
||||
tables: Record<string, GenericTable>
|
||||
versionsSuffix?: string
|
||||
} & PostgresDrizzleAdapter
|
||||
|
||||
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
|
||||
|
||||
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
|
||||
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
|
||||
poolOptions: PoolConfig
|
||||
} & BasePostgresAdapter
|
||||
|
||||
declare module 'payload' {
|
||||
export interface DatabaseAdapter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -140,6 +140,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-sqlite',
|
||||
payload,
|
||||
queryDrafts,
|
||||
rejectInitializing,
|
||||
|
||||
@@ -717,7 +717,7 @@ export const traverseFields = ({
|
||||
case 'upload':
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
||||
} else if (field.type === 'relationship' && field.hasMany) {
|
||||
} else if (field.hasMany) {
|
||||
relationships.add(field.relationTo)
|
||||
} else {
|
||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||
|
||||
1
packages/db-vercel-postgres/.gitignore
vendored
Normal file
1
packages/db-vercel-postgres/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/migrations
|
||||
10
packages/db-vercel-postgres/.prettierignore
Normal file
10
packages/db-vercel-postgres/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
15
packages/db-vercel-postgres/.swcrc
Normal file
15
packages/db-vercel-postgres/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
43
packages/db-vercel-postgres/README.md
Normal file
43
packages/db-vercel-postgres/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Payload Postgres Adapter
|
||||
|
||||
[Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres) adapter for [Payload](https://payloadcms.com).
|
||||
|
||||
- [Main Repository](https://github.com/payloadcms/payload)
|
||||
- [Payload Docs](https://payloadcms.com/docs)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/db-vercel-postgres
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Explicit Connection String
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
|
||||
|
||||
export default buildConfig({
|
||||
db: vercelPostgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URI,
|
||||
},
|
||||
}),
|
||||
// ...rest of config
|
||||
})
|
||||
```
|
||||
|
||||
### Automatic Connection String Detection
|
||||
|
||||
Have Vercel automatically detect from environment variable (typically `process.env.POSTGRES_URL`)
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
db: postgresAdapter(),
|
||||
// ...rest of config
|
||||
})
|
||||
```
|
||||
|
||||
More detailed usage can be found in the [Payload Docs](https://payloadcms.com/docs/configuration/overview).
|
||||
20
packages/db-vercel-postgres/eslint.config.js
Normal file
20
packages/db-vercel-postgres/eslint.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.FlatConfig} */
|
||||
let FlatConfig
|
||||
|
||||
/** @type {FlatConfig[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigDirName: import.meta.dirname,
|
||||
...rootParserOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
89
packages/db-vercel-postgres/package.json
Normal file
89
packages/db-vercel-postgres/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/payloadcms/payload.git",
|
||||
"directory": "packages/db-vercel-postgres"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
"types": "./src/exports/migration-utils.ts",
|
||||
"default": "./src/exports/migration-utils.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rimraf .dist && rimraf tsconfig.tsbuildinfo && pnpm build:types && pnpm build:swc && pnpm build:esbuild && pnpm renamePredefinedMigrations",
|
||||
"build:esbuild": "echo skipping esbuild",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepack": "pnpm clean && pnpm turbo build",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"renamePredefinedMigrations": "node --no-deprecation --import @swc-node/register/esm-register ./scripts/renamePredefinedMigrations.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@vercel/postgres": "^0.9.0",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.23.2-df9e596",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.23.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
"types": "./dist/exports/migration-utils.d.ts",
|
||||
"default": "./dist/exports/migration-utils.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
}
|
||||
13
packages/db-vercel-postgres/relationships-v2-v3.mjs
Normal file
13
packages/db-vercel-postgres/relationships-v2-v3.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
const imports = `import { migratePostgresV2toV3 } from '@payloadcms/migratePostgresV2toV3'`
|
||||
const up = ` await migratePostgresV2toV3({
|
||||
// enables logging of changes that will be made to the database
|
||||
debug: false,
|
||||
// skips calls that modify schema or data
|
||||
dryRun: false,
|
||||
payload,
|
||||
req,
|
||||
})
|
||||
`
|
||||
export { imports, up }
|
||||
|
||||
//# sourceMappingURL=relationships-v2-v3.js.map
|
||||
@@ -0,0 +1,19 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* Changes built .js files to .mjs to for ESM imports
|
||||
*/
|
||||
const rename = () => {
|
||||
fs.readdirSync(path.resolve('./dist/predefinedMigrations'))
|
||||
.filter((f) => {
|
||||
return f.endsWith('.js')
|
||||
})
|
||||
.forEach((file) => {
|
||||
const newPath = path.join('./dist/predefinedMigrations', file)
|
||||
fs.renameSync(newPath, newPath.replace('.js', '.mjs'))
|
||||
})
|
||||
console.log('done')
|
||||
}
|
||||
|
||||
rename()
|
||||
61
packages/db-vercel-postgres/src/connect.ts
Normal file
61
packages/db-vercel-postgres/src/connect.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { VercelPool, sql } from '@vercel/postgres'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
|
||||
import type { VercelPostgresAdapter } from './types.js'
|
||||
|
||||
export const connect: Connect = async function connect(
|
||||
this: VercelPostgresAdapter,
|
||||
options = {
|
||||
hotReload: false,
|
||||
},
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
}
|
||||
|
||||
try {
|
||||
const logger = this.logger || false
|
||||
// Passed the poolOptions if provided,
|
||||
// else have vercel/postgres detect the connection string from the environment
|
||||
this.drizzle = drizzle(this.poolOptions ? new VercelPool(this.poolOptions) : sql, {
|
||||
logger,
|
||||
schema: this.schema,
|
||||
})
|
||||
|
||||
if (!hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
|
||||
await this.dropDatabase({ adapter: this })
|
||||
this.payload.logger.info('---- DROPPED TABLES ----')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
|
||||
if (typeof this.rejectInitializing === 'function') this.rejectInitializing()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Only push schema if not in production
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_MIGRATING !== 'true' &&
|
||||
this.push !== false
|
||||
) {
|
||||
await pushDevSchema(this as unknown as DrizzleAdapter)
|
||||
}
|
||||
|
||||
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { migratePostgresV2toV3 } from '../predefinedMigrations/v2-v3/index.js'
|
||||
163
packages/db-vercel-postgres/src/index.ts
Normal file
163
packages/db-vercel-postgres/src/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { DatabaseAdapterObj, Payload } from 'payload'
|
||||
|
||||
import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createVersion,
|
||||
deleteMany,
|
||||
deleteOne,
|
||||
deleteVersions,
|
||||
destroy,
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
findMigrationDir,
|
||||
findOne,
|
||||
findVersions,
|
||||
migrate,
|
||||
migrateDown,
|
||||
migrateFresh,
|
||||
migrateRefresh,
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
operatorMap,
|
||||
queryDrafts,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
convertPathToJSONTraversal,
|
||||
countDistinct,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
defaultDrizzleSnapshot,
|
||||
deleteWhere,
|
||||
dropDatabase,
|
||||
execute,
|
||||
getMigrationTemplate,
|
||||
init,
|
||||
insert,
|
||||
requireDrizzleKit,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createDatabaseAdapter } from 'payload'
|
||||
|
||||
import type { Args, VercelPostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
|
||||
export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<VercelPostgresAdapter> {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
|
||||
|
||||
function adapter({ payload }: { payload: Payload }) {
|
||||
const migrationDir = findMigrationDir(args.migrationDir)
|
||||
let resolveInitializing
|
||||
let rejectInitializing
|
||||
let adapterSchema: VercelPostgresAdapter['pgSchema']
|
||||
|
||||
const initializing = new Promise<void>((res, rej) => {
|
||||
resolveInitializing = res
|
||||
rejectInitializing = rej
|
||||
})
|
||||
|
||||
if (args.schemaName) {
|
||||
adapterSchema = pgSchema(args.schemaName)
|
||||
} else {
|
||||
adapterSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
return createDatabaseAdapter<VercelPostgresAdapter>({
|
||||
name: 'postgres',
|
||||
defaultDrizzleSnapshot,
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
features: {
|
||||
json: true,
|
||||
},
|
||||
fieldConstraints: {},
|
||||
getMigrationTemplate,
|
||||
idType: postgresIDType,
|
||||
initializing,
|
||||
localesSuffix: args.localesSuffix || '_locales',
|
||||
logger: args.logger,
|
||||
operators: operatorMap,
|
||||
pgSchema: adapterSchema,
|
||||
pool: undefined,
|
||||
poolOptions: args.pool,
|
||||
prodMigrations: args.prodMigrations,
|
||||
push: args.push,
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
schema: {},
|
||||
schemaName: args.schemaName,
|
||||
sessions: {},
|
||||
tableNameMap: new Map<string, string>(),
|
||||
tables: {},
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
// DatabaseAdapter
|
||||
beginTransaction: args.transactionOptions === false ? undefined : beginTransaction,
|
||||
commitTransaction,
|
||||
connect,
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
createVersion,
|
||||
defaultIDType: payloadIDType,
|
||||
deleteMany,
|
||||
deleteOne,
|
||||
deleteVersions,
|
||||
deleteWhere,
|
||||
destroy,
|
||||
dropDatabase,
|
||||
execute,
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
findOne,
|
||||
findVersions,
|
||||
init,
|
||||
insert,
|
||||
migrate,
|
||||
migrateDown,
|
||||
migrateFresh,
|
||||
migrateRefresh,
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
migrationDir,
|
||||
packageName: '@payloadcms/db-vercel-postgres',
|
||||
payload,
|
||||
queryDrafts,
|
||||
rejectInitializing,
|
||||
requireDrizzleKit,
|
||||
resolveInitializing,
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
|
||||
export { sql } from 'drizzle-orm'
|
||||
@@ -0,0 +1,10 @@
|
||||
const imports = `import { migratePostgresV2toV3 } from '@payloadcms/db-postgres/migration-utils'`
|
||||
const upSQL = ` await migratePostgresV2toV3({
|
||||
// enables logging of changes that will be made to the database
|
||||
debug: false,
|
||||
payload,
|
||||
req,
|
||||
})
|
||||
`
|
||||
|
||||
export { imports, upSQL }
|
||||
@@ -0,0 +1,237 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { Field, Payload, PayloadRequest } from 'payload'
|
||||
|
||||
import { upsertRow } from '@payloadcms/drizzle'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../../types.js'
|
||||
import type { DocsToResave } from '../types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
adapter: VercelPostgresAdapter
|
||||
collectionSlug?: string
|
||||
db: TransactionPg
|
||||
debug: boolean
|
||||
docsToResave: DocsToResave
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
isVersions: boolean
|
||||
payload: Payload
|
||||
req: PayloadRequest
|
||||
tableName: string
|
||||
}
|
||||
|
||||
export const fetchAndResave = async ({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
db,
|
||||
debug,
|
||||
docsToResave,
|
||||
fields,
|
||||
globalSlug,
|
||||
isVersions,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
}: Args) => {
|
||||
for (const [id, rows] of Object.entries(docsToResave)) {
|
||||
if (collectionSlug) {
|
||||
const collectionConfig = payload.collections[collectionSlug].config
|
||||
|
||||
if (collectionConfig) {
|
||||
if (isVersions) {
|
||||
const doc = await payload.findVersionByID({
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`The collection "${collectionConfig.slug}" version with ID ${id} will be migrated`,
|
||||
)
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
id: doc.id,
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(
|
||||
`"${collectionConfig.slug}" version with ID ${doc.id} FAILED TO MIGRATE`,
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`"${collectionConfig.slug}" version with ID ${doc.id} migrated successfully!`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const doc = await payload.findByID({
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`The collection "${collectionConfig.slug}" with ID ${doc.id} will be migrated`,
|
||||
)
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
id: doc.id,
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(
|
||||
`The collection "${collectionConfig.slug}" with ID ${doc.id} has FAILED TO MIGRATE`,
|
||||
)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`The collection "${collectionConfig.slug}" with ID ${doc.id} has migrated successfully!`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
const globalConfig = payload.config.globals?.find((global) => global.slug === globalSlug)
|
||||
|
||||
if (globalConfig) {
|
||||
if (isVersions) {
|
||||
const { docs } = await payload.findGlobalVersions({
|
||||
slug: globalSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
limit: 0,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(`${docs.length} global "${globalSlug}" versions will be migrated`)
|
||||
}
|
||||
|
||||
for (const doc of docs) {
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
id: doc.id,
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(`"${globalSlug}" version with ID ${doc.id} FAILED TO MIGRATE`)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(
|
||||
`"${globalSlug}" version with ID ${doc.id} migrated successfully!`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const doc = await payload.findGlobal({
|
||||
slug: globalSlug,
|
||||
depth: 0,
|
||||
fallbackLocale: null,
|
||||
locale: 'all',
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
traverseFields({
|
||||
doc,
|
||||
fields,
|
||||
path: '',
|
||||
rows,
|
||||
})
|
||||
|
||||
try {
|
||||
await upsertRow({
|
||||
adapter,
|
||||
data: doc,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: true,
|
||||
operation: 'update',
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
} catch (err) {
|
||||
payload.logger.error(`The global "${globalSlug}" has FAILED TO MIGRATE`)
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info(`The global "${globalSlug}" has migrated successfully!`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { tabHasName } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
doc: Record<string, unknown>
|
||||
fields: Field[]
|
||||
locale?: string
|
||||
path: string
|
||||
rows: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
const newPath = `${path ? `${path}.` : ''}${field.name}`
|
||||
const newDoc = doc?.[field.name]
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (field.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: field.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
return traverseFields({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const rowData = doc?.[field.name]
|
||||
|
||||
if (field.localized && typeof rowData === 'object' && rowData !== null) {
|
||||
Object.entries(rowData).forEach(([locale, localeRows]) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
locale,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData.forEach((row, i) => {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rowData = doc?.[field.name]
|
||||
|
||||
if (field.localized && typeof rowData === 'object' && rowData !== null) {
|
||||
Object.entries(rowData).forEach(([locale, localeRows]) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: matchedBlock.fields,
|
||||
locale,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
doc: row as Record<string, unknown>,
|
||||
fields: matchedBlock.fields,
|
||||
path: `${path ? `${path}.` : ''}${field.name}.${i}`,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
return field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
const newDoc = doc?.[tab.name]
|
||||
const newPath = `${path ? `${path}.` : ''}${tab.name}`
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (tab.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: tab.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: tab.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traverseFields({
|
||||
doc,
|
||||
fields: tab.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
const relationshipPath = `${path ? `${path}.` : ''}${field.name}`
|
||||
|
||||
if (field.localized) {
|
||||
const matchedRelationshipsWithLocales = rows.filter(
|
||||
(row) => row.path === relationshipPath,
|
||||
)
|
||||
|
||||
if (matchedRelationshipsWithLocales.length && !doc[field.name]) {
|
||||
doc[field.name] = {}
|
||||
}
|
||||
|
||||
const newDoc = doc[field.name] as Record<string, unknown>
|
||||
|
||||
matchedRelationshipsWithLocales.forEach((localeRow) => {
|
||||
if (typeof localeRow.locale === 'string') {
|
||||
const [, id] = Object.entries(localeRow).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key),
|
||||
)
|
||||
|
||||
newDoc[localeRow.locale] = id
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const matchedRelationship = rows.find((row) => {
|
||||
const matchesPath = row.path === relationshipPath
|
||||
|
||||
if (locale) return matchesPath && locale === row.locale
|
||||
|
||||
return row.path === relationshipPath
|
||||
})
|
||||
|
||||
if (matchedRelationship) {
|
||||
const [, id] = Object.entries(matchedRelationship).find(
|
||||
([key, val]) =>
|
||||
val !== null && !['id', 'locale', 'order', 'parent_id', 'path'].includes(key),
|
||||
)
|
||||
|
||||
doc[field.name] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
export type Groups =
|
||||
| 'addColumn'
|
||||
| 'addConstraint'
|
||||
| 'dropColumn'
|
||||
| 'dropConstraint'
|
||||
| 'dropTable'
|
||||
| 'notNull'
|
||||
|
||||
/**
|
||||
* Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement
|
||||
* example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;
|
||||
* to: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
* @param sql
|
||||
*/
|
||||
function convertAddColumnToAlterColumn(sql) {
|
||||
// Regular expression to match the ADD COLUMN statement with its constraints
|
||||
const regex = /ALTER TABLE ("[^"]+") ADD COLUMN ("[^"]+") [\w\s]+ NOT NULL;/
|
||||
|
||||
// Replace the matched part with "ALTER COLUMN ... SET NOT NULL;"
|
||||
return sql.replace(regex, 'ALTER TABLE $1 ALTER COLUMN $2 SET NOT NULL;')
|
||||
}
|
||||
|
||||
export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> => {
|
||||
const groups = {
|
||||
addColumn: 'ADD COLUMN',
|
||||
// example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
|
||||
|
||||
addConstraint: 'ADD CONSTRAINT',
|
||||
//example:
|
||||
// DO $$ BEGIN
|
||||
// ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
// EXCEPTION
|
||||
// WHEN duplicate_object THEN null;
|
||||
// END $$;
|
||||
|
||||
dropColumn: 'DROP COLUMN',
|
||||
// example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
|
||||
|
||||
dropConstraint: 'DROP CONSTRAINT',
|
||||
// example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
|
||||
|
||||
dropTable: 'DROP TABLE',
|
||||
// example: DROP TABLE "pages_rels";
|
||||
|
||||
notNull: 'NOT NULL',
|
||||
// example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
}
|
||||
|
||||
const result = Object.keys(groups).reduce((result, group: Groups) => {
|
||||
result[group] = []
|
||||
return result
|
||||
}, {}) as Record<Groups, string[]>
|
||||
|
||||
for (const line of list) {
|
||||
Object.entries(groups).some(([key, value]) => {
|
||||
if (line.endsWith('NOT NULL;')) {
|
||||
// split up the ADD COLUMN and ALTER COLUMN NOT NULL statements
|
||||
// example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;
|
||||
// becomes two separate statements:
|
||||
// 1. ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer;
|
||||
// 2. ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
|
||||
result.addColumn.push(line.replace(' NOT NULL;', ';'))
|
||||
result.notNull.push(convertAddColumnToAlterColumn(line))
|
||||
return true
|
||||
}
|
||||
if (line.includes(value)) {
|
||||
result[key].push(line)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import fs from 'fs'
|
||||
import { createRequire } from 'module'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../types.js'
|
||||
import type { PathsToQuery } from './types.js'
|
||||
|
||||
import { groupUpSQLStatements } from './groupUpSQLStatements.js'
|
||||
import { migrateRelationships } from './migrateRelationships.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
type Args = {
|
||||
debug?: boolean
|
||||
payload: Payload
|
||||
req?: Partial<PayloadRequest>
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves upload and relationship columns from the join table and into the tables while moving data
|
||||
* This is done in the following order:
|
||||
* ADD COLUMNs
|
||||
* -- manipulate data to move relationships to new columns
|
||||
* ADD CONSTRAINTs
|
||||
* NOT NULLs
|
||||
* DROP TABLEs
|
||||
* DROP CONSTRAINTs
|
||||
* DROP COLUMNs
|
||||
* @param debug
|
||||
* @param payload
|
||||
* @param req
|
||||
*/
|
||||
export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
|
||||
const adapter = payload.db as unknown as VercelPostgresAdapter
|
||||
const db = adapter.sessions[await req.transactionID].db as TransactionPg
|
||||
const dir = payload.db.migrationDir
|
||||
|
||||
// get the drizzle migrateUpSQL from drizzle using the last schema
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/api')
|
||||
const drizzleJsonAfter = generateDrizzleJson(adapter.schema)
|
||||
|
||||
// Get the previous migration snapshot
|
||||
const previousSnapshot = fs
|
||||
.readdirSync(dir)
|
||||
.filter((file) => file.endsWith('.json') && !file.endsWith('relationships_v2_v3.json'))
|
||||
.sort()
|
||||
.reverse()?.[0]
|
||||
|
||||
if (!previousSnapshot) {
|
||||
throw new Error(
|
||||
`No previous migration schema file found! A prior migration from v2 is required to migrate to v3.`,
|
||||
)
|
||||
}
|
||||
|
||||
const drizzleJsonBefore = JSON.parse(
|
||||
fs.readFileSync(`${dir}/${previousSnapshot}`, 'utf8'),
|
||||
) as DrizzleSnapshotJSON
|
||||
|
||||
const generatedSQL = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
|
||||
|
||||
if (!generatedSQL.length) {
|
||||
payload.logger.info(`No schema changes needed.`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const sqlUpStatements = groupUpSQLStatements(generatedSQL)
|
||||
|
||||
const addColumnsStatement = sqlUpStatements.addColumn.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS')
|
||||
payload.logger.info(addColumnsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(addColumnsStatement))
|
||||
|
||||
for (const collection of payload.config.collections) {
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const pathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: false,
|
||||
fields: collection.fields,
|
||||
isVersions: false,
|
||||
newTableName: tableName,
|
||||
parentTableName: tableName,
|
||||
path: '',
|
||||
pathsToQuery,
|
||||
payload,
|
||||
rootTableName: tableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
db,
|
||||
debug,
|
||||
fields: collection.fields,
|
||||
isVersions: false,
|
||||
pathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
|
||||
if (collection.versions) {
|
||||
const versionsTableName = adapter.tableNameMap.get(
|
||||
`_${toSnakeCase(collection.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
const versionFields = buildVersionCollectionFields(collection)
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: true,
|
||||
fields: versionFields,
|
||||
isVersions: true,
|
||||
newTableName: versionsTableName,
|
||||
parentTableName: versionsTableName,
|
||||
path: '',
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
rootTableName: versionsTableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
collectionSlug: collection.slug,
|
||||
db,
|
||||
debug,
|
||||
fields: versionFields,
|
||||
isVersions: true,
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName: versionsTableName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const global of payload.config.globals) {
|
||||
const tableName = adapter.tableNameMap.get(toSnakeCase(global.slug))
|
||||
|
||||
const pathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: false,
|
||||
fields: global.fields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: false,
|
||||
newTableName: tableName,
|
||||
parentTableName: tableName,
|
||||
path: '',
|
||||
pathsToQuery,
|
||||
payload,
|
||||
rootTableName: tableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
db,
|
||||
debug,
|
||||
fields: global.fields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: false,
|
||||
pathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
|
||||
if (global.versions) {
|
||||
const versionsTableName = adapter.tableNameMap.get(
|
||||
`_${toSnakeCase(global.slug)}${adapter.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const versionFields = buildVersionGlobalFields(global)
|
||||
|
||||
const versionPathsToQuery: PathsToQuery = new Set()
|
||||
|
||||
traverseFields({
|
||||
adapter,
|
||||
columnPrefix: '',
|
||||
db,
|
||||
disableNotNull: true,
|
||||
fields: versionFields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: true,
|
||||
newTableName: versionsTableName,
|
||||
parentTableName: versionsTableName,
|
||||
path: '',
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
rootTableName: versionsTableName,
|
||||
})
|
||||
|
||||
await migrateRelationships({
|
||||
adapter,
|
||||
db,
|
||||
debug,
|
||||
fields: versionFields,
|
||||
globalSlug: global.slug,
|
||||
isVersions: true,
|
||||
pathsToQuery: versionPathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName: versionsTableName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ADD CONSTRAINT
|
||||
const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('ADDING CONSTRAINTS')
|
||||
payload.logger.info(addConstraintsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(addConstraintsStatement))
|
||||
|
||||
// NOT NULL
|
||||
const notNullStatements = sqlUpStatements.notNull.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('NOT NULL CONSTRAINTS')
|
||||
payload.logger.info(notNullStatements)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(notNullStatements))
|
||||
|
||||
// DROP TABLE
|
||||
const dropTablesStatement = sqlUpStatements.dropTable.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING TABLES')
|
||||
payload.logger.info(dropTablesStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropTablesStatement))
|
||||
|
||||
// DROP CONSTRAINT
|
||||
const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING CONSTRAINTS')
|
||||
payload.logger.info(dropConstraintsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropConstraintsStatement))
|
||||
|
||||
// DROP COLUMN
|
||||
const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n')
|
||||
|
||||
if (debug) {
|
||||
payload.logger.info('DROPPING COLUMNS')
|
||||
payload.logger.info(dropColumnsStatement)
|
||||
}
|
||||
|
||||
await db.execute(sql.raw(dropColumnsStatement))
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { Field, Payload, PayloadRequest } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../types.js'
|
||||
import type { DocsToResave, PathsToQuery } from './types.js'
|
||||
|
||||
import { fetchAndResave } from './fetchAndResave/index.js'
|
||||
|
||||
type Args = {
|
||||
adapter: VercelPostgresAdapter
|
||||
collectionSlug?: string
|
||||
db: TransactionPg
|
||||
debug: boolean
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
isVersions: boolean
|
||||
pathsToQuery: PathsToQuery
|
||||
payload: Payload
|
||||
req?: Partial<PayloadRequest>
|
||||
tableName: string
|
||||
}
|
||||
|
||||
export const migrateRelationships = async ({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
db,
|
||||
debug,
|
||||
fields,
|
||||
globalSlug,
|
||||
isVersions,
|
||||
pathsToQuery,
|
||||
payload,
|
||||
req,
|
||||
tableName,
|
||||
}: Args) => {
|
||||
if (pathsToQuery.size === 0) return
|
||||
|
||||
let offset = 0
|
||||
|
||||
let paginationResult
|
||||
|
||||
const where = Array.from(pathsToQuery).reduce((statement, path, i) => {
|
||||
return (statement += `
|
||||
"${tableName}${adapter.relationshipsSuffix}"."path" LIKE '${path}'${pathsToQuery.size !== i + 1 ? ' OR' : ''}
|
||||
`)
|
||||
}, '')
|
||||
|
||||
while (typeof paginationResult === 'undefined' || paginationResult.rows.length > 0) {
|
||||
const paginationStatement = `SELECT DISTINCT parent_id FROM ${tableName}${adapter.relationshipsSuffix} WHERE
|
||||
${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500};
|
||||
`
|
||||
|
||||
paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`))
|
||||
|
||||
if (paginationResult.rows.length === 0) return
|
||||
|
||||
offset += 1
|
||||
|
||||
const statement = `SELECT * FROM ${tableName}${adapter.relationshipsSuffix} WHERE
|
||||
(${where}) AND parent_id IN (${paginationResult.rows.map((row) => row.parent_id).join(', ')});
|
||||
`
|
||||
if (debug) {
|
||||
payload.logger.info('FINDING ROWS TO MIGRATE')
|
||||
payload.logger.info(statement)
|
||||
}
|
||||
|
||||
const result = await adapter.drizzle.execute(sql.raw(`${statement}`))
|
||||
|
||||
const docsToResave: DocsToResave = {}
|
||||
|
||||
result.rows.forEach((row) => {
|
||||
const parentID = row.parent_id
|
||||
|
||||
if (typeof parentID === 'string' || typeof parentID === 'number') {
|
||||
if (!docsToResave[parentID]) docsToResave[parentID] = []
|
||||
docsToResave[parentID].push(row)
|
||||
}
|
||||
})
|
||||
|
||||
await fetchAndResave({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
db,
|
||||
debug,
|
||||
docsToResave,
|
||||
fields,
|
||||
globalSlug,
|
||||
isVersions,
|
||||
payload,
|
||||
req: req as unknown as PayloadRequest,
|
||||
tableName,
|
||||
})
|
||||
}
|
||||
|
||||
const deleteStatement = `DELETE FROM ${tableName}${adapter.relationshipsSuffix} WHERE ${where}`
|
||||
if (debug) {
|
||||
payload.logger.info('DELETING ROWS')
|
||||
payload.logger.info(deleteStatement)
|
||||
}
|
||||
await db.execute(sql.raw(`${deleteStatement}`))
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
import type { Field, Payload } from 'payload'
|
||||
|
||||
import { tabHasName } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { VercelPostgresAdapter } from '../../types.js'
|
||||
import type { PathsToQuery } from './types.js'
|
||||
|
||||
type Args = {
|
||||
adapter: VercelPostgresAdapter
|
||||
collectionSlug?: string
|
||||
columnPrefix: string
|
||||
db: TransactionPg
|
||||
disableNotNull: boolean
|
||||
fields: Field[]
|
||||
globalSlug?: string
|
||||
isVersions: boolean
|
||||
newTableName: string
|
||||
parentTableName: string
|
||||
path: string
|
||||
pathsToQuery: PathsToQuery
|
||||
payload: Payload
|
||||
rootTableName: string
|
||||
}
|
||||
|
||||
export const traverseFields = (args: Args) => {
|
||||
args.fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
|
||||
|
||||
if (field.localized && args.payload.config.localization) {
|
||||
newTableName += args.adapter.localesSuffix
|
||||
}
|
||||
|
||||
return traverseFields({
|
||||
...args,
|
||||
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
|
||||
fields: field.fields,
|
||||
newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
|
||||
})
|
||||
}
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
return traverseFields({
|
||||
...args,
|
||||
fields: field.fields,
|
||||
})
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const newTableName = args.adapter.tableNameMap.get(
|
||||
`${args.newTableName}_${toSnakeCase(field.name)}`,
|
||||
)
|
||||
|
||||
return traverseFields({
|
||||
...args,
|
||||
columnPrefix: '',
|
||||
fields: field.fields,
|
||||
newTableName,
|
||||
parentTableName: newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}.%`,
|
||||
})
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
return field.blocks.forEach((block) => {
|
||||
const newTableName = args.adapter.tableNameMap.get(
|
||||
`${args.rootTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
|
||||
traverseFields({
|
||||
...args,
|
||||
columnPrefix: '',
|
||||
fields: block.fields,
|
||||
newTableName,
|
||||
parentTableName: newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}.%`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
return field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
args.columnPrefix = `${args.columnPrefix}_${toSnakeCase(tab.name)}_`
|
||||
args.path = `${args.path ? `${args.path}.` : ''}${tab.name}`
|
||||
args.newTableName = `${args.newTableName}_${toSnakeCase(tab.name)}`
|
||||
|
||||
if (tab.localized && args.payload.config.localization) {
|
||||
args.newTableName += args.adapter.localesSuffix
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
...args,
|
||||
fields: tab.fields,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Set of all paths which should be moved
|
||||
* This will be built up into one WHERE query
|
||||
*/
|
||||
export type PathsToQuery = Set<string>
|
||||
|
||||
export type DocsToResave = {
|
||||
[id: number | string]: Record<string, unknown>[]
|
||||
}
|
||||
78
packages/db-vercel-postgres/src/types.ts
Normal file
78
packages/db-vercel-postgres/src/types.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
BasePostgresAdapter,
|
||||
GenericEnum,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
PostgresDB,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { VercelPool, VercelPostgresPoolConfig } from '@vercel/postgres'
|
||||
import type { DrizzleConfig } from 'drizzle-orm'
|
||||
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
|
||||
|
||||
export type Args = {
|
||||
connectionString?: string
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
migrationDir?: string
|
||||
/**
|
||||
* Optional pool configuration for Vercel Postgres
|
||||
* If not provided, vercel/postgres will attempt to use the Vercel environment variables
|
||||
*/
|
||||
pool?: VercelPostgresPoolConfig
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push?: boolean
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
* @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle.
|
||||
*/
|
||||
schemaName?: string
|
||||
transactionOptions?: PgTransactionConfig | false
|
||||
versionsSuffix?: string
|
||||
}
|
||||
|
||||
export type VercelPostgresAdapter = {
|
||||
pool?: VercelPool
|
||||
poolOptions?: Args['pool']
|
||||
} & BasePostgresAdapter
|
||||
|
||||
declare module 'payload' {
|
||||
export interface DatabaseAdapter
|
||||
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
|
||||
DrizzleAdapter {
|
||||
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
|
||||
drizzle: PostgresDB
|
||||
enums: Record<string, GenericEnum>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
*/
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
idType: Args['idType']
|
||||
initializing: Promise<void>
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pool: VercelPool
|
||||
poolOptions: Args['pool']
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relationshipsSuffix?: string
|
||||
resolveInitializing: () => void
|
||||
schema: Record<string, unknown>
|
||||
schemaName?: Args['schemaName']
|
||||
tableNameMap: Map<string, string>
|
||||
versionsSuffix?: string
|
||||
}
|
||||
}
|
||||
38
packages/db-vercel-postgres/tsconfig.json
Normal file
38
packages/db-vercel-postgres/tsconfig.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"tests",
|
||||
"test",
|
||||
"node_modules",
|
||||
"eslint.config.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": [
|
||||
"src",
|
||||
"src/**/*.ts",
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../payload"
|
||||
},
|
||||
{
|
||||
"path": "../translations"
|
||||
},
|
||||
{
|
||||
"path": "../drizzle"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,11 @@
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./postgres": {
|
||||
"import": "./src/exports/postgres.ts",
|
||||
"types": "./src/exports/postgres.ts",
|
||||
"default": "./src/exports/postgres.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
@@ -58,11 +63,18 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./postgres": {
|
||||
"import": "./dist/exports/postgres.js",
|
||||
"types": "./dist/exports/postgres.d.ts",
|
||||
"default": "./dist/exports/postgres.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts"
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
13
packages/drizzle/src/exports/postgres.ts
Normal file
13
packages/drizzle/src/exports/postgres.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export { countDistinct } from '../postgres/countDistinct.js'
|
||||
export { convertPathToJSONTraversal } from '../postgres/createJSONQuery/convertPathToJSONTraversal.js'
|
||||
export { createJSONQuery } from '../postgres/createJSONQuery/index.js'
|
||||
export { createMigration } from '../postgres/createMigration.js'
|
||||
export { defaultDrizzleSnapshot } from '../postgres/defaultSnapshot.js'
|
||||
export { deleteWhere } from '../postgres/deleteWhere.js'
|
||||
export { dropDatabase } from '../postgres/dropDatabase.js'
|
||||
export { execute } from '../postgres/execute.js'
|
||||
export { getMigrationTemplate } from '../postgres/getMigrationTemplate.js'
|
||||
export { init } from '../postgres/init.js'
|
||||
export { insert } from '../postgres/insert.js'
|
||||
export { requireDrizzleKit } from '../postgres/requireDrizzleKit.js'
|
||||
export * from '../postgres/types.js'
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { fieldAffectsData, tabHasName } from 'payload/shared'
|
||||
@@ -34,8 +33,9 @@ export const traverseFields = ({
|
||||
// handle simple relationship
|
||||
if (
|
||||
depth > 0 &&
|
||||
(field.type === 'upload' ||
|
||||
(field.type === 'relationship' && !field.hasMany && typeof field.relationTo === 'string'))
|
||||
(field.type === 'upload' || field.type === 'relationship') &&
|
||||
!field.hasMany &&
|
||||
typeof field.relationTo === 'string'
|
||||
) {
|
||||
if (field.localized) {
|
||||
_locales.with[`${path}${field.name}`] = true
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ChainedMethods, TransactionPg } from '@payloadcms/drizzle/types'
|
||||
|
||||
import { chainMethods } from '@payloadcms/drizzle'
|
||||
import { sql } from 'drizzle-orm'
|
||||
|
||||
import type { CountDistinct, PostgresAdapter } from './types.js'
|
||||
import type { ChainedMethods, TransactionPg } from '../types.js'
|
||||
import type { BasePostgresAdapter, CountDistinct } from './types.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
|
||||
export const countDistinct: CountDistinct = async function countDistinct(
|
||||
this: PostgresAdapter,
|
||||
this: BasePostgresAdapter,
|
||||
{ db, joins, tableName, where },
|
||||
) {
|
||||
const chainedMethods: ChainedMethods = []
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { CreateMigration } from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
@@ -8,7 +7,7 @@ import { getPredefinedMigration, writeMigrationIndex } from 'payload'
|
||||
import prompts from 'prompts'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
import type { BasePostgresAdapter } from './types.js'
|
||||
|
||||
import { defaultDrizzleSnapshot } from './defaultSnapshot.js'
|
||||
import { getMigrationTemplate } from './getMigrationTemplate.js'
|
||||
@@ -16,7 +15,7 @@ import { getMigrationTemplate } from './getMigrationTemplate.js'
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
export const createMigration: CreateMigration = async function createMigration(
|
||||
this: PostgresAdapter,
|
||||
this: BasePostgresAdapter,
|
||||
{ file, forceAcceptWarning, migrationName, payload },
|
||||
) {
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
@@ -112,6 +111,7 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
getMigrationTemplate({
|
||||
downSQL: downSQL || ` // Migration code`,
|
||||
imports,
|
||||
packageName: payload.db.packageName,
|
||||
upSQL: upSQL || ` // Migration code`,
|
||||
}),
|
||||
)
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
|
||||
import type { TransactionPg } from '../types.js'
|
||||
import type { DeleteWhere } from './types.js'
|
||||
|
||||
export const deleteWhere: DeleteWhere = async function deleteWhere({ db, tableName, where }) {
|
||||
@@ -9,8 +9,9 @@ export const indent = (text: string) =>
|
||||
export const getMigrationTemplate = ({
|
||||
downSQL,
|
||||
imports,
|
||||
packageName,
|
||||
upSQL,
|
||||
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '${packageName}'
|
||||
${imports ? `${imports}\n` : ''}
|
||||
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
|
||||
${indent(upSQL)}
|
||||
@@ -1,16 +1,15 @@
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { createTableName } from '@payloadcms/drizzle'
|
||||
import { uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { BaseExtraConfig } from './schema/build.js'
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
import type { BaseExtraConfig, BasePostgresAdapter } from './types.js'
|
||||
|
||||
import { createTableName } from '../createTableName.js'
|
||||
import { buildTable } from './schema/build.js'
|
||||
|
||||
export const init: Init = function init(this: PostgresAdapter) {
|
||||
export const init: Init = function init(this: BasePostgresAdapter) {
|
||||
if (this.payload.config.localization) {
|
||||
this.enums.enum__locales = this.pgSchema.enum(
|
||||
'_locales',
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { TransactionPg } from '@payloadcms/drizzle/types'
|
||||
|
||||
import type { TransactionPg } from '../types.js'
|
||||
import type { Insert } from './types.js'
|
||||
|
||||
export const insert: Insert = async function insert({
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RequireDrizzleKit } from '@payloadcms/drizzle/types'
|
||||
|
||||
import { createRequire } from 'module'
|
||||
|
||||
import type { RequireDrizzleKit } from '../types.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
export const requireDrizzleKit: RequireDrizzleKit = () => require('drizzle-kit/api')
|
||||
@@ -4,11 +4,9 @@ import type {
|
||||
IndexBuilder,
|
||||
PgColumnBuilder,
|
||||
PgTableWithColumns,
|
||||
UniqueConstraintBuilder,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { createTableName } from '@payloadcms/drizzle'
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
foreignKey,
|
||||
@@ -22,21 +20,22 @@ import {
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
|
||||
import type {
|
||||
BaseExtraConfig,
|
||||
BasePostgresAdapter,
|
||||
GenericColumns,
|
||||
GenericTable,
|
||||
IDType,
|
||||
RelationMap,
|
||||
} from '../types.js'
|
||||
|
||||
import { createTableName } from '../../createTableName.js'
|
||||
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
||||
import { setColumnID } from './setColumnID.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
export type BaseExtraConfig = Record<
|
||||
string,
|
||||
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
|
||||
>
|
||||
|
||||
export type RelationMap = Map<string, { localized: boolean; target: string; type: 'many' | 'one' }>
|
||||
|
||||
type Args = {
|
||||
adapter: PostgresAdapter
|
||||
adapter: BasePostgresAdapter
|
||||
baseColumns?: Record<string, PgColumnBuilder>
|
||||
/**
|
||||
* After table is created, run these functions to add extra config to the table
|
||||
@@ -4,9 +4,13 @@ import { numeric, serial, uuid, varchar } from 'drizzle-orm/pg-core'
|
||||
import { type Field, flattenTopLevelFields } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
|
||||
import type { IDType, PostgresAdapter } from '../types.js'
|
||||
import type { BasePostgresAdapter, IDType } from '../types.js'
|
||||
|
||||
type Args = { adapter: PostgresAdapter; columns: Record<string, PgColumnBuilder>; fields: Field[] }
|
||||
type Args = {
|
||||
adapter: BasePostgresAdapter
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
fields: Field[]
|
||||
}
|
||||
export const setColumnID = ({ adapter, columns, fields }: Args): IDType => {
|
||||
const idField = flattenTopLevelFields(fields).find(
|
||||
(field) => fieldAffectsData(field) && field.name === 'id',
|
||||
@@ -2,11 +2,6 @@ import type { Relation } from 'drizzle-orm'
|
||||
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
|
||||
import type { Field, TabAsField } from 'payload'
|
||||
|
||||
import {
|
||||
createTableName,
|
||||
hasLocalesTable,
|
||||
validateExistingBlockIsIdentical,
|
||||
} from '@payloadcms/drizzle'
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
PgNumericBuilder,
|
||||
@@ -26,9 +21,17 @@ import { InvalidConfiguration } from 'payload'
|
||||
import { fieldAffectsData, optionIsObject } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
|
||||
import type { BaseExtraConfig, RelationMap } from './build.js'
|
||||
import type {
|
||||
BaseExtraConfig,
|
||||
BasePostgresAdapter,
|
||||
GenericColumns,
|
||||
IDType,
|
||||
RelationMap,
|
||||
} from '../types.js'
|
||||
|
||||
import { createTableName } from '../../createTableName.js'
|
||||
import { hasLocalesTable } from '../../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { buildTable } from './build.js'
|
||||
import { createIndex } from './createIndex.js'
|
||||
import { idToUUID } from './idToUUID.js'
|
||||
@@ -36,7 +39,7 @@ import { parentIDColumnMap } from './parentIDColumnMap.js'
|
||||
import { withDefault } from './withDefault.js'
|
||||
|
||||
type Args = {
|
||||
adapter: PostgresAdapter
|
||||
adapter: BasePostgresAdapter
|
||||
columnPrefix?: string
|
||||
columns: Record<string, PgColumnBuilder>
|
||||
disableNotNull: boolean
|
||||
@@ -723,7 +726,7 @@ export const traverseFields = ({
|
||||
case 'upload':
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
field.relationTo.forEach((relation) => relationships.add(relation))
|
||||
} else if (field.type === 'relationship' && field.hasMany) {
|
||||
} else if (field.hasMany) {
|
||||
relationships.add(field.relationTo)
|
||||
} else {
|
||||
// simple relationships get a column on the targetTable with a foreign key to the relationTo table
|
||||
152
packages/drizzle/src/postgres/types.ts
Normal file
152
packages/drizzle/src/postgres/types.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type {
|
||||
ColumnBaseConfig,
|
||||
ColumnDataType,
|
||||
DrizzleConfig,
|
||||
Relation,
|
||||
Relations,
|
||||
SQL,
|
||||
} from 'drizzle-orm'
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
import type {
|
||||
ForeignKeyBuilder,
|
||||
IndexBuilder,
|
||||
PgColumn,
|
||||
PgEnum,
|
||||
PgInsertOnConflictDoUpdateConfig,
|
||||
PgSchema,
|
||||
PgTableWithColumns,
|
||||
UniqueConstraintBuilder,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
import type { QueryResult } from 'pg'
|
||||
|
||||
import type { Operators } from '../index.js'
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter, TransactionPg } from '../types.js'
|
||||
|
||||
export type BaseExtraConfig = Record<
|
||||
string,
|
||||
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
|
||||
>
|
||||
|
||||
export type RelationMap = Map<string, { localized: boolean; target: string; type: 'many' | 'one' }>
|
||||
|
||||
export type GenericColumn = PgColumn<
|
||||
ColumnBaseConfig<ColumnDataType, string>,
|
||||
Record<string, unknown>
|
||||
>
|
||||
|
||||
export type GenericColumns = {
|
||||
[x: string]: GenericColumn
|
||||
}
|
||||
|
||||
export type GenericTable = PgTableWithColumns<{
|
||||
columns: GenericColumns
|
||||
dialect: string
|
||||
name: string
|
||||
schema: string
|
||||
}>
|
||||
|
||||
export type GenericEnum = PgEnum<[string, ...string[]]>
|
||||
|
||||
export type GenericRelation = Relations<string, Record<string, Relation<string>>>
|
||||
|
||||
export type PostgresDB = NodePgDatabase<Record<string, unknown>>
|
||||
|
||||
export type CountDistinct = (args: {
|
||||
db: PostgresDB | TransactionPg
|
||||
joins: BuildQueryJoinAliases
|
||||
tableName: string
|
||||
where: SQL
|
||||
}) => Promise<number>
|
||||
|
||||
export type DeleteWhere = (args: {
|
||||
db: PostgresDB | TransactionPg
|
||||
tableName: string
|
||||
where: SQL
|
||||
}) => Promise<void>
|
||||
|
||||
export type DropDatabase = (args: { adapter: BasePostgresAdapter }) => Promise<void>
|
||||
|
||||
export type Execute<T> = (args: {
|
||||
db?: PostgresDB | TransactionPg
|
||||
drizzle?: PostgresDB
|
||||
raw?: string
|
||||
sql?: SQL<unknown>
|
||||
}) => Promise<QueryResult<Record<string, T>>>
|
||||
|
||||
export type Insert = (args: {
|
||||
db: PostgresDB | TransactionPg
|
||||
onConflictDoUpdate?: PgInsertOnConflictDoUpdateConfig<any>
|
||||
tableName: string
|
||||
values: Record<string, unknown> | Record<string, unknown>[]
|
||||
}) => Promise<Record<string, unknown>[]>
|
||||
|
||||
type Schema =
|
||||
| {
|
||||
enum: typeof pgEnum
|
||||
table: PgTableFn
|
||||
}
|
||||
| PgSchema
|
||||
|
||||
export type BasePostgresAdapter = {
|
||||
countDistinct: CountDistinct
|
||||
defaultDrizzleSnapshot: DrizzleSnapshotJSON
|
||||
deleteWhere: DeleteWhere
|
||||
drizzle: PostgresDB
|
||||
dropDatabase: DropDatabase
|
||||
enums: Record<string, GenericEnum>
|
||||
execute: Execute<unknown>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
*/
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
idType: 'serial' | 'uuid'
|
||||
initializing: Promise<void>
|
||||
insert: Insert
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
operators: Operators
|
||||
pgSchema?: Schema
|
||||
prodMigrations?: {
|
||||
down: (args: MigrateDownArgs) => Promise<void>
|
||||
name: string
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
rejectInitializing: () => void
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
resolveInitializing: () => void
|
||||
schemaName?: string
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
db: PostgresDB | TransactionPg
|
||||
reject: () => Promise<void>
|
||||
resolve: () => Promise<void>
|
||||
}
|
||||
}
|
||||
tableNameMap: Map<string, string>
|
||||
tables: Record<string, GenericTable>
|
||||
versionsSuffix?: string
|
||||
} & PostgresDrizzleAdapter
|
||||
|
||||
export type PostgresDrizzleAdapter = Omit<
|
||||
DrizzleAdapter,
|
||||
| 'countDistinct'
|
||||
| 'deleteWhere'
|
||||
| 'drizzle'
|
||||
| 'dropDatabase'
|
||||
| 'execute'
|
||||
| 'insert'
|
||||
| 'operators'
|
||||
| 'relations'
|
||||
>
|
||||
|
||||
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
|
||||
|
||||
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
|
||||
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
|
||||
@@ -445,7 +445,7 @@ export const getTableColumnFromPath = ({
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
||||
if (Array.isArray(field.relationTo) || (field.type === 'relationship' && field.hasMany)) {
|
||||
if (Array.isArray(field.relationTo) || field.hasMany) {
|
||||
let relationshipFields
|
||||
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
|
||||
const {
|
||||
|
||||
@@ -351,6 +351,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
throw error.code === '23505'
|
||||
? new ValidationError(
|
||||
{
|
||||
id,
|
||||
errors: [
|
||||
{
|
||||
field: adapter.fieldConstraints[tableName][error.constraint],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
67
packages/eslint-plugin/customRules/no-imports-from-self.js
Normal file
67
packages/eslint-plugin/customRules/no-imports-from-self.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export const rule = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow a package from importing from itself',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let packageName = null
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value
|
||||
const pkgName = getPackageName(context, packageName)
|
||||
if (pkgName && importPath.startsWith(pkgName)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Package "${pkgName}" should not import from itself. Use relative instead.`,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default rule
|
||||
|
||||
/**
|
||||
* @param {import('eslint').Rule.RuleContext} context
|
||||
* @param {string|undefined} packageName
|
||||
*/
|
||||
function getPackageName(context, packageName) {
|
||||
if (packageName) {
|
||||
return packageName
|
||||
}
|
||||
|
||||
const fileName = context.getFilename()
|
||||
const pkg = findNearestPackageJson(path.dirname(fileName))
|
||||
if (pkg) {
|
||||
return pkg.name
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} startDir
|
||||
*/
|
||||
function findNearestPackageJson(startDir) {
|
||||
let currentDir = startDir
|
||||
while (currentDir !== path.dirname(currentDir)) {
|
||||
// Root directory check
|
||||
const pkgPath = path.join(currentDir, 'package.json')
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const pkgContent = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||||
return pkgContent
|
||||
}
|
||||
currentDir = path.dirname(currentDir)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const rule = {
|
||||
const importPath = node.source.value
|
||||
|
||||
// Match imports starting with any number of "../" followed by "packages/"
|
||||
const regex = /^(\.\.\/)*packages\/[^/]+\/src/
|
||||
const regex = /^(\.\.\/)*((?!src\b)\w+\/)+src\//
|
||||
|
||||
if (regex.test(importPath)) {
|
||||
context.report({
|
||||
|
||||
@@ -3,6 +3,7 @@ import noNonRetryableAssertions from './customRules/no-non-retryable-assertions.
|
||||
import noRelativeMonorepoImports from './customRules/no-relative-monorepo-imports.js'
|
||||
import noImportsFromExportsDir from './customRules/no-imports-from-exports-dir.js'
|
||||
import noFlakyAssertions from './customRules/no-flaky-assertions.js'
|
||||
import noImportsFromSelf from './customRules/no-imports-from-self.js'
|
||||
|
||||
/**
|
||||
* @type {import('eslint').ESLint.Plugin}
|
||||
@@ -13,6 +14,7 @@ const index = {
|
||||
'no-non-retryable-assertions': noNonRetryableAssertions,
|
||||
'no-relative-monorepo-imports': noRelativeMonorepoImports,
|
||||
'no-imports-from-exports-dir': noImportsFromExportsDir,
|
||||
'no-imports-from-self': noImportsFromSelf,
|
||||
'no-flaky-assertions': noFlakyAssertions,
|
||||
'no-wait-function': {
|
||||
create: function (context) {
|
||||
|
||||
@@ -1,21 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { register } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
// Allow disabling SWC for debugging
|
||||
if (process.env.DISABLE_SWC !== 'true') {
|
||||
const useSwc = process.argv.includes('--use-swc')
|
||||
const disableTranspile = process.argv.includes('--disable-transpile')
|
||||
|
||||
if (disableTranspile) {
|
||||
// Remove --disable-transpile from arguments
|
||||
process.argv = process.argv.filter((arg) => arg !== '--disable-transpile')
|
||||
|
||||
const start = async () => {
|
||||
const { bin } = await import('./dist/bin/index.js')
|
||||
await bin()
|
||||
}
|
||||
|
||||
void start()
|
||||
} else {
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const url = pathToFileURL(dirname).toString() + '/'
|
||||
|
||||
register('@swc-node/register/esm', url)
|
||||
}
|
||||
if (!useSwc) {
|
||||
const start = async () => {
|
||||
// Use tsx
|
||||
let tsImport = (await import('tsx/esm/api')).tsImport
|
||||
|
||||
const start = async () => {
|
||||
const { bin } = await import('./dist/bin/index.js')
|
||||
await bin()
|
||||
}
|
||||
const { bin } = await tsImport('./dist/bin/index.js', url)
|
||||
await bin()
|
||||
}
|
||||
|
||||
void start()
|
||||
void start()
|
||||
} else if (useSwc) {
|
||||
const { register } = await import('node:module')
|
||||
// Remove --use-swc from arguments
|
||||
process.argv = process.argv.filter((arg) => arg !== '--use-swc')
|
||||
|
||||
try {
|
||||
register('@swc-node/register/esm', url)
|
||||
} catch (_) {
|
||||
console.error(
|
||||
'@swc-node/register is not installed. Please install @swc-node/register in your project, if you want to use swc in payload run.',
|
||||
)
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
const { bin } = await import('./dist/bin/index.js')
|
||||
await bin()
|
||||
}
|
||||
|
||||
void start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -43,8 +43,8 @@
|
||||
"dependencies": {
|
||||
"graphql-scalars": "1.22.2",
|
||||
"pluralize": "8.0.0",
|
||||
"@swc-node/register": "1.10.9",
|
||||
"ts-essentials": "7.0.3"
|
||||
"ts-essentials": "7.0.3",
|
||||
"tsx": "4.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
|
||||
@@ -307,10 +307,51 @@ export function buildMutationInputType({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
|
||||
}),
|
||||
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: withNullableType(field, GraphQLString, forceNullable) },
|
||||
}),
|
||||
upload: (inputObjectTypeConfig: InputObjectTypeConfig, field: UploadField) => {
|
||||
const { relationTo } = field
|
||||
type PayloadGraphQLRelationshipType =
|
||||
| GraphQLInputObjectType
|
||||
| GraphQLList<GraphQLScalarType>
|
||||
| GraphQLScalarType
|
||||
let type: PayloadGraphQLRelationshipType
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
const fullName = `${combineParentName(
|
||||
parentName,
|
||||
toWords(field.name, true),
|
||||
)}RelationshipInput`
|
||||
type = new GraphQLInputObjectType({
|
||||
name: fullName,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: new GraphQLEnumType({
|
||||
name: `${fullName}RelationTo`,
|
||||
values: relationTo.reduce(
|
||||
(values, option) => ({
|
||||
...values,
|
||||
[formatName(option)]: {
|
||||
value: option,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
},
|
||||
value: { type: GraphQLJSON },
|
||||
},
|
||||
})
|
||||
} else {
|
||||
type = getCollectionIDType(
|
||||
config.db.defaultIDType,
|
||||
graphqlResult.collections[relationTo].config,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const fieldName = formatName(name)
|
||||
|
||||
@@ -594,49 +594,164 @@ export function buildObjectType({
|
||||
}),
|
||||
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
|
||||
const { relationTo } = field
|
||||
const isRelatedToManyCollections = Array.isArray(relationTo)
|
||||
const hasManyValues = field.hasMany
|
||||
const relationshipName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
const uploadName = combineParentName(parentName, toWords(field.name, true))
|
||||
let type
|
||||
let relationToType = null
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
relationToType = new GraphQLEnumType({
|
||||
name: `${relationshipName}_RelationTo`,
|
||||
values: relationTo.reduce(
|
||||
(relations, relation) => ({
|
||||
...relations,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
})
|
||||
|
||||
const types = relationTo.map((relation) => graphqlResult.collections[relation].graphQL.type)
|
||||
|
||||
type = new GraphQLObjectType({
|
||||
name: `${relationshipName}_Relationship`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: relationToType,
|
||||
},
|
||||
value: {
|
||||
type: new GraphQLUnionType({
|
||||
name: relationshipName,
|
||||
resolveType(data, { req }) {
|
||||
return graphqlResult.collections[data.collection].graphQL.type.name
|
||||
},
|
||||
types,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
;({ type } = graphqlResult.collections[relationTo].graphQL)
|
||||
}
|
||||
|
||||
// If the relationshipType is undefined at this point,
|
||||
// it can be assumed that this blockType can have a relationship
|
||||
// to itself. Therefore, we set the relationshipType equal to the blockType
|
||||
// that is currently being created.
|
||||
|
||||
const type = withNullableType(
|
||||
field,
|
||||
graphqlResult.collections[relationTo].graphQL.type || newlyCreatedBlockType,
|
||||
forceNullable,
|
||||
type = type || newlyCreatedBlockType
|
||||
|
||||
const relationshipArgs: {
|
||||
draft?: unknown
|
||||
fallbackLocale?: unknown
|
||||
limit?: unknown
|
||||
locale?: unknown
|
||||
page?: unknown
|
||||
where?: unknown
|
||||
} = {}
|
||||
|
||||
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo]).some(
|
||||
(relation) => graphqlResult.collections[relation].config.versions?.drafts,
|
||||
)
|
||||
|
||||
const uploadArgs = {} as LocaleInputType
|
||||
if (relationsUseDrafts) {
|
||||
relationshipArgs.draft = {
|
||||
type: GraphQLBoolean,
|
||||
}
|
||||
}
|
||||
|
||||
if (config.localization) {
|
||||
uploadArgs.locale = {
|
||||
relationshipArgs.locale = {
|
||||
type: graphqlResult.types.localeInputType,
|
||||
}
|
||||
|
||||
uploadArgs.fallbackLocale = {
|
||||
relationshipArgs.fallbackLocale = {
|
||||
type: graphqlResult.types.fallbackLocaleInputType,
|
||||
}
|
||||
}
|
||||
|
||||
const relatedCollectionSlug = field.relationTo
|
||||
|
||||
const upload = {
|
||||
type,
|
||||
args: uploadArgs,
|
||||
extensions: { complexity: 20 },
|
||||
const relationship = {
|
||||
type: withNullableType(
|
||||
field,
|
||||
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
forceNullable,
|
||||
),
|
||||
args: relationshipArgs,
|
||||
extensions: { complexity: 10 },
|
||||
async resolve(parent, args, context: Context) {
|
||||
const value = parent[field.name]
|
||||
const locale = args.locale || context.req.locale
|
||||
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
||||
const id = value
|
||||
let relatedCollectionSlug = field.relationTo
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
if (hasManyValues) {
|
||||
const results = []
|
||||
const resultPromises = []
|
||||
|
||||
const createPopulationPromise = async (relatedDoc, i) => {
|
||||
let id = relatedDoc
|
||||
let collectionSlug = field.relationTo
|
||||
|
||||
if (isRelatedToManyCollections) {
|
||||
collectionSlug = relatedDoc.relationTo
|
||||
id = relatedDoc.value
|
||||
}
|
||||
|
||||
const result = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: collectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (result) {
|
||||
if (isRelatedToManyCollections) {
|
||||
results[i] = {
|
||||
relationTo: collectionSlug,
|
||||
value: {
|
||||
...result,
|
||||
collection: collectionSlug,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
results[i] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
value.forEach((relatedDoc, i) => {
|
||||
resultPromises.push(createPopulationPromise(relatedDoc, i))
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(resultPromises)
|
||||
return results
|
||||
}
|
||||
|
||||
let id = value
|
||||
if (isRelatedToManyCollections && value) {
|
||||
id = value.value
|
||||
relatedCollectionSlug = value.relationTo
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const relatedDocument = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: relatedCollectionSlug,
|
||||
collectionSlug: relatedCollectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
@@ -649,26 +764,30 @@ export function buildObjectType({
|
||||
}),
|
||||
)
|
||||
|
||||
return relatedDocument || null
|
||||
if (relatedDocument) {
|
||||
if (isRelatedToManyCollections) {
|
||||
return {
|
||||
relationTo: relatedCollectionSlug,
|
||||
value: {
|
||||
...relatedDocument,
|
||||
collection: relatedCollectionSlug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return relatedDocument
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
const whereFields = graphqlResult.collections[relationTo].config.fields
|
||||
|
||||
upload.args.where = {
|
||||
type: buildWhereInputType({
|
||||
name: uploadName,
|
||||
fields: whereFields,
|
||||
parentName: uploadName,
|
||||
}),
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: upload,
|
||||
[field.name]: relationship,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,9 +130,36 @@ const fieldToSchemaMap = ({ nestedFieldName, parentName }: Args): any => ({
|
||||
textarea: (field: TextareaField) => ({
|
||||
type: withOperators(field, parentName),
|
||||
}),
|
||||
upload: (field: UploadField) => ({
|
||||
type: withOperators(field, parentName),
|
||||
}),
|
||||
upload: (field: UploadField) => {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
return {
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${combineParentName(parentName, field.name)}_Relation`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: new GraphQLEnumType({
|
||||
name: `${combineParentName(parentName, field.name)}_Relation_RelationTo`,
|
||||
values: field.relationTo.reduce(
|
||||
(values, relation) => ({
|
||||
...values,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
},
|
||||
value: { type: GraphQLJSON },
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: withOperators(field, parentName),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default fieldToSchemaMap
|
||||
|
||||
@@ -230,9 +230,9 @@ const defaults: DefaultsType = {
|
||||
},
|
||||
upload: {
|
||||
operators: [
|
||||
...operators.equality.map((operator) => ({
|
||||
...[...operators.equality, ...operators.contains].map((operator) => ({
|
||||
name: operator,
|
||||
type: GraphQLString,
|
||||
type: GraphQLJSON,
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.79",
|
||||
"version": "3.0.0-beta.90",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import { isPlainObject } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { ShouldRenderTabs } from './ShouldRenderTabs.js'
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
.doc-header {
|
||||
width: 100%;
|
||||
margin-top: base(0.5);
|
||||
padding-bottom: calc(var(--base) * 1.5);
|
||||
margin-top: base(0.4);
|
||||
padding-bottom: calc(var(--base) * 1.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
@@ -27,6 +27,9 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
padding-bottom: base(0.4);
|
||||
line-height: 1;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
|
||||
@@ -50,6 +50,7 @@ const Component: React.FC<{
|
||||
}
|
||||
|
||||
export const LeaveWithoutSaving: React.FC = () => {
|
||||
const { closeModal } = useModal()
|
||||
const modified = useFormModified()
|
||||
const { user } = useAuth()
|
||||
const [show, setShow] = React.useState(false)
|
||||
@@ -61,7 +62,11 @@ export const LeaveWithoutSaving: React.FC = () => {
|
||||
setShow(true)
|
||||
}, [])
|
||||
|
||||
usePreventLeave({ hasAccepted, onPrevent, prevent })
|
||||
const handleAccept = useCallback(() => {
|
||||
closeModal(modalSlug)
|
||||
}, [closeModal])
|
||||
|
||||
usePreventLeave({ hasAccepted, onAccept: handleAccept, onPrevent, prevent })
|
||||
|
||||
return (
|
||||
<Component
|
||||
|
||||
@@ -57,12 +57,14 @@ export const useBeforeUnload = (enabled: (() => boolean) | boolean = true, messa
|
||||
export const usePreventLeave = ({
|
||||
hasAccepted = false,
|
||||
message = 'Are you sure want to leave this page?',
|
||||
onAccept,
|
||||
onPrevent,
|
||||
prevent = true,
|
||||
}: {
|
||||
hasAccepted: boolean
|
||||
// if no `onPrevent` is provided, the message will be displayed in a confirm dialog
|
||||
message?: string
|
||||
onAccept?: () => void
|
||||
// to use a custom confirmation dialog, provide a function that returns a boolean
|
||||
onPrevent?: () => void
|
||||
prevent: boolean
|
||||
@@ -142,7 +144,8 @@ export const usePreventLeave = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAccepted && cancelledURL.current) {
|
||||
if (onAccept) onAccept()
|
||||
router.push(cancelledURL.current)
|
||||
}
|
||||
}, [hasAccepted, router])
|
||||
}, [hasAccepted, onAccept, router])
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
width: var(--nav-width);
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
opacity: 0;
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
|
||||
&--nav-animate {
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
}
|
||||
|
||||
&--nav-open {
|
||||
opacity: 1;
|
||||
|
||||
@@ -10,10 +10,19 @@ export const NavWrapper: React.FC<{
|
||||
}> = (props) => {
|
||||
const { baseClass, children } = props
|
||||
|
||||
const { navOpen, navRef } = useNav()
|
||||
const { hydrated, navOpen, navRef, shouldAnimate } = useNav()
|
||||
|
||||
return (
|
||||
<aside className={[baseClass, navOpen && `${baseClass}--nav-open`].filter(Boolean).join(' ')}>
|
||||
<aside
|
||||
className={[
|
||||
baseClass,
|
||||
navOpen && `${baseClass}--nav-open`,
|
||||
shouldAnimate && `${baseClass}--nav-animate`,
|
||||
hydrated && `${baseClass}--nav-hydrated`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__scroll`} ref={navRef}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user