Compare commits
160 Commits
fix/locali
...
chore/sani
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6be26f6dd5 | ||
|
|
a44e4c46c5 | ||
|
|
57f4fb6cfe | ||
|
|
fcaf9893bd | ||
|
|
dede3a4759 | ||
|
|
7a0308fb9b | ||
|
|
6c4dfe45e6 | ||
|
|
a5ec55c02a | ||
|
|
11ac230905 | ||
|
|
9d5f801488 | ||
|
|
4831f66f63 | ||
|
|
85e0e0ea1e | ||
|
|
5f6fad69fb | ||
|
|
25e3902242 | ||
|
|
59f536c2c9 | ||
|
|
dffdee89d8 | ||
|
|
9c5adba5c6 | ||
|
|
d1826c647f | ||
|
|
84cb2b5819 | ||
|
|
810869f3fa | ||
|
|
215f49efa5 | ||
|
|
704518248c | ||
|
|
b372a34ebf | ||
|
|
769ca03bff | ||
|
|
4e2e4d2aed | ||
|
|
9943b3508d | ||
|
|
8235fe137f | ||
|
|
f2e04222f4 | ||
|
|
3edcc40174 | ||
|
|
e60db0750a | ||
|
|
06ad17108b | ||
|
|
65309b1d21 | ||
|
|
245a2dee7e | ||
|
|
7fef589ffa | ||
|
|
53835f2620 | ||
|
|
729b676e98 | ||
|
|
77f380544f | ||
|
|
df8be92d47 | ||
|
|
e7b5884ec2 | ||
|
|
d0e647a992 | ||
|
|
9364d51f4b | ||
|
|
b556fe3daf | ||
|
|
c04c257712 | ||
|
|
f64a0aec5f | ||
|
|
143aff57ae | ||
|
|
cf43c5cd08 | ||
|
|
67fb29b2a4 | ||
|
|
018317dfba | ||
|
|
37afbe6c04 | ||
|
|
458a04b77c | ||
|
|
d8626adc3b | ||
|
|
a19921d08f | ||
|
|
860e0b4ff9 | ||
|
|
08d5b2b79d | ||
|
|
cb3f9bb3e9 | ||
|
|
192cc97f6e | ||
|
|
3313ab7e75 | ||
|
|
c4e5831fbb | ||
|
|
a43d1a685f | ||
|
|
9d2817e647 | ||
|
|
254ffecaea | ||
|
|
38652d7b7c | ||
|
|
08fbcb5c29 | ||
|
|
53f8838830 | ||
|
|
96f417bee5 | ||
|
|
ebe7fdea27 | ||
|
|
0a357372e9 | ||
|
|
865a9cd9d1 | ||
|
|
4ac1894cbe | ||
|
|
ff615d3fa8 | ||
|
|
9fbc3f6453 | ||
|
|
afbdf3da76 | ||
|
|
8f4c4423f3 | ||
|
|
773e4ad4dd | ||
|
|
c6659db9bd | ||
|
|
3d177fd935 | ||
|
|
678bfc7d93 | ||
|
|
7b21270959 | ||
|
|
34fe36b56d | ||
|
|
6f76d724dc | ||
|
|
3f670ca1a2 | ||
|
|
f80dc5b99e | ||
|
|
aef4f779b1 | ||
|
|
6466684ede | ||
|
|
7c05c775cb | ||
|
|
8d7dbe6c56 | ||
|
|
629e74d693 | ||
|
|
4ee4aa7d71 | ||
|
|
e10e445a64 | ||
|
|
6f82154ce4 | ||
|
|
a10c3a5ba3 | ||
|
|
1dd4a123ab | ||
|
|
3f7debd224 | ||
|
|
5635ec513e | ||
|
|
53de775603 | ||
|
|
fd5cd1a4f5 | ||
|
|
48e5ee6aa1 | ||
|
|
0c3ff88e76 | ||
|
|
be52a203a3 | ||
|
|
9581092995 | ||
|
|
545d870650 | ||
|
|
337f6188da | ||
|
|
48218bccb5 | ||
|
|
bd512f1eda | ||
|
|
c08cdff498 | ||
|
|
cbc37d84bd | ||
|
|
76bf459ff2 | ||
|
|
30bb749e25 | ||
|
|
2bd098c9ea | ||
|
|
08ec837339 | ||
|
|
505eaa2bba | ||
|
|
6ec21a53ff | ||
|
|
625d8d9319 | ||
|
|
a9ff375cc0 | ||
|
|
0ceb96b12d | ||
|
|
319d3355de | ||
|
|
2b40e0f21f | ||
|
|
30dd9a23a3 | ||
|
|
c639c5f278 | ||
|
|
05eeddba7c | ||
|
|
08a6f88a4b | ||
|
|
ede5c671b8 | ||
|
|
684c43604a | ||
|
|
8199a7d32a | ||
|
|
6f8cff7764 | ||
|
|
89ced5ec6b | ||
|
|
836fd86090 | ||
|
|
7c094dc572 | ||
|
|
c83e791014 | ||
|
|
6119d89fa5 | ||
|
|
d5611953a7 | ||
|
|
71df378fb0 | ||
|
|
5e3a94bbc9 | ||
|
|
3670886bee | ||
|
|
6888f13f27 | ||
|
|
12395e497b | ||
|
|
a17d84e570 | ||
|
|
ca6f849b53 | ||
|
|
7e873a9d63 | ||
|
|
d85909e5ae | ||
|
|
699af8dc5b | ||
|
|
0c0b0fe0f8 | ||
|
|
bfdcb51793 | ||
|
|
ca26402377 | ||
|
|
3022cab8ac | ||
|
|
8a7ac784c4 | ||
|
|
54a04840c7 | ||
|
|
f2b54b5b43 | ||
|
|
4a41369a00 | ||
|
|
7fa879c3a0 | ||
|
|
166dafe05e | ||
|
|
68ba24d91f | ||
|
|
20f7017758 | ||
|
|
0204f0dcbc | ||
|
|
032375b016 | ||
|
|
8448e5b6b6 | ||
|
|
d6f6b05d77 | ||
|
|
feb7e082af | ||
|
|
dfa0974894 | ||
|
|
f2b6c4a707 |
6
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
6
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -43,6 +43,7 @@ body:
|
||||
- 'plugin: cloud'
|
||||
- 'plugin: cloud-storage'
|
||||
- 'plugin: form-builder'
|
||||
- 'plugin: multi-tenant'
|
||||
- 'plugin: nested-docs'
|
||||
- 'plugin: richtext-lexical'
|
||||
- 'plugin: richtext-slate'
|
||||
@@ -59,10 +60,7 @@ body:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
Node.js:
|
||||
Next.js:
|
||||
placeholder: Run `pnpm payload info` in your terminal and paste the output here.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -10,7 +10,7 @@ inputs:
|
||||
pnpm-version:
|
||||
description: Pnpm version
|
||||
required: true
|
||||
default: 9.7.1
|
||||
default: 10.12.1
|
||||
pnpm-run-install:
|
||||
description: Whether to run pnpm install
|
||||
required: false
|
||||
|
||||
141
.github/workflows/main.yml
vendored
141
.github/workflows/main.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- labeled
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -17,7 +18,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
PNPM_VERSION: 10.12.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -363,7 +364,7 @@ jobs:
|
||||
run: pnpm exec playwright install-deps chromium
|
||||
|
||||
- name: E2E Tests
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e:prod:ci ${{ matrix.suite }}
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.suite }}.json pnpm test:e2e:prod:ci:noturbo ${{ matrix.suite }}
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME: results_${{ matrix.suite }}.json
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
@@ -383,6 +384,142 @@ jobs:
|
||||
# report-tag: ${{ matrix.suite }}
|
||||
# job-summary: true
|
||||
|
||||
tests-e2e-turbo:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: >-
|
||||
needs.changes.outputs.needs_tests == 'true' &&
|
||||
(
|
||||
contains(github.event.pull_request.labels.*.name, 'run-e2e-turbo') ||
|
||||
github.event.label.name == 'run-e2e-turbo'
|
||||
)
|
||||
name: e2e-turbo-${{ matrix.suite }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
|
||||
suite:
|
||||
- _community
|
||||
- access-control
|
||||
- admin__e2e__general
|
||||
- admin__e2e__list-view
|
||||
- admin__e2e__document-view
|
||||
- admin-bar
|
||||
- admin-root
|
||||
- auth
|
||||
- auth-basic
|
||||
- bulk-edit
|
||||
- joins
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
- fields__collections__Array
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Blocks#config.blockreferences.ts
|
||||
- fields__collections__Checkbox
|
||||
- fields__collections__Collapsible
|
||||
- fields__collections__ConditionalLogic
|
||||
- fields__collections__CustomID
|
||||
- fields__collections__Date
|
||||
- fields__collections__Email
|
||||
- fields__collections__Indexed
|
||||
- fields__collections__JSON
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Radio
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__Row
|
||||
- fields__collections__Select
|
||||
- fields__collections__Tabs
|
||||
- fields__collections__Tabs2
|
||||
- fields__collections__Text
|
||||
- fields__collections__UI
|
||||
- fields__collections__Upload
|
||||
- hooks
|
||||
- lexical__collections__Lexical__e2e__main
|
||||
- lexical__collections__Lexical__e2e__blocks
|
||||
- lexical__collections__Lexical__e2e__blocks#config.blockreferences.ts
|
||||
- lexical__collections__RichText
|
||||
- query-presets
|
||||
- form-state
|
||||
- live-preview
|
||||
- localization
|
||||
- locked-documents
|
||||
- i18n
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-import-export
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- sort
|
||||
- versions
|
||||
- uploads
|
||||
env:
|
||||
SUITE_NAME: ${{ matrix.suite }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Node setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-run-install: false
|
||||
pnpm-restore-cache: false # Full build is restored below
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Start LocalStack
|
||||
run: pnpm docker:start
|
||||
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
|
||||
|
||||
- name: Store Playwright's Version
|
||||
run: |
|
||||
# Extract the version number using a more targeted regex pattern with awk
|
||||
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
|
||||
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
|
||||
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Playwright Browsers for Playwright's Version
|
||||
id: cache-playwright-browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
|
||||
|
||||
- name: Setup Playwright - Browsers and Dependencies
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Setup Playwright - Dependencies-only
|
||||
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
|
||||
run: pnpm exec playwright install-deps chromium
|
||||
|
||||
- name: E2E Tests
|
||||
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
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-turbo${{ matrix.suite }}
|
||||
path: test/test-results/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
# Disabled until this is fixed: https://github.com/daun/playwright-report-summary/issues/156
|
||||
# - uses: daun/playwright-report-summary@v3
|
||||
# with:
|
||||
# report-file: results_${{ matrix.suite }}.json
|
||||
# report-tag: ${{ matrix.suite }}
|
||||
# job-summary: true
|
||||
|
||||
# Build listed templates with packed local packages
|
||||
build-templates:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
2
.github/workflows/post-release-templates.yml
vendored
2
.github/workflows/post-release-templates.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
PNPM_VERSION: 10.12.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
|
||||
2
.github/workflows/post-release.yml
vendored
2
.github/workflows/post-release.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
PNPM_VERSION: 10.12.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
|
||||
2
.github/workflows/publish-prerelease.yml
vendored
2
.github/workflows/publish-prerelease.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 23.11.0
|
||||
PNPM_VERSION: 9.7.1
|
||||
PNPM_VERSION: 10.12.1
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pnpm 9.7.1
|
||||
pnpm 10.12.1
|
||||
nodejs 23.11.0
|
||||
|
||||
@@ -87,41 +87,43 @@ You can run the entire test suite using `pnpm test`. If you wish to only run e2e
|
||||
|
||||
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
|
||||
|
||||
### Commits
|
||||
### Pull Requests
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for our commit messages. Please follow this format when creating commits. Here are some examples:
|
||||
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR description.
|
||||
|
||||
- `feat: adds new feature`
|
||||
- `fix: fixes bug`
|
||||
- `docs: adds documentation`
|
||||
- `chore: does chore`
|
||||
All commits within a PR are squashed when merged, using the PR title as the commit message. For that reason, please use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for your PR titles.
|
||||
|
||||
Here's a breakdown of the format. At the top-level, we use the following types to categorize our commits:
|
||||
Here are some examples:
|
||||
|
||||
- `feat`: new feature that adds functionality. These are automatically added to the changelog when creating new releases.
|
||||
- `fix`: a fix to an existing feature. These are automatically added to the changelog when creating new releases.
|
||||
- `docs`: changes to [docs](./docs) only. These do not appear in the changelog.
|
||||
- `chore`: changes to code that is neither a fix nor a feature (e.g. refactoring, adding tests, etc.). These do not appear in the changelog.
|
||||
- `feat: add new feature`
|
||||
- `fix: fix bug`
|
||||
- `docs: add documentation`
|
||||
- `test: add/fix tests`
|
||||
- `refactor: refactor code`
|
||||
- `chore: anything that does not fit into the above categories`
|
||||
|
||||
If applicable, you must indicate the affected packages in parentheses to "scope" the changes. Changes to the payload chore package do not require scoping.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
- `feat(ui): add new feature`
|
||||
- `fix(richtext-lexical): fix bug`
|
||||
|
||||
If you are committing to [templates](./templates) or [examples](./examples), use the `chore` type with the proper scope, like this:
|
||||
|
||||
- `chore(templates): adds feature to template`
|
||||
- `chore(examples): fixes bug in example`
|
||||
|
||||
## Pull Requests
|
||||
|
||||
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
|
||||
|
||||
## Previewing docs
|
||||
|
||||
This is how you can preview changes you made locally to the docs:
|
||||
|
||||
1. Clone our [website repository](https://github.com/payloadcms/website)
|
||||
2. Run `yarn install`
|
||||
2. Run `pnpm install`
|
||||
3. Duplicate the `.env.example` file and rename it to `.env`
|
||||
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
|
||||
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)
|
||||
5. Run `pnpm fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
6. You're done! Now you can start the website locally using `pnpm dev` and preview the docs under [http://localhost:3000/docs/local](http://localhost:3000/docs/local)
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
|
||||
@@ -97,7 +97,6 @@ The following options are available:
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`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](../custom-components/overview). |
|
||||
| **`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. |
|
||||
|
||||
@@ -981,7 +981,15 @@ const MyComponent: React.FC = () => {
|
||||
|
||||
## useTableColumns
|
||||
|
||||
Returns methods to manipulate table columns
|
||||
Returns properties and methods to manipulate table columns:
|
||||
| Property | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| **`columns`** | The current state of columns including their active status and configuration |
|
||||
| **`LinkedCellOverride`** | A component override for linked cells in the table |
|
||||
| **`moveColumn`** | A method to reorder columns. Accepts `{ fromIndex: number, toIndex: number }` as arguments |
|
||||
| **`resetColumnsState`** | A method to reset columns back to their default configuration as defined in the collection config |
|
||||
| **`setActiveColumns`** | A method to set specific columns to active state while preserving the existing column order. Accepts an array of column names to activate |
|
||||
| **`toggleColumn`** | A method to toggle a single column's visibility. Accepts a column name as string |
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -989,17 +997,30 @@ import { useTableColumns } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { setActiveColumns } = useTableColumns()
|
||||
const { setActiveColumns, resetColumnsState } = useTableColumns()
|
||||
|
||||
const resetColumns = () => {
|
||||
setActiveColumns(['id', 'createdAt', 'updatedAt'])
|
||||
const activateSpecificColumns = () => {
|
||||
// Only activates the id and createdAt columns
|
||||
// Other columns retain their current active/inactive state
|
||||
// The original column order is preserved
|
||||
setActiveColumns(['id', 'createdAt'])
|
||||
}
|
||||
|
||||
const resetToDefaults = () => {
|
||||
// Resets to the default columns defined in the collection config
|
||||
resetColumnsState()
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<button type="button" onClick={resetColumns}>
|
||||
Reset columns
|
||||
</button>
|
||||
<div>
|
||||
<button type="button" onClick={activateSpecificColumns}>
|
||||
Activate Specific Columns
|
||||
</button>
|
||||
<button type="button" onClick={resetToDefaults}>
|
||||
Reset To Defaults
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -25,11 +25,12 @@ A strategy is made up of the following:
|
||||
|
||||
The `authenticate` function is passed the following arguments:
|
||||
|
||||
| Argument | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
|
||||
| **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. |
|
||||
| **`isGraphQL`** | Whether or not the request was made from a GraphQL endpoint. Default is `false`. |
|
||||
| Argument | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`canSetHeaders`** \* | Whether or not the strategy is being executed from a context where response headers can be set. Default is `false`. |
|
||||
| **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
|
||||
| **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. |
|
||||
| **`isGraphQL`** | Whether or not the strategy is being executed within the GraphQL endpoint. Default is `false`. |
|
||||
|
||||
### Example Strategy
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ Example:
|
||||
```ts
|
||||
const user = await fetch('http://localhost:3000/api/users/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'password',
|
||||
|
||||
@@ -142,14 +142,17 @@ import { buildConfig } from 'payload'
|
||||
export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
autoLogin:
|
||||
process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN === 'true'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
admin: {
|
||||
autoLogin:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
},
|
||||
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
@@ -175,7 +178,7 @@ All auth-related operations are available via Payload's REST, Local, and GraphQL
|
||||
|
||||
## Strategies
|
||||
|
||||
Out of the box Payload ships with a three powerful Authentication strategies:
|
||||
Out of the box Payload ships with three powerful Authentication strategies:
|
||||
|
||||
- [HTTP-Only Cookies](./cookies)
|
||||
- [JSON Web Tokens (JWT)](./jwt)
|
||||
|
||||
@@ -85,6 +85,7 @@ The following options are available:
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -194,13 +195,15 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). |
|
||||
| `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#savedraftbutton). |
|
||||
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). |
|
||||
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). |
|
||||
| `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). |
|
||||
| Option | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](../custom-components/edit-view#beforedocumentcontrols). |
|
||||
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document controls bar. [More details](../custom-components/edit-view#editmenuitems). |
|
||||
| `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). |
|
||||
| `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#savedraftbutton). |
|
||||
| `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). |
|
||||
| `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). |
|
||||
| `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). |
|
||||
|
||||
<Banner type="success">
|
||||
**Note:** For details on how to build Custom Components, see [Building Custom
|
||||
|
||||
@@ -88,7 +88,7 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
### Edit View
|
||||
|
||||
The Edit View is where users interact with individual Collection and Global Documents. This is where they can view, edit, and save their content. the Edit View is keyed under the `default` property in the `views.edit` object.
|
||||
The Edit View is where users interact with individual Collection and Global Documents. This is where they can view, edit, and save their content. The Edit View is keyed under the `default` property in the `views.edit` object.
|
||||
|
||||
For more information on customizing the Edit View, see the [Edit View](./edit-view) documentation.
|
||||
|
||||
@@ -107,8 +107,8 @@ export const MyCollection: CollectionConfig = {
|
||||
components: {
|
||||
views: {
|
||||
edit: {
|
||||
myCustomTab: {
|
||||
Component: '/path/to/MyCustomTab',
|
||||
myCustomView: {
|
||||
Component: '/path/to/MyCustomView',
|
||||
path: '/my-custom-tab',
|
||||
// highlight-start
|
||||
tab: {
|
||||
@@ -116,13 +116,14 @@ export const MyCollection: CollectionConfig = {
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
anotherCustomTab: {
|
||||
anotherCustomView: {
|
||||
Component: '/path/to/AnotherCustomView',
|
||||
path: '/another-custom-view',
|
||||
// highlight-start
|
||||
tab: {
|
||||
label: 'Another Custom View',
|
||||
href: '/another-custom-view',
|
||||
order: '100',
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
@@ -143,6 +144,7 @@ The following options are available for tabs:
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `label` | The label to display in the tab. |
|
||||
| `href` | The URL to navigate to when the tab is clicked. This is optional and defaults to the tab's `path`. |
|
||||
| `order` | The order in which the tab appears in the navigation. Can be set on default and custom tabs. |
|
||||
| `Component` | The component to render in the tab. This can be a Server or Client component. [More details](#tab-components) |
|
||||
|
||||
### Tab Components
|
||||
|
||||
@@ -101,15 +101,16 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
|
||||
| `Description` | A description of the Collection. [More details](#description). |
|
||||
| `Upload` | A file upload component. [More details](#upload). |
|
||||
| Path | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
|
||||
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
|
||||
| `Description` | A description of the Collection. [More details](#description). |
|
||||
| `Upload` | A file upload component. [More details](#upload). |
|
||||
|
||||
#### Globals
|
||||
|
||||
@@ -134,14 +135,15 @@ export const MyGlobal: GlobalConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
|
||||
| `Description` | A description of the Global. [More details](#description). |
|
||||
| Path | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). |
|
||||
| `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). |
|
||||
| `SaveButton` | A button that saves the current document. [More details](#savebutton). |
|
||||
| `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). |
|
||||
| `PublishButton` | A button that publishes the current document. [More details](#publishbutton). |
|
||||
| `PreviewButton` | A button that previews the current document. [More details](#previewbutton). |
|
||||
| `Description` | A description of the Global. [More details](#description). |
|
||||
|
||||
### SaveButton
|
||||
|
||||
@@ -260,6 +262,92 @@ export function MyCustomDocumentControlButton(
|
||||
}
|
||||
```
|
||||
|
||||
### editMenuItems
|
||||
|
||||
The `editMenuItems` property allows you to inject custom components into the 3-dot menu dropdown located in the document controls bar. This dropdown contains default options including `Create New`, `Duplicate`, `Delete`, and other options when additional features are enabled. Any custom components you add will appear below these default items.
|
||||
|
||||
To add `editMenuItems`, use the `components.edit.editMenuItems` property in your [Collection Config](../configuration/collections):
|
||||
|
||||
#### Config Example
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
components: {
|
||||
edit: {
|
||||
// highlight-start
|
||||
editMenuItems: ['/path/to/CustomEditMenuItem'],
|
||||
// highlight-end
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Here's an example of a custom `editMenuItems` component:
|
||||
|
||||
#### Server Component
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { PopupList } from '@payloadcms/ui'
|
||||
|
||||
import type { EditMenuItemsServerProps } from 'payload'
|
||||
|
||||
export const EditMenuItems = async (props: EditMenuItemsServerProps) => {
|
||||
const href = `/custom-action?id=${props.id}`
|
||||
|
||||
return (
|
||||
<PopupList.ButtonGroup>
|
||||
<PopupList.Button href={href}>Custom Edit Menu Item</PopupList.Button>
|
||||
<PopupList.Button href={href}>
|
||||
Another Custom Edit Menu Item - add as many as you need!
|
||||
</PopupList.Button>
|
||||
</PopupList.ButtonGroup>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Client Component
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { PopupList } from '@payloadcms/ui'
|
||||
|
||||
import type { EditViewMenuItemClientProps } from 'payload'
|
||||
|
||||
export const EditMenuItems = (props: EditViewMenuItemClientProps) => {
|
||||
const handleClick = () => {
|
||||
console.log('Custom button clicked!')
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupList.ButtonGroup>
|
||||
<PopupList.Button onClick={handleClick}>
|
||||
Custom Edit Menu Item
|
||||
</PopupList.Button>
|
||||
<PopupList.Button onClick={handleClick}>
|
||||
Another Custom Edit Menu Item - add as many as you need!
|
||||
</PopupList.Button>
|
||||
</PopupList.ButtonGroup>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Styling:** Use Payload's built-in `PopupList.Button` to ensure your menu
|
||||
items automatically match the default dropdown styles. If you want a different
|
||||
look, you can customize the appearance by passing your own `className` to
|
||||
`PopupList.Button`, or use a completely custom button built with a standard
|
||||
HTML `button` element or any other component that fits your design
|
||||
preferences.
|
||||
</Banner>
|
||||
|
||||
### SaveDraftButton
|
||||
|
||||
The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View.
|
||||
|
||||
@@ -94,6 +94,7 @@ The following options are available:
|
||||
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforelisttable). |
|
||||
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterlist). |
|
||||
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterlisttable). |
|
||||
| `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) |
|
||||
| `Description` | A component to render a description of the Collection. [More details](#description). |
|
||||
|
||||
### beforeList
|
||||
|
||||
@@ -372,13 +372,13 @@ export default function MyCustomLogo() {
|
||||
}
|
||||
```
|
||||
|
||||
### Header
|
||||
### header
|
||||
|
||||
The `Header` property allows you to inject Custom Components above the Payload header.
|
||||
The `header` property allows you to inject Custom Components above the Payload header.
|
||||
|
||||
Examples of a custom header components might include an announcements banner, a notifications bar, or anything else you'd like to display at the top of the Admin Panel in a prominent location.
|
||||
|
||||
To add `Header` components, use the `admin.components.header` property in your Payload Config:
|
||||
To add `header` components, use the `admin.components.header` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -388,14 +388,14 @@ export default buildConfig({
|
||||
admin: {
|
||||
// highlight-start
|
||||
components: {
|
||||
Header: ['/path/to/your/component'],
|
||||
header: ['/path/to/your/component'],
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Here is an example of a simple `Header` component:
|
||||
Here is an example of a simple `header` component:
|
||||
|
||||
```tsx
|
||||
export default function MyCustomHeader() {
|
||||
|
||||
@@ -183,13 +183,13 @@ Depending on which Database Adapter you use, your migration workflow might diffe
|
||||
|
||||
In relational databases, migrations will be **required** for non-development database environments. But with MongoDB, you might only need to run migrations once in a while (or never even need them).
|
||||
|
||||
#### MongoDB
|
||||
#### MongoDB#mongodb-migrations
|
||||
|
||||
In MongoDB, you'll only ever really need to run migrations for times where you change your database shape, and you have lots of existing data that you'd like to transform from Shape A to Shape B.
|
||||
|
||||
In this case, you can create a migration by running `pnpm payload migrate:create`, and then write the logic that you need to perform to migrate your documents to their new shape. You can then either run your migrations in CI before you build / deploy, or you can run them locally, against your production database, by using your production database connection string on your local computer and running the `pnpm payload migrate` command.
|
||||
|
||||
#### Postgres
|
||||
#### Postgres#postgres-migrations
|
||||
|
||||
In relational databases like Postgres, migrations are a bit more important, because each time you add a new field or a new collection, you'll need to update the shape of your database to match your Payload Config (otherwise you'll see errors upon trying to read / write your data).
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ export default buildConfig({
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
|
||||
| `readReplicas` | An array of DB read replicas connection strings, can be used to offload read-heavy traffic. |
|
||||
| `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export default buildConfig({
|
||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||
| `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows |
|
||||
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
|
||||
| `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const MyGroupField: Field = {
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`fields`** \* | Array of field types to nest within this Group. |
|
||||
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Required when name is undefined, defaults to name converted to words. |
|
||||
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Defaults to the field name, if defined. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
@@ -113,8 +113,7 @@ export const ExampleCollection: CollectionConfig = {
|
||||
|
||||
## Presentational group fields
|
||||
|
||||
You can also use the Group field to create a presentational group of fields. This is useful when you want to group fields together visually without affecting the data structure.
|
||||
The label will be required when a `name` is not provided.
|
||||
You can also use the Group field to only visually group fields without affecting the data structure. Not defining a label will render just the grouped fields.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
@@ -305,6 +305,22 @@ The following additional properties are provided in the `ctx` object:
|
||||
| `req` | The current HTTP request object. Contains `payload`, `user`, etc. |
|
||||
| `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#async-field-validations). |
|
||||
|
||||
#### Localized and Built-in Error Messages
|
||||
|
||||
You can return localized error messages by utilizing the translation function provided in the `req` object:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const MyField: Field = {
|
||||
type: 'text',
|
||||
name: 'myField',
|
||||
validate: (value, {req: { t }}) => Boolean(value) || t('validation:required'), // highlight-line
|
||||
}
|
||||
```
|
||||
|
||||
This way you can use [Custom Translations](https://payloadcms.com/docs/configuration/i18n#custom-translations) as well as Payload's built in error messages (like `validation:required` used in the example above). For a full list of available translation strings, see the [english translation file](https://github.com/payloadcms/payload/blob/main/packages/translations/src/languages/en.ts) of Payload.
|
||||
|
||||
#### Reusing Default Field Validations
|
||||
|
||||
When using custom validation functions, Payload will use yours in place of the default. However, you might want to simply augment the default validation with your own custom logic.
|
||||
|
||||
@@ -54,7 +54,7 @@ export const MySelectField: Field = {
|
||||
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
|
||||
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
|
||||
| **`filterOptions`** | Dynamically filter which options are available based on the user, data, etc. [More details](#filterOptions) |
|
||||
| **`filterOptions`** | Dynamically filter which options are available based on the user, data, etc. [More details](#filteroptions) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/fields/relationship#linking-virtual-fields-with-relationships). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
@@ -70,7 +70,7 @@ _\* An asterisk denotes that a property is required._
|
||||
|
||||
### filterOptions
|
||||
|
||||
Used to dynamically filter which options are available based on the user, data, etc.
|
||||
Used to dynamically filter which options are available based on the current user, document data, or other criteria.
|
||||
|
||||
Some examples of this might include:
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ On the payload config, you can configure the following settings under the `folde
|
||||
// Type definition
|
||||
|
||||
type RootFoldersConfiguration = {
|
||||
/**
|
||||
* If true, the browse by folder view will be enabled
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
browseByFolder?: boolean
|
||||
/**
|
||||
* An array of functions to be ran when the folder collection is initialized
|
||||
* This allows plugins to modify the collection configuration
|
||||
@@ -82,7 +88,16 @@ To enable folders on a collection, you need to set the `admin.folders` property
|
||||
```ts
|
||||
// Type definition
|
||||
|
||||
type CollectionFoldersConfiguration = boolean
|
||||
type CollectionFoldersConfiguration =
|
||||
| boolean
|
||||
| {
|
||||
/**
|
||||
* If true, the collection will be included in the browse by folder view
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
browseByFolder?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
|
||||
@@ -121,7 +121,7 @@ The following arguments are provided to the `beforeValidate` hook:
|
||||
|
||||
### beforeChange
|
||||
|
||||
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved.
|
||||
Immediately before validation, beforeChange hooks will run during create and update operations. At this stage, the data should be treated as unvalidated user input. There is no guarantee that required fields exist or that fields are in the correct format. As such, using this data for side effects requires manual validation. You can optionally modify the shape of the data to be saved.
|
||||
|
||||
```ts
|
||||
import type { CollectionBeforeChangeHook } from 'payload'
|
||||
@@ -160,6 +160,7 @@ The following arguments are provided to the `afterChange` hook:
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. |
|
||||
| **`context`** | Custom context passed between hooks. [More details](./context). |
|
||||
| **`data`** | The incoming data passed through the operation. |
|
||||
| **`doc`** | The resulting Document after changes are applied. |
|
||||
| **`operation`** | The name of the operation that this hook is running within. |
|
||||
| **`previousDoc`** | The Document before changes were applied. |
|
||||
|
||||
@@ -128,20 +128,18 @@ const MyCollection: CollectionConfig = {
|
||||
|
||||
## TypeScript
|
||||
|
||||
The default TypeScript interface for `context` is `{ [key: string]: unknown }`. If you prefer a more strict typing in your project or when authoring plugins for others, you can override this using the `declare` syntax.
|
||||
The default TypeScript interface for `context` is `{ [key: string]: unknown }`. If you prefer a more strict typing in your project or when authoring plugins for others, you can override this using the `declare module` syntax.
|
||||
|
||||
This is known as "type augmentation", a TypeScript feature which allows us to add types to existing types. Simply put this in any `.ts` or `.d.ts` file:
|
||||
This is known as [module augmentation / declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation), a TypeScript feature which allows us to add properties to existing types. Simply put this in any `.ts` or `.d.ts` file:
|
||||
|
||||
```ts
|
||||
import { RequestContext as OriginalRequestContext } from 'payload'
|
||||
|
||||
declare module 'payload' {
|
||||
// Create a new interface that merges your additional fields with the original one
|
||||
export interface RequestContext extends OriginalRequestContext {
|
||||
// Augment the RequestContext interface to include your custom properties
|
||||
export interface RequestContext {
|
||||
myObject?: string
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This will add the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as type augmentation can mess up your types if you do it wrong.
|
||||
This will add the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as module augmentation can mess up your types if you do it wrong.
|
||||
|
||||
@@ -128,6 +128,7 @@ The following arguments are provided to the `afterChange` hook:
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`global`** | The [Global](../configuration/globals) in which this Hook is running against. |
|
||||
| **`context`** | Custom context passed between hooks. [More details](./context). |
|
||||
| **`data`** | The incoming data passed through the operation. |
|
||||
| **`doc`** | The resulting Document after changes are applied. |
|
||||
| **`previousDoc`** | The Document before changes were applied. |
|
||||
| **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. |
|
||||
|
||||
@@ -68,3 +68,26 @@ Here's a quick overview:
|
||||
- Workflows are groupings of specific tasks which should be run in-order, and can be retried from a specific point of failure
|
||||
- A Job is an instance of a single task or workflow which will be executed
|
||||
- A Queue is a way to segment your jobs into different "groups" - for example, some to run nightly, and others to run every 10 minutes
|
||||
|
||||
### Visualizing Jobs in the Admin UI
|
||||
|
||||
By default, the internal `payload-jobs` collection is hidden from the Payload Admin Panel. To make this collection visible for debugging or inspection purposes, you can override its configuration using `jobsCollectionOverrides`.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ... other config
|
||||
jobs: {
|
||||
// ... other job settings
|
||||
jobsCollectionOverrides: ({ defaultJobsCollection }) => {
|
||||
if (!defaultJobsCollection.admin) {
|
||||
defaultJobsCollection.admin = {}
|
||||
}
|
||||
|
||||
defaultJobsCollection.admin.hidden = false
|
||||
return defaultJobsCollection
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -30,7 +30,9 @@ As mentioned above, you can queue jobs, but the jobs won't run unless a worker p
|
||||
|
||||
### Cron jobs
|
||||
|
||||
You can use the `jobs.autoRun` property to configure cron jobs:
|
||||
The `jobs.autoRun` property allows you to configure cron jobs that automatically run queued jobs at specified intervals. Note that this does not _queue_ new jobs - only _runs_ jobs that are already in the specified queue.
|
||||
|
||||
**Example**:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
@@ -80,6 +82,12 @@ await fetch('/api/payload-jobs/run?limit=100&queue=nightly', {
|
||||
|
||||
This endpoint is automatically mounted for you and is helpful in conjunction with serverless platforms like Vercel, where you might want to use Vercel Cron to invoke a serverless function that executes your jobs.
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
- `limit`: The maximum number of jobs to run in this invocation (default: 10).
|
||||
- `queue`: The name of the queue to run jobs from. If not specified, jobs will be run from the `default` queue.
|
||||
- `allQueues`: If set to `true`, all jobs from all queues will be run. This will ignore the `queue` parameter.
|
||||
|
||||
**Vercel Cron Example**
|
||||
|
||||
If you're deploying on Vercel, you can add a `vercel.json` file in the root of your project that configures Vercel Cron to invoke the `run` endpoint on a cron schedule.
|
||||
@@ -137,11 +145,15 @@ If you want to process jobs programmatically from your server-side code, you can
|
||||
**Run all jobs:**
|
||||
|
||||
```ts
|
||||
// Run all jobs from the `default` queue - default limit is 10
|
||||
const results = await payload.jobs.run()
|
||||
|
||||
// You can customize the queue name and limit by passing them as arguments:
|
||||
await payload.jobs.run({ queue: 'nightly', limit: 100 })
|
||||
|
||||
// Run all jobs from all queues:
|
||||
await payload.jobs.run({ allQueues: true })
|
||||
|
||||
// You can provide a where clause to filter the jobs that should be run:
|
||||
await payload.jobs.run({
|
||||
where: { 'input.message': { equals: 'secret' } },
|
||||
@@ -158,10 +170,22 @@ const results = await payload.jobs.runByID({
|
||||
|
||||
### Bin script
|
||||
|
||||
Finally, you can process jobs via the bin script that comes with Payload out of the box.
|
||||
Finally, you can process jobs via the bin script that comes with Payload out of the box. By default, this script will run jobs from the `default` queue, with a limit of 10 jobs per invocation:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --queue default --limit 10
|
||||
npx payload jobs:run
|
||||
```
|
||||
|
||||
You can override the default queue and limit by passing the `--queue` and `--limit` flags:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --queue myQueue --limit 15
|
||||
```
|
||||
|
||||
If you want to run all jobs from all queues, you can pass the `--all-queues` flag:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --all-queues
|
||||
```
|
||||
|
||||
In addition, the bin script allows you to pass a `--cron` flag to the `jobs:run` command to run the jobs on a scheduled, cron basis:
|
||||
|
||||
@@ -329,7 +329,7 @@ available:
|
||||
// responseHeaders: { ... } // returned headers from the response
|
||||
// }
|
||||
|
||||
const result = await payload.auth({ headers })
|
||||
const result = await payload.auth({ headers, canSetHeaders: false })
|
||||
```
|
||||
|
||||
### Login
|
||||
|
||||
@@ -470,6 +470,31 @@ formBuilderPlugin({
|
||||
})
|
||||
```
|
||||
|
||||
### Preventing generated schema naming conflicts
|
||||
|
||||
Plugin fields can cause GraphQL type name collisions with your own blocks or collections. This results in errors like:
|
||||
|
||||
```plaintext
|
||||
Error: Schema must contain uniquely named types but contains multiple types named "Country"
|
||||
```
|
||||
|
||||
You can resolve this by overriding:
|
||||
|
||||
- `graphQL.singularName` in your collection config (for GraphQL schema conflicts)
|
||||
- `interfaceName` in your block config
|
||||
- `interfaceName` in the plugin field config
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
formBuilderPlugin({
|
||||
fields: {
|
||||
country: {
|
||||
interfaceName: 'CountryFormBlock', // overrides the generated type name to avoid a conflict
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Email
|
||||
|
||||
This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided.
|
||||
|
||||
@@ -16,8 +16,8 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
If you need help, check out our [Community
|
||||
Help](https://payloadcms.com/community-help). If you think you've found a bug,
|
||||
please [open a new
|
||||
issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%multi-tenant&template=bug_report.md&title=plugin-multi-tenant%3A)
|
||||
with as much detail as possible.
|
||||
issue](https://github.com/payloadcms/payload/issues/new/choose) with as much
|
||||
detail as possible.
|
||||
</Banner>
|
||||
|
||||
## Core features
|
||||
@@ -35,7 +35,7 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
||||
strong access control on your tenants collection to prevent deletions by unauthorized users.
|
||||
|
||||
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
You can disable this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
|
||||
</Banner>
|
||||
|
||||
|
||||
@@ -180,8 +180,8 @@ and `createBreadcrumbField` methods. They will merge your customizations overtop
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { createParentField } from '@payloadcms/plugin-nested-docs/fields'
|
||||
import { createBreadcrumbsField } from '@payloadcms/plugin-nested-docs/fields'
|
||||
import { createParentField } from '@payloadcms/plugin-nested-docs'
|
||||
import { createBreadcrumbsField } from '@payloadcms/plugin-nested-docs'
|
||||
|
||||
const examplePageConfig: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
|
||||
@@ -84,6 +84,28 @@ const config = buildConfig({
|
||||
export default config
|
||||
```
|
||||
|
||||
## Instrumenting Database Queries
|
||||
|
||||
If you want Sentry to capture Postgres query performance traces, you need to inject the Sentry-patched `pg` driver into the Postgres adapter. This ensures Sentry’s instrumentation hooks into your database calls.
|
||||
|
||||
```ts
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { buildConfig } from 'payload'
|
||||
import { sentryPlugin } from '@payloadcms/plugin-sentry'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import pg from 'pg'
|
||||
|
||||
export default buildConfig({
|
||||
db: postgresAdapter({
|
||||
pool: { connectionString: process.env.DATABASE_URL },
|
||||
pg, // Inject the patched pg driver for Sentry instrumentation
|
||||
}),
|
||||
plugins: [
|
||||
sentryPlugin({ Sentry }),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `Sentry` : Sentry | **required**
|
||||
|
||||
@@ -115,6 +115,7 @@ Set the `uploadsCollection` to your application's upload-enabled collection slug
|
||||
##### `tabbedUI`
|
||||
|
||||
When the `tabbedUI` property is `true`, it appends an `SEO` tab onto your config using Payload's [Tabs Field](../fields/tabs). If your collection is not already tab-enabled, meaning the first field in your config is not of type `tabs`, then one will be created for you called `Content`. Defaults to `false`.
|
||||
Note that the order of plugins or fields in your config may affect whether or not the plugin can smartly merge tabs with your existing fields. If you have a complex structure we recommend you [make use of the fields directly](#direct-use-of-fields) instead of relying on this config option.
|
||||
|
||||
<Banner type="info">
|
||||
If you wish to continue to use top-level or sidebar fields with `tabbedUI`,
|
||||
|
||||
30
docs/production/building-without-a-db-connection.mdx
Normal file
30
docs/production/building-without-a-db-connection.mdx
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Building without a DB connection
|
||||
label: Building without a DB connection
|
||||
order: 10
|
||||
desc: You don't want to have a DB connection while building your Docker container? Learn how to prevent that!
|
||||
keywords: deployment, production, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
One of the most common problems when building a site for production, especially with Docker - is the DB connection requirement.
|
||||
|
||||
The important note is that Payload by itself does not have this requirement, But [Next.js' SSG ](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation) does if any of your route segments have SSG enabled (which is default, unless you opted out or used a [Dynamic API](https://nextjs.org/docs/app/deep-dive/caching#dynamic-apis)) and use the Payload Local API.
|
||||
|
||||
Solutions:
|
||||
|
||||
## Using the experimental-build-mode Next.js build flag
|
||||
|
||||
You can run Next.js build using the `pnpx next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpx next build --experimental-build-mode generate` command when you have a DB connection.
|
||||
|
||||
[Next.js documentation](https://nextjs.org/docs/pages/api-reference/cli/next#next-build-options)
|
||||
|
||||
## Opting-out of SSG
|
||||
|
||||
You can opt out of SSG by adding this all the route segment files:
|
||||
|
||||
```ts
|
||||
export const dynamic = 'force-dynamic'
|
||||
```
|
||||
|
||||
**Note that it will disable static optimization and your site will be slower**.
|
||||
More on [Next.js documentation](https://nextjs.org/docs/app/deep-dive/caching#opting-out-2)
|
||||
@@ -150,7 +150,7 @@ Follow the docs to configure any one of these storage providers. For local devel
|
||||
## Docker
|
||||
|
||||
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment
|
||||
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
|
||||
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed. If you don't want to have a DB connection and your build requires that, learn [here](./building-without-a-db-connection) how to prevent that.
|
||||
|
||||
In your Next.js config, set the `output` property `standalone`.
|
||||
|
||||
|
||||
@@ -46,11 +46,12 @@ const config = buildConfig({
|
||||
|
||||
The following options are available for Query Presets:
|
||||
|
||||
| Option | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
|
||||
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
|
||||
| `labels` | Custom labels to use for the Query Presets collection. |
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
|
||||
| `filterConstraints` | Used to define which constraints are available to users when managing presets. [More details](#constraint-access-control). |
|
||||
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
|
||||
| `labels` | Custom labels to use for the Query Presets collection. |
|
||||
|
||||
## Access Control
|
||||
|
||||
@@ -59,7 +60,7 @@ Query Presets are subject to the same [Access Control](../access-control/overvie
|
||||
Access Control for Query Presets can be customized in two ways:
|
||||
|
||||
1. [Collection Access Control](#collection-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
|
||||
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
|
||||
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are dynamically defined on each record in the database.
|
||||
|
||||
### Collection Access Control
|
||||
|
||||
@@ -97,7 +98,7 @@ This example restricts all Query Presets to users with the role of `admin`.
|
||||
|
||||
### Document Access Control
|
||||
|
||||
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each document.
|
||||
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each record.
|
||||
|
||||
When a user manages a preset, document-level access control options will be available to them in the Admin Panel for each operation.
|
||||
|
||||
@@ -150,8 +151,8 @@ const config = buildConfig({
|
||||
}),
|
||||
},
|
||||
],
|
||||
// highlight-end
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -171,3 +172,39 @@ The following options are available for each constraint:
|
||||
| `value` | The value to store in the database when this constraint is selected. |
|
||||
| `fields` | An array of fields to render when this constraint is selected. |
|
||||
| `access` | A function that determines the access control rules for this constraint. |
|
||||
|
||||
### Constraint Access Control
|
||||
|
||||
Used to dynamically filter which constraints are available based on the current user, document data, or other criteria.
|
||||
|
||||
Some examples of this might include:
|
||||
|
||||
- Ensuring that only "admins" are allowed to make a preset available to "everyone"
|
||||
- Preventing the "onlyMe" option from being selected based on a hypothetical "disablePrivatePresets" checkbox
|
||||
|
||||
When a user lacks the permission to set a constraint, the option will either be hidden from them, or disabled if it is already saved to that preset.
|
||||
|
||||
To do this, you can use the `filterConstraints` property in your [Payload Config](../configuration/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
queryPresets: {
|
||||
// ...
|
||||
// highlight-start
|
||||
filterConstraints: ({ req, options }) =>
|
||||
!req.user?.roles?.includes('admin')
|
||||
? options.filter(
|
||||
(option) =>
|
||||
(typeof option === 'string' ? option : option.value) !==
|
||||
'everyone',
|
||||
)
|
||||
: options,
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `filterConstraints` function receives the same arguments as [`filterOptions`](../fields/select#filteroptions) in the [Select field](../fields/select).
|
||||
|
||||
@@ -738,7 +738,7 @@ Payload supports a method override feature that allows you to send GET requests
|
||||
|
||||
### How to Use
|
||||
|
||||
To use this feature, include the `X-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`.
|
||||
To use this feature, include the `X-Payload-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`.
|
||||
|
||||
### Example
|
||||
|
||||
@@ -753,7 +753,7 @@ const res = await fetch(`${api}/${collectionSlug}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-HTTP-Method-Override': 'GET',
|
||||
'X-Payload-HTTP-Method-Override': 'GET',
|
||||
},
|
||||
body: qs.stringify({
|
||||
depth: 1,
|
||||
|
||||
@@ -336,3 +336,17 @@ You can customize the placeholder (the text that appears in the editor when it's
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Detecting empty editor state
|
||||
|
||||
When you first type into a rich text field and subsequently delete everything through the admin panel, its value changes from `null` to a JSON object containing an empty paragraph.
|
||||
|
||||
If needed, you can reset the field value to `null` programmatically - for example, by using a custom hook to detect when the editor is empty.
|
||||
|
||||
This also applies to fields like `text` and `textArea`, which could be stored as either `null` or an empty value in the database. Since the empty value for richText is a JSON object, checking for emptiness is a bit more involved - so Payload provides a utility for it:
|
||||
|
||||
```ts
|
||||
import { hasText } from '@payloadcms/richtext-lexical/shared'
|
||||
|
||||
hasText(richtextData)
|
||||
```
|
||||
|
||||
@@ -81,6 +81,7 @@ The default `elements` available in Payload are:
|
||||
- `link`
|
||||
- `ol`
|
||||
- `ul`
|
||||
- `li`
|
||||
- `textAlign`
|
||||
- `indent`
|
||||
- [`relationship`](#relationship-element)
|
||||
|
||||
@@ -95,6 +95,7 @@ _An asterisk denotes that an option is required._
|
||||
| **`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 |
|
||||
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
|
||||
| **`constructorOptions`** | An object passed to the the Sharp image library that accepts any Constructor options and applies them to the upload file. [More](https://sharp.pixelplumbing.com/api-constructor/) |
|
||||
| **`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). |
|
||||
|
||||
@@ -84,7 +84,7 @@ pnpm add @payloadcms/storage-s3
|
||||
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
|
||||
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
|
||||
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
|
||||
- Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control.
|
||||
- Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control. Additionally, with `signedDownloads.shouldUseSignedURL` you can specify a condition whether Payload should use a presigned URL, if you want to use this feature only for specific files.
|
||||
|
||||
```ts
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
@@ -100,6 +100,14 @@ export default buildConfig({
|
||||
'media-with-prefix': {
|
||||
prefix,
|
||||
},
|
||||
'media-with-presigned-downloads': {
|
||||
// Filter only mp4 files
|
||||
signedDownloads: {
|
||||
shouldUseSignedURL: ({ collection, filename, req }) => {
|
||||
return filename.endsWith('.mp4')
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bucket: process.env.S3_BUCKET,
|
||||
config: {
|
||||
|
||||
@@ -51,12 +51,37 @@ Within the Admin UI, if drafts are enabled, a document can be shown with one of
|
||||
|
||||
#### Updating or creating drafts
|
||||
|
||||
If you enable drafts on a collection or global, the `create` and `update` operations for REST, GraphQL, and Local APIs expose a new option called `draft` which allows you to specify if you are creating or updating a **draft**, or if you're just sending your changes straight to the published document. For example, if you pass the query parameter `?draft=true` to a REST `create` or `update` operation, your action will be treated as if you are creating a `draft` and not a published document. By default, the `draft` argument is set to `false`.
|
||||
If you enable drafts on a collection or global, the `create` and `update` operations for REST, GraphQL, and Local APIs expose a new option called `draft` which allows you to specify if you are creating or updating a **draft**, or if you're just sending your changes straight to the published document.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
// REST API
|
||||
POST /api/your-collection?draft=true
|
||||
|
||||
// Local API
|
||||
await payload.create({
|
||||
collection: 'your-collection',
|
||||
data: {
|
||||
// your data here
|
||||
},
|
||||
draft: true, // This is required to create a draft
|
||||
})
|
||||
|
||||
// GraphQL
|
||||
mutation {
|
||||
createYourCollection(data: { ... }, draft: true) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required fields**
|
||||
|
||||
If `draft` is enabled while creating or updating a document, all fields are considered as not required, so that you can save drafts that are incomplete.
|
||||
|
||||
Setting `_status: "draft"` will not bypass the required fields. You need to set `draft: true` as shown in the previous examples.
|
||||
|
||||
#### Reading drafts vs. published documents
|
||||
|
||||
In addition to the `draft` argument within `create` and `update` operations, a `draft` argument is also exposed for `find` and `findByID` operations.
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const GET = async () => {
|
||||
export const GET = async (request: Request) => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
const data = await payload.find({
|
||||
collection: 'users',
|
||||
return Response.json({
|
||||
message: 'This is an example of a custom route.',
|
||||
})
|
||||
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,8 @@
|
||||
width: var(--base);
|
||||
|
||||
.stroke {
|
||||
stroke-width: 1px;
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
&:local() {
|
||||
.stroke {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
examples/live-preview/next-env.d.ts
vendored
2
examples/live-preview/next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Payload Live Preview example.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||
|
||||
3278
examples/live-preview/pnpm-lock.yaml
generated
3278
examples/live-preview/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
import type { Page } from '../payload-types'
|
||||
import { DefaultDocumentIDType } from 'payload'
|
||||
|
||||
export const home: Partial<Page> = {
|
||||
export const home = (id: DefaultDocumentIDType): Partial<Page> => ({
|
||||
slug: 'home',
|
||||
richText: [
|
||||
{
|
||||
@@ -41,11 +42,26 @@ export const home: Partial<Page> = {
|
||||
{
|
||||
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',
|
||||
},
|
||||
...(id
|
||||
? [
|
||||
{
|
||||
text: ' To get started, visit ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
children: [{ text: 'this page' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: `/admin/collections/pages/${id}/preview`,
|
||||
},
|
||||
{ text: '.' },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Home',
|
||||
}
|
||||
})
|
||||
|
||||
export const examplePage: Partial<Page> = {
|
||||
slug: 'example-page',
|
||||
@@ -83,11 +99,18 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
data: examplePage as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home))
|
||||
|
||||
const { id: homePageID } = await payload.create({
|
||||
const { id: ogHomePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: homepageJSON,
|
||||
data: {
|
||||
title: 'Home',
|
||||
richText: [],
|
||||
},
|
||||
})
|
||||
|
||||
const { id: homePageID } = await payload.update({
|
||||
id: ogHomePageID,
|
||||
collection: 'pages',
|
||||
data: home(ogHomePageID),
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
@@ -121,7 +144,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
type: 'custom',
|
||||
label: 'Dashboard',
|
||||
reference: undefined,
|
||||
url: 'http://localhost:3000/admin',
|
||||
url: '/admin',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,10 +6,66 @@
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
pages: Page;
|
||||
users: User;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const GET = async () => {
|
||||
export const GET = async (request: Request) => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
const data = await payload.find({
|
||||
collection: 'users',
|
||||
return Response.json({
|
||||
message: 'This is an example of a custom route.',
|
||||
})
|
||||
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
64
package.json
64
package.json
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"test/*"
|
||||
],
|
||||
"scripts": {
|
||||
"bf": "pnpm run build:force",
|
||||
"build": "pnpm run build:core",
|
||||
@@ -60,11 +64,12 @@
|
||||
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:build:allowtgz": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/meta_*.json'",
|
||||
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts",
|
||||
"dev": "cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=16384\" tsx ./test/dev.ts",
|
||||
"dev:generate-db-schema": "pnpm runts ./test/generateDatabaseSchema.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:memorydb": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --start-memory-db",
|
||||
"dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts",
|
||||
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod",
|
||||
"dev:prod:memorydb": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod --start-memory-db",
|
||||
@@ -84,7 +89,7 @@
|
||||
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release": "pnpm --filter releaser release --tag latest",
|
||||
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"runts": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" node --no-deprecation --no-experimental-strip-types --import @swc-node/register/esm-register",
|
||||
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
|
||||
"script:gen-templates": "pnpm --filter scripts gen-templates",
|
||||
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
|
||||
@@ -93,18 +98,20 @@
|
||||
"script:pack": "pnpm --filter scripts pack-all-to-dest",
|
||||
"pretest": "pnpm build",
|
||||
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
|
||||
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation\" jest --config=jest.components.config.js",
|
||||
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation --no-experimental-strip-types\" jest --config=jest.components.config.js",
|
||||
"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:debug": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:e2e:headed": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:noturbo": "pnpm runts ./test/runE2E.ts --no-turbo",
|
||||
"test:e2e:prod": "pnpm prepare-run-test-against-prod && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:e2e:prod:ci": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:e2e:prod:ci:noturbo": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod --no-turbo",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" 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 --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:types": "tstyche",
|
||||
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
|
||||
"translateNewKeys": "pnpm --filter translations run translateNewKeys"
|
||||
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
|
||||
"translateNewKeys": "pnpm --filter @tools/scripts run generateTranslations:core"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
@@ -115,12 +122,12 @@
|
||||
],
|
||||
"templates/website/**/*": "sh -c \"cd templates/website; pnpm install --no-frozen-lockfile --ignore-workspace; pnpm run lint --fix\"",
|
||||
"templates/**/pnpm-lock.yaml": "pnpm runts scripts/remove-template-lock-files.ts",
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
"tsconfig.base.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@next/bundle-analyzer": "15.3.0",
|
||||
"@next/bundle-analyzer": "15.3.2",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
@@ -128,13 +135,13 @@
|
||||
"@playwright/test": "1.50.0",
|
||||
"@sentry/nextjs": "^8.33.1",
|
||||
"@sentry/node": "^8.33.1",
|
||||
"@swc-node/register": "1.10.9",
|
||||
"@swc/cli": "0.6.0",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/cli": "0.7.7",
|
||||
"@swc/jest": "0.2.38",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/shelljs": "0.8.15",
|
||||
@@ -144,9 +151,10 @@
|
||||
"create-payload-app": "workspace:*",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"drizzle-kit": "0.31.0",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"eslint": "9.22.0",
|
||||
"execa": "5.1.1",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
@@ -155,10 +163,11 @@
|
||||
"jest": "29.7.0",
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^10",
|
||||
"next": "15.3.0",
|
||||
"mongodb-memory-server": "10.1.4",
|
||||
"next": "15.3.2",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"pg": "8.11.3",
|
||||
"playwright": "1.50.0",
|
||||
"playwright-core": "1.50.0",
|
||||
"prettier": "3.5.3",
|
||||
@@ -169,33 +178,28 @@
|
||||
"shelljs": "0.8.5",
|
||||
"slash": "3.0.0",
|
||||
"sort-package-json": "^2.10.0",
|
||||
"swc-plugin-transform-remove-imports": "3.1.0",
|
||||
"swc-plugin-transform-remove-imports": "4.0.4",
|
||||
"tempy": "1.0.1",
|
||||
"tstyche": "^3.1.1",
|
||||
"tsx": "4.19.2",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"packageManager": "pnpm@10.12.1",
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0",
|
||||
"pnpm": "^9.7.0"
|
||||
"pnpm": "^10.12.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"copyfiles": "$copyfiles",
|
||||
"cross-env": "$cross-env",
|
||||
"dotenv": "$dotenv",
|
||||
"drizzle-orm": "$drizzle-orm",
|
||||
"graphql": "^16.8.1",
|
||||
"mongodb-memory-server": "$mongodb-memory-server",
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom",
|
||||
"typescript": "$typescript"
|
||||
}
|
||||
},
|
||||
"workspaces:": [
|
||||
"packages/*",
|
||||
"test/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -16,6 +16,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./types": {
|
||||
@@ -60,7 +61,7 @@
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
"@swc/core": "1.10.12",
|
||||
"@swc/core": "1.11.29",
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"comment-json": "^4.2.3",
|
||||
@@ -76,7 +77,7 @@
|
||||
"@types/esprima": "^4.0.6",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "22.5.4"
|
||||
"@types/node": "22.15.30"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
|
||||
@@ -16,12 +16,15 @@ export const configurePluginProject = ({
|
||||
const devPayloadConfigPath = path.resolve(projectDirPath, './dev/payload.config.ts')
|
||||
const devTsConfigPath = path.resolve(projectDirPath, './dev/tsconfig.json')
|
||||
const indexTsPath = path.resolve(projectDirPath, './src/index.ts')
|
||||
const devImportMapPath = path.resolve(projectDirPath, './dev/app/(payload)/admin/importMap.js')
|
||||
|
||||
const devPayloadConfig = fse.readFileSync(devPayloadConfigPath, 'utf8')
|
||||
const devTsConfig = fse.readFileSync(devTsConfigPath, 'utf8')
|
||||
const indexTs = fse.readFileSync(indexTsPath, 'utf-8')
|
||||
const devImportMap = fse.readFileSync(devImportMapPath, 'utf-8')
|
||||
|
||||
const updatedTsConfig = devTsConfig.replaceAll('plugin-package-name-placeholder', projectName)
|
||||
const updatedImportMap = devImportMap.replaceAll('plugin-package-name-placeholder', projectName)
|
||||
let updatedIndexTs = indexTs.replaceAll('plugin-package-name-placeholder', projectName)
|
||||
|
||||
const pluginExportVariableName = toCamelCase(projectName)
|
||||
@@ -43,4 +46,5 @@ export const configurePluginProject = ({
|
||||
fse.writeFileSync(devPayloadConfigPath, updatedPayloadConfig)
|
||||
fse.writeFileSync(devTsConfigPath, updatedTsConfig)
|
||||
fse.writeFileSync(indexTsPath, updatedIndexTs)
|
||||
fse.writeFileSync(devImportMapPath, updatedImportMap)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import type { CliArgs, DbType, ProjectExample, ProjectTemplate } from '../types.
|
||||
import { createProject } from './create-project.js'
|
||||
import { dbReplacements } from './replacements.js'
|
||||
import { getValidTemplates } from './templates.js'
|
||||
import { manageEnvFiles } from './manage-env-files.js'
|
||||
|
||||
describe('createProject', () => {
|
||||
let projectDir: string
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log = jest.fn()
|
||||
@@ -63,6 +63,30 @@ describe('createProject', () => {
|
||||
expect(packageJson.name).toStrictEqual(projectName)
|
||||
})
|
||||
|
||||
it('updates project name in plugin template importMap file', async () => {
|
||||
const projectName = 'my-custom-plugin'
|
||||
const template: ProjectTemplate = {
|
||||
name: 'plugin',
|
||||
type: 'plugin',
|
||||
description: 'Template for creating a Payload plugin',
|
||||
url: 'https://github.com/payloadcms/payload/templates/plugin',
|
||||
}
|
||||
|
||||
await createProject({
|
||||
cliArgs: { ...args, '--local-template': 'plugin' } as CliArgs,
|
||||
packageManager,
|
||||
projectDir,
|
||||
projectName,
|
||||
template,
|
||||
})
|
||||
|
||||
const importMapPath = path.resolve(projectDir, './dev/app/(payload)/admin/importMap.js')
|
||||
const importMapFile = fse.readFileSync(importMapPath, 'utf-8')
|
||||
|
||||
expect(importMapFile).not.toContain('plugin-package-name-placeholder')
|
||||
expect(importMapFile).toContain('my-custom-plugin')
|
||||
})
|
||||
|
||||
it('creates example', async () => {
|
||||
const projectName = 'custom-server-example'
|
||||
const example: ProjectExample = {
|
||||
@@ -155,75 +179,5 @@ describe('createProject', () => {
|
||||
expect(content).toContain(dbReplacement.configReplacement().join('\n'))
|
||||
})
|
||||
})
|
||||
describe('managing env files', () => {
|
||||
it('updates .env files without overwriting existing data', async () => {
|
||||
const envFilePath = path.join(projectDir, '.env')
|
||||
const envExampleFilePath = path.join(projectDir, '.env.example')
|
||||
|
||||
fse.ensureDirSync(projectDir)
|
||||
fse.ensureFileSync(envFilePath)
|
||||
fse.ensureFileSync(envExampleFilePath)
|
||||
|
||||
const initialEnvContent = `CUSTOM_VAR=custom-value\nDATABASE_URI=old-connection\n`
|
||||
const initialEnvExampleContent = `CUSTOM_VAR=custom-value\nDATABASE_URI=old-connection\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
|
||||
|
||||
fse.writeFileSync(envFilePath, initialEnvContent)
|
||||
fse.writeFileSync(envExampleFilePath, initialEnvExampleContent)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseType: 'mongodb',
|
||||
databaseUri: 'mongodb://localhost:27017/test',
|
||||
payloadSecret: 'test-secret',
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toContain('CUSTOM_VAR=custom-value')
|
||||
expect(updatedEnvContent).toContain('DATABASE_URI=mongodb://localhost:27017/test')
|
||||
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=test-secret')
|
||||
|
||||
const updatedEnvExampleContent = fse.readFileSync(envExampleFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvExampleContent).toContain('CUSTOM_VAR=custom-value')
|
||||
expect(updatedEnvContent).toContain('DATABASE_URI=mongodb://localhost:27017/test')
|
||||
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=test-secret')
|
||||
})
|
||||
|
||||
it('creates .env and .env.example if they do not exist', async () => {
|
||||
const envFilePath = path.join(projectDir, '.env')
|
||||
const envExampleFilePath = path.join(projectDir, '.env.example')
|
||||
|
||||
fse.ensureDirSync(projectDir)
|
||||
|
||||
if (fse.existsSync(envFilePath)) fse.removeSync(envFilePath)
|
||||
if (fse.existsSync(envExampleFilePath)) fse.removeSync(envExampleFilePath)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseUri: '',
|
||||
payloadSecret: '',
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
expect(fse.existsSync(envFilePath)).toBe(true)
|
||||
expect(fse.existsSync(envExampleFilePath)).toBe(true)
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
expect(updatedEnvContent).toContain('DATABASE_URI=your-connection-string-here')
|
||||
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=YOUR_SECRET_HERE')
|
||||
|
||||
const updatedEnvExampleContent = fse.readFileSync(envExampleFilePath, 'utf-8')
|
||||
expect(updatedEnvExampleContent).toContain('DATABASE_URI=your-connection-string-here')
|
||||
expect(updatedEnvExampleContent).toContain('PAYLOAD_SECRET=YOUR_SECRET_HERE')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -144,17 +144,14 @@ export async function createProject(
|
||||
}
|
||||
}
|
||||
|
||||
// Call manageEnvFiles before initializing Git
|
||||
if (dbDetails) {
|
||||
await manageEnvFiles({
|
||||
cliArgs,
|
||||
databaseType: dbDetails.type,
|
||||
databaseUri: dbDetails.dbUri,
|
||||
payloadSecret: generateSecret(),
|
||||
projectDir,
|
||||
template: 'template' in args ? args.template : undefined,
|
||||
})
|
||||
}
|
||||
await manageEnvFiles({
|
||||
cliArgs,
|
||||
databaseType: dbDetails?.type,
|
||||
databaseUri: dbDetails?.dbUri,
|
||||
payloadSecret: generateSecret(),
|
||||
projectDir,
|
||||
template: 'template' in args ? args.template : undefined,
|
||||
})
|
||||
|
||||
// Remove yarn.lock file. This is only desired in Payload Cloud.
|
||||
const lockPath = path.resolve(projectDir, 'pnpm-lock.yaml')
|
||||
|
||||
@@ -7,7 +7,7 @@ export async function getExamples({ branch }: { branch: string }): Promise<Proje
|
||||
|
||||
const response = await fetch(url)
|
||||
|
||||
const examplesResponseList: { name: string; path: string }[] = await response.json()
|
||||
const examplesResponseList = (await response.json()) as { name: string; path: string }[]
|
||||
|
||||
const examples: ProjectExample[] = examplesResponseList.map((example) => ({
|
||||
name: example.name,
|
||||
|
||||
165
packages/create-payload-app/src/lib/manage-env-files.spec.ts
Normal file
165
packages/create-payload-app/src/lib/manage-env-files.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import fs from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import * as os from 'node:os'
|
||||
import path from 'path'
|
||||
|
||||
import type { CliArgs } from '../types.js'
|
||||
|
||||
import { manageEnvFiles } from './manage-env-files.js'
|
||||
|
||||
describe('createProject', () => {
|
||||
let projectDir: string
|
||||
let envFilePath = ''
|
||||
let envExampleFilePath = ''
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log = jest.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const tempDirectory = fs.realpathSync(os.tmpdir())
|
||||
projectDir = `${tempDirectory}/${Math.random().toString(36).substring(7)}`
|
||||
|
||||
envFilePath = path.join(projectDir, '.env')
|
||||
envExampleFilePath = path.join(projectDir, '.env.example')
|
||||
|
||||
if (fse.existsSync(envFilePath)) {
|
||||
fse.removeSync(envFilePath)
|
||||
}
|
||||
|
||||
fse.ensureDirSync(projectDir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (fse.existsSync(projectDir)) {
|
||||
fse.rmSync(projectDir, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('generates .env using defaults (not from .env.example)', async () => {
|
||||
// ensure no .env.example exists so that the default values are used
|
||||
// the `manageEnvFiles` function will look for .env.example in the file system
|
||||
if (fse.existsSync(envExampleFilePath)) {
|
||||
fse.removeSync(envExampleFilePath)
|
||||
}
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseUri: '', // omitting this will ensure the default vars are used
|
||||
payloadSecret: '', // omitting this will ensure the default vars are used
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
expect(fse.existsSync(envFilePath)).toBe(true)
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=YOUR_SECRET_HERE\nDATABASE_URI=your-connection-string-here`,
|
||||
)
|
||||
})
|
||||
|
||||
it('generates .env from .env.example', async () => {
|
||||
// create or override the .env.example file with a connection string that will NOT be overridden
|
||||
fse.ensureFileSync(envExampleFilePath)
|
||||
fse.writeFileSync(
|
||||
envExampleFilePath,
|
||||
`DATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value\n`,
|
||||
)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseUri: '', // omitting this will ensure the `.env.example` vars are used
|
||||
payloadSecret: '', // omitting this will ensure the `.env.example` vars are used
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
expect(fse.existsSync(envFilePath)).toBe(true)
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`DATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n# Added by Payload`,
|
||||
)
|
||||
})
|
||||
|
||||
it('updates existing .env without overriding vars', async () => {
|
||||
// create an existing .env file with some custom variables that should NOT be overridden
|
||||
fse.ensureFileSync(envFilePath)
|
||||
fse.writeFileSync(
|
||||
envFilePath,
|
||||
`CUSTOM_VAR=custom-value\nDATABASE_URI=example-connection-string\n`,
|
||||
)
|
||||
|
||||
// create an .env.example file to ensure that its contents DO NOT override existing .env vars
|
||||
fse.ensureFileSync(envExampleFilePath)
|
||||
fse.writeFileSync(
|
||||
envExampleFilePath,
|
||||
`CUSTOM_VAR=custom-value-2\nDATABASE_URI=example-connection-string-2\n`,
|
||||
)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseUri: '', // omitting this will ensure the `.env` vars are kept
|
||||
payloadSecret: '', // omitting this will ensure the `.env` vars are kept
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
expect(fse.existsSync(envFilePath)).toBe(true)
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=YOUR_SECRET_HERE\nDATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value`,
|
||||
)
|
||||
})
|
||||
|
||||
it('sanitizes .env based on selected database type', async () => {
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseType: 'mongodb', // this mimics the CLI selection and will be used as the DATABASE_URI
|
||||
databaseUri: 'mongodb://localhost:27017/test', // this mimics the CLI selection and will be used as the DATABASE_URI
|
||||
payloadSecret: 'test-secret', // this mimics the CLI selection and will be used as the PAYLOAD_SECRET
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=test-secret\nDATABASE_URI=mongodb://localhost:27017/test`,
|
||||
)
|
||||
|
||||
// delete the generated .env file and do it again, but this time, omit the databaseUri to ensure the default is generated
|
||||
fse.removeSync(envFilePath)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseType: 'mongodb', // this mimics the CLI selection and will be used as the DATABASE_URI
|
||||
databaseUri: '', // omit this to ensure the default is generated based on the selected database type
|
||||
payloadSecret: 'test-secret',
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
const updatedEnvContentWithDefault = fse.readFileSync(envFilePath, 'utf-8')
|
||||
expect(updatedEnvContentWithDefault).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=test-secret\nDATABASE_URI=mongodb://127.0.0.1/your-database-name`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -6,21 +6,42 @@ import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
|
||||
import { debug, error } from '../utils/log.js'
|
||||
import { dbChoiceRecord } from './select-db.js'
|
||||
|
||||
const updateEnvExampleVariables = (
|
||||
contents: string,
|
||||
databaseType: DbType | undefined,
|
||||
payloadSecret?: string,
|
||||
databaseUri?: string,
|
||||
): string => {
|
||||
const sanitizeEnv = ({
|
||||
contents,
|
||||
databaseType,
|
||||
databaseUri,
|
||||
payloadSecret,
|
||||
}: {
|
||||
contents: string
|
||||
databaseType: DbType | undefined
|
||||
databaseUri?: string
|
||||
payloadSecret?: string
|
||||
}): string => {
|
||||
const seenKeys = new Set<string>()
|
||||
const updatedEnv = contents
|
||||
|
||||
// add defaults
|
||||
let withDefaults = contents
|
||||
|
||||
if (
|
||||
!contents.includes('DATABASE_URI') &&
|
||||
!contents.includes('POSTGRES_URL') &&
|
||||
!contents.includes('MONGODB_URI')
|
||||
) {
|
||||
withDefaults += '\nDATABASE_URI=your-connection-string-here'
|
||||
}
|
||||
|
||||
if (!contents.includes('PAYLOAD_SECRET')) {
|
||||
withDefaults += '\nPAYLOAD_SECRET=YOUR_SECRET_HERE'
|
||||
}
|
||||
|
||||
let updatedEnv = withDefaults
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (line.startsWith('#') || !line.includes('=')) {
|
||||
return line
|
||||
}
|
||||
|
||||
const [key] = line.split('=')
|
||||
const [key, value] = line.split('=')
|
||||
|
||||
if (!key) {
|
||||
return
|
||||
@@ -28,6 +49,7 @@ const updateEnvExampleVariables = (
|
||||
|
||||
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
|
||||
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
|
||||
|
||||
if (dbChoice) {
|
||||
const placeholderUri = databaseUri
|
||||
? databaseUri
|
||||
@@ -36,6 +58,8 @@ const updateEnvExampleVariables = (
|
||||
databaseType === 'vercel-postgres'
|
||||
? `POSTGRES_URL=${placeholderUri}`
|
||||
: `DATABASE_URI=${placeholderUri}`
|
||||
} else {
|
||||
line = `${key}=${value}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +80,10 @@ const updateEnvExampleVariables = (
|
||||
.reverse()
|
||||
.join('\n')
|
||||
|
||||
if (!updatedEnv.includes('# Added by Payload')) {
|
||||
updatedEnv = `# Added by Payload\n${updatedEnv}`
|
||||
}
|
||||
|
||||
return updatedEnv
|
||||
}
|
||||
|
||||
@@ -63,7 +91,7 @@ const updateEnvExampleVariables = (
|
||||
export async function manageEnvFiles(args: {
|
||||
cliArgs: CliArgs
|
||||
databaseType?: DbType
|
||||
databaseUri: string
|
||||
databaseUri?: string
|
||||
payloadSecret: string
|
||||
projectDir: string
|
||||
template?: ProjectTemplate
|
||||
@@ -77,70 +105,63 @@ export async function manageEnvFiles(args: {
|
||||
return
|
||||
}
|
||||
|
||||
const envExamplePath = path.join(projectDir, '.env.example')
|
||||
const pathToEnvExample = path.join(projectDir, '.env.example')
|
||||
const envPath = path.join(projectDir, '.env')
|
||||
const emptyEnvContent = `# Added by Payload\nDATABASE_URI=your-connection-string-here\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
|
||||
try {
|
||||
let updatedExampleContents: string
|
||||
|
||||
let exampleEnv: null | string = ''
|
||||
|
||||
try {
|
||||
if (template?.type === 'plugin') {
|
||||
if (debugFlag) {
|
||||
debug(`plugin template detected - no .env added .env.example added`)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!fs.existsSync(envExamplePath)) {
|
||||
updatedExampleContents = updateEnvExampleVariables(
|
||||
emptyEnvContent,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
// If there's a .env.example file, use it to create or update the .env file
|
||||
if (fs.existsSync(pathToEnvExample)) {
|
||||
const envExampleContents = await fs.readFile(pathToEnvExample, 'utf8')
|
||||
|
||||
await fs.writeFile(envExamplePath, updatedExampleContents)
|
||||
if (debugFlag) {
|
||||
debug(`.env.example file successfully created`)
|
||||
}
|
||||
} else {
|
||||
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
|
||||
const mergedEnvs = envExampleContents + '\n' + emptyEnvContent
|
||||
updatedExampleContents = updateEnvExampleVariables(
|
||||
mergedEnvs,
|
||||
exampleEnv = sanitizeEnv({
|
||||
contents: envExampleContents,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
payloadSecret,
|
||||
})
|
||||
|
||||
await fs.writeFile(envExamplePath, updatedExampleContents)
|
||||
if (debugFlag) {
|
||||
debug(`.env.example file successfully updated`)
|
||||
debug(`.env.example file successfully read`)
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no .env file, create it using the .env.example content (if it exists)
|
||||
if (!fs.existsSync(envPath)) {
|
||||
const envContent = updateEnvExampleVariables(
|
||||
emptyEnvContent,
|
||||
const envContent = sanitizeEnv({
|
||||
contents: exampleEnv,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
payloadSecret,
|
||||
})
|
||||
|
||||
await fs.writeFile(envPath, envContent)
|
||||
|
||||
if (debugFlag) {
|
||||
debug(`.env file successfully created`)
|
||||
}
|
||||
} else {
|
||||
// If the .env file already exists, sanitize it as-is
|
||||
const envContents = await fs.readFile(envPath, 'utf8')
|
||||
const mergedEnvs = envContents + '\n' + emptyEnvContent
|
||||
const updatedEnvContents = updateEnvExampleVariables(
|
||||
mergedEnvs,
|
||||
|
||||
const updatedEnvContents = sanitizeEnv({
|
||||
contents: envContents,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
payloadSecret,
|
||||
})
|
||||
|
||||
await fs.writeFile(envPath, updatedEnvContents)
|
||||
|
||||
if (debugFlag) {
|
||||
debug(`.env file successfully updated`)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function getLatestPackageVersion({
|
||||
}) {
|
||||
try {
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}`)
|
||||
const data = await response.json()
|
||||
const data = (await response.json()) as { 'dist-tags': { latest: string } }
|
||||
const latestVersion = data['dist-tags'].latest
|
||||
|
||||
if (debug) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
// Do not include DOM and DOM.Iterable as this is a server-only package.
|
||||
"lib": ["ES2022"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -46,7 +47,7 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongoose": "8.9.5",
|
||||
"mongoose": "8.15.1",
|
||||
"mongoose-paginate-v2": "1.8.5",
|
||||
"prompts": "2.4.2",
|
||||
"uuid": "10.0.0"
|
||||
@@ -56,8 +57,8 @@
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
|
||||
"@types/prompts": "^2.4.5",
|
||||
"@types/uuid": "10.0.0",
|
||||
"mongodb": "6.12.0",
|
||||
"mongodb-memory-server": "^10",
|
||||
"mongodb": "6.16.0",
|
||||
"mongodb-memory-server": "10.1.4",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -40,6 +40,9 @@ export const connect: Connect = async function connect(
|
||||
// If we are running a replica set with MongoDB Memory Server,
|
||||
// wait until the replica set elects a primary before proceeding
|
||||
if (this.mongoMemoryServer) {
|
||||
this.payload.logger.info(
|
||||
'Waiting for MongoDB Memory Server replica set to elect a primary...',
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
}
|
||||
|
||||
@@ -50,7 +53,7 @@ export const connect: Connect = async function connect(
|
||||
this.beginTransaction = defaultBeginTransaction()
|
||||
}
|
||||
|
||||
if (!this.mongoMemoryServer && !hotReload) {
|
||||
if (!hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info('---- DROPPING DATABASE ----')
|
||||
await mongoose.connection.dropDatabase()
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
import { buildVersionCollectionFields, type DeleteVersions } from 'payload'
|
||||
import type { DeleteVersions, FlattenedField } from 'payload'
|
||||
|
||||
import { APIError, buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
import type { CollectionModel } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getCollection, getGlobal } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteVersions: DeleteVersions = async function deleteVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection: collectionSlug, locale, req, where },
|
||||
{ collection: collectionSlug, globalSlug, locale, req, where },
|
||||
) {
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
let fields: FlattenedField[]
|
||||
let VersionsModel: CollectionModel
|
||||
|
||||
if (globalSlug) {
|
||||
const { globalConfig, Model } = getGlobal({
|
||||
adapter: this,
|
||||
globalSlug,
|
||||
versions: true,
|
||||
})
|
||||
fields = buildVersionGlobalFields(this.payload.config, globalConfig, true)
|
||||
VersionsModel = Model
|
||||
} else if (collectionSlug) {
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
VersionsModel = Model
|
||||
} else {
|
||||
throw new APIError('Either collection or globalSlug must be passed.')
|
||||
}
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
fields,
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
await Model.deleteMany(query, { session })
|
||||
await VersionsModel.deleteMany(query, { session })
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import mongoose from 'mongoose'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
export const destroy: Destroy = async function destroy(this: MongooseAdapter) {
|
||||
if (this.mongoMemoryServer) {
|
||||
await this.mongoMemoryServer.stop()
|
||||
} else {
|
||||
await mongoose.disconnect()
|
||||
}
|
||||
await mongoose.disconnect()
|
||||
|
||||
Object.keys(mongoose.models).map((model) => mongoose.deleteModel(model))
|
||||
}
|
||||
|
||||
@@ -245,10 +245,13 @@ export async function buildSearchParam({
|
||||
value: { $in },
|
||||
}
|
||||
} else {
|
||||
relationshipQuery = {
|
||||
value: {
|
||||
_id: { $in },
|
||||
},
|
||||
const nextSubPath = pathsToQuery[i + 1]?.path
|
||||
if (nextSubPath) {
|
||||
relationshipQuery = {
|
||||
value: {
|
||||
[nextSubPath]: { $in },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ export const sanitizeQueryValue = ({
|
||||
return buildExistsQuery(
|
||||
formattedValue,
|
||||
path,
|
||||
!['relationship', 'upload'].includes(field.type),
|
||||
!['checkbox', 'relationship', 'upload'].includes(field.type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { BaseJob, UpdateJobs, Where } from 'payload'
|
||||
import type { Job, UpdateJobs, Where } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
@@ -47,7 +47,7 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
|
||||
transform({ adapter: this, data, fields: collectionConfig.fields, operation: 'write' })
|
||||
|
||||
let result: BaseJob[] = []
|
||||
let result: Job[] = []
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"references": [{ "path": "../payload" }, { "path": "../translations" }]
|
||||
"references": [{ "path": "../payload" }, { "path": "../translations" }],
|
||||
"compilerOptions": {
|
||||
// Do not include DOM and DOM.Iterable as this is a server-only package.
|
||||
"lib": ["ES2022"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -77,18 +78,18 @@
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"drizzle-kit": "0.31.1",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild": "0.25.5",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect, Migration, Payload } from 'payload'
|
||||
import type { Connect, Migration } from 'payload'
|
||||
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { withReplicas } from 'drizzle-orm/pg-core'
|
||||
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
|
||||
const connectWithReconnect = async function ({
|
||||
adapter,
|
||||
payload,
|
||||
pool,
|
||||
reconnect = false,
|
||||
}: {
|
||||
adapter: PostgresAdapter
|
||||
payload: Payload
|
||||
pool: PostgresAdapter['pool']
|
||||
reconnect?: boolean
|
||||
}) {
|
||||
let result
|
||||
|
||||
if (!reconnect) {
|
||||
result = await adapter.pool.connect()
|
||||
result = await pool.connect()
|
||||
} else {
|
||||
try {
|
||||
result = await adapter.pool.connect()
|
||||
result = await pool.connect()
|
||||
} catch (ignore) {
|
||||
setTimeout(() => {
|
||||
payload.logger.info('Reconnecting to postgres')
|
||||
void connectWithReconnect({ adapter, payload, reconnect: true })
|
||||
adapter.payload.logger.info('Reconnecting to postgres')
|
||||
void connectWithReconnect({ adapter, pool, reconnect: true })
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +36,7 @@ const connectWithReconnect = async function ({
|
||||
result.prependListener('error', (err) => {
|
||||
try {
|
||||
if (err.code === 'ECONNRESET') {
|
||||
void connectWithReconnect({ adapter, payload, reconnect: true })
|
||||
void connectWithReconnect({ adapter, pool, reconnect: true })
|
||||
}
|
||||
} catch (ignore) {
|
||||
// swallow error
|
||||
@@ -51,22 +52,32 @@ export const connect: Connect = async function connect(
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.pool) {
|
||||
this.pool = new this.pg.Pool(this.poolOptions)
|
||||
await connectWithReconnect({ adapter: this, payload: this.payload })
|
||||
await connectWithReconnect({ adapter: this, pool: this.pool })
|
||||
}
|
||||
|
||||
const logger = this.logger || false
|
||||
this.drizzle = drizzle({ client: this.pool, logger, schema: this.schema })
|
||||
|
||||
if (this.readReplicaOptions) {
|
||||
const readReplicas = this.readReplicaOptions.map((connectionString) => {
|
||||
const options = {
|
||||
...this.poolOptions,
|
||||
connectionString,
|
||||
}
|
||||
const pool = new this.pg.Pool(options)
|
||||
void connectWithReconnect({
|
||||
adapter: this,
|
||||
pool,
|
||||
})
|
||||
return drizzle({ client: pool, logger, schema: this.schema })
|
||||
})
|
||||
const myReplicas = withReplicas(this.drizzle, readReplicas as any)
|
||||
this.drizzle = myReplicas
|
||||
}
|
||||
|
||||
if (!hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
|
||||
|
||||
@@ -99,6 +99,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
allowIDOnCreate,
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
createMigration: buildCreateMigration({
|
||||
@@ -139,6 +140,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
prodMigrations: args.prodMigrations,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
push: args.push,
|
||||
readReplicaOptions: args.readReplicas,
|
||||
relations: {},
|
||||
relationshipsSuffix: args.relationshipsSuffix || '_rels',
|
||||
schema: {},
|
||||
|
||||
@@ -3,13 +3,19 @@ import type {
|
||||
GenericEnum,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
PostgresDB,
|
||||
PostgresSchemaHook,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleConfig } from 'drizzle-orm'
|
||||
import type { DrizzleConfig, ExtractTablesWithRelations } from 'drizzle-orm'
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
|
||||
import type {
|
||||
PgDatabase,
|
||||
PgQueryResultHKT,
|
||||
PgSchema,
|
||||
PgTableFn,
|
||||
PgTransactionConfig,
|
||||
PgWithReplicas,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { Pool, PoolConfig } from 'pg'
|
||||
|
||||
type PgDependency = typeof import('pg')
|
||||
@@ -35,6 +41,10 @@ export type Args = {
|
||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
*/
|
||||
beforeSchemaInit?: PostgresSchemaHook[]
|
||||
/**
|
||||
* Store blocks as JSON column instead of storing them in relational structure.
|
||||
*/
|
||||
blocksAsJSON?: boolean
|
||||
/**
|
||||
* Pass `true` to disale auto database creation if it doesn't exist.
|
||||
* @default false
|
||||
@@ -55,6 +65,7 @@ export type Args = {
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push?: boolean
|
||||
readReplicas?: string[]
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
@@ -74,7 +85,16 @@ type ResolveSchemaType<T> = 'schema' extends keyof T
|
||||
? T['schema']
|
||||
: GeneratedDatabaseSchema['schemaUntyped']
|
||||
|
||||
type Drizzle = NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
|
||||
type Drizzle =
|
||||
| NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
|
||||
| PgWithReplicas<
|
||||
PgDatabase<
|
||||
PgQueryResultHKT,
|
||||
Record<string, unknown>,
|
||||
ExtractTablesWithRelations<Record<string, unknown>>
|
||||
>
|
||||
>
|
||||
|
||||
export type PostgresAdapter = {
|
||||
drizzle: Drizzle
|
||||
pg: PgDependency
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
{
|
||||
"path": "../drizzle"
|
||||
}
|
||||
]
|
||||
],
|
||||
"compilerOptions": {
|
||||
// Do not include DOM and DOM.Iterable as this is a server-only package.
|
||||
"lib": ["ES2022"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -75,8 +76,8 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"drizzle-kit": "0.31.1",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
|
||||
@@ -15,11 +15,6 @@ export const connect: Connect = async function connect(
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.client) {
|
||||
this.client = createClient(this.clientConfig)
|
||||
|
||||
@@ -89,6 +89,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
allowIDOnCreate,
|
||||
autoIncrement: args.autoIncrement ?? false,
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
client: undefined,
|
||||
clientConfig: args.client,
|
||||
|
||||
@@ -36,4 +36,9 @@ export const init: Init = async function init(this: SQLiteAdapter) {
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||
|
||||
this.schema = {
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ export type Args = {
|
||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
*/
|
||||
beforeSchemaInit?: SQLiteSchemaHook[]
|
||||
/**
|
||||
* Store blocks as JSON column instead of storing them in relational structure.
|
||||
*/
|
||||
blocksAsJSON?: boolean
|
||||
client: Config
|
||||
/** Generated schema from payload generate:db-schema file path */
|
||||
generateSchemaOutputFile?: string
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
{
|
||||
"path": "../drizzle"
|
||||
}
|
||||
]
|
||||
],
|
||||
"compilerOptions": {
|
||||
// Do not include DOM and DOM.Iterable as this is a server-only package.
|
||||
"lib": ["ES2022"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -77,19 +78,19 @@
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@vercel/postgres": "^0.9.0",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"drizzle-kit": "0.31.1",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild": "0.25.5",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Connect, Migration } from 'payload'
|
||||
import { pushDevSchema } from '@payloadcms/drizzle'
|
||||
import { sql, VercelPool } from '@vercel/postgres'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { withReplicas } from 'drizzle-orm/pg-core'
|
||||
import pg from 'pg'
|
||||
|
||||
import type { VercelPostgresAdapter } from './types.js'
|
||||
@@ -16,13 +17,6 @@ export const connect: Connect = async function connect(
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
}
|
||||
|
||||
try {
|
||||
const logger = this.logger || false
|
||||
|
||||
@@ -53,6 +47,19 @@ export const connect: Connect = async function connect(
|
||||
schema: this.schema,
|
||||
})
|
||||
|
||||
if (this.readReplicaOptions) {
|
||||
const readReplicas = this.readReplicaOptions.map((connectionString) => {
|
||||
const options = {
|
||||
...this.poolOptions,
|
||||
connectionString,
|
||||
}
|
||||
const pool = new VercelPool(options)
|
||||
return drizzle({ client: pool, logger, schema: this.schema })
|
||||
})
|
||||
const myReplicas = withReplicas(this.drizzle, readReplicas as any)
|
||||
this.drizzle = myReplicas
|
||||
}
|
||||
|
||||
if (!hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
|
||||
|
||||
@@ -95,6 +95,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
allowIDOnCreate,
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
defaultDrizzleSnapshot,
|
||||
@@ -174,6 +175,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
readReplicaOptions: args.readReplicas,
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
findOne,
|
||||
findVersions,
|
||||
|
||||
@@ -33,6 +33,10 @@ export type Args = {
|
||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||
*/
|
||||
beforeSchemaInit?: PostgresSchemaHook[]
|
||||
/**
|
||||
* Store blocks as JSON column instead of storing them in relational structure.
|
||||
*/
|
||||
blocksAsJSON?: boolean
|
||||
connectionString?: string
|
||||
/**
|
||||
* Pass `true` to disale auto database creation if it doesn't exist.
|
||||
@@ -64,6 +68,7 @@ export type Args = {
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push?: boolean
|
||||
readReplicas?: string[]
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
{
|
||||
"path": "../drizzle"
|
||||
}
|
||||
]
|
||||
],
|
||||
"compilerOptions": {
|
||||
// Do not include DOM and DOM.Iterable as this is a server-only package.
|
||||
"lib": ["ES2022"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.39.1",
|
||||
"version": "3.43.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -54,7 +55,7 @@
|
||||
"dependencies": {
|
||||
"console-table-printer": "2.12.1",
|
||||
"dequal": "2.0.3",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DeleteVersions, SanitizedCollectionConfig } from 'payload'
|
||||
import type { DeleteVersions, FlattenedField, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
import { APIError, buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
@@ -11,16 +11,27 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteVersions: DeleteVersions = async function deleteVersion(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, locale, req, where: where },
|
||||
{ collection: collectionSlug, globalSlug, locale, req, where: where },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
let tableName: string
|
||||
let fields: FlattenedField[]
|
||||
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
if (globalSlug) {
|
||||
const globalConfig = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
|
||||
tableName = this.tableNameMap.get(`_${toSnakeCase(globalSlug)}${this.versionsSuffix}`)
|
||||
fields = buildVersionGlobalFields(this.payload.config, globalConfig, true)
|
||||
} else if (collectionSlug) {
|
||||
const collectionConfig: SanitizedCollectionConfig =
|
||||
this.payload.collections[collectionSlug].config
|
||||
tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
} else {
|
||||
throw new APIError('Either collection or globalSlug must be passed.')
|
||||
}
|
||||
|
||||
const { docs } = await findMany({
|
||||
adapter: this,
|
||||
|
||||
@@ -252,6 +252,20 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (adapter.blocksAsJSON) {
|
||||
if (select || selectAllOnCurrentLevel) {
|
||||
const fieldPath = `${path}${field.name}`
|
||||
|
||||
if ((isFieldLocalized || parentIsLocalized) && _locales) {
|
||||
_locales.columns[fieldPath] = true
|
||||
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
|
||||
currentArgs.columns[fieldPath] = true
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
const blockKey = `_blocks_${block.slug}${!block[InternalBlockTableNameIndex] ? '' : `_${block[InternalBlockTableNameIndex]}`}`
|
||||
|
||||
@@ -35,4 +35,11 @@ export const init: Init = async function init(this: BasePostgresAdapter) {
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ export type BasePostgresAdapter = {
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push: boolean
|
||||
readReplicaOptions?: string[]
|
||||
rejectInitializing: () => void
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
|
||||
@@ -180,6 +180,9 @@ export const getTableColumnFromPath = ({
|
||||
})
|
||||
}
|
||||
case 'blocks': {
|
||||
if (adapter.blocksAsJSON) {
|
||||
break
|
||||
}
|
||||
let blockTableColumn: TableColumn
|
||||
let newTableName: string
|
||||
|
||||
@@ -387,6 +390,18 @@ export const getTableColumnFromPath = ({
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
|
||||
if (newCollectionPath === 'id') {
|
||||
return {
|
||||
columnName: 'parent',
|
||||
constraints,
|
||||
field: {
|
||||
name: 'id',
|
||||
type: adapter.idType === 'uuid' ? 'text' : 'number',
|
||||
} as NumberField | TextField,
|
||||
table: aliasRelationshipTable,
|
||||
}
|
||||
}
|
||||
|
||||
const relationshipConfig = adapter.payload.collections[field.collection].config
|
||||
const relationshipTableName = adapter.tableNameMap.get(
|
||||
toSnakeCase(relationshipConfig.slug),
|
||||
@@ -437,6 +452,18 @@ export const getTableColumnFromPath = ({
|
||||
table: newAliasTable,
|
||||
})
|
||||
|
||||
if (newCollectionPath === 'id') {
|
||||
return {
|
||||
columnName: 'id',
|
||||
constraints,
|
||||
field: {
|
||||
name: 'id',
|
||||
type: adapter.idType === 'uuid' ? 'text' : 'number',
|
||||
} as NumberField | TextField,
|
||||
table: newAliasTable,
|
||||
}
|
||||
}
|
||||
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
aliasTable: newAliasTable,
|
||||
|
||||
@@ -117,7 +117,8 @@ export function parseParams({
|
||||
})
|
||||
|
||||
if (
|
||||
['json', 'richText'].includes(field.type) &&
|
||||
(['json', 'richText'].includes(field.type) ||
|
||||
(field.type === 'blocks' && adapter.blocksAsJSON)) &&
|
||||
Array.isArray(pathSegments) &&
|
||||
pathSegments.length > 1
|
||||
) {
|
||||
@@ -367,7 +368,25 @@ export function parseParams({
|
||||
break
|
||||
}
|
||||
|
||||
constraints.push(adapter.operators[queryOperator](resolvedColumn, queryValue))
|
||||
const orConditions: SQL<unknown>[] = []
|
||||
let resolvedQueryValue = queryValue
|
||||
if (
|
||||
operator === 'in' &&
|
||||
Array.isArray(queryValue) &&
|
||||
queryValue.some((v) => v === null)
|
||||
) {
|
||||
orConditions.push(isNull(resolvedColumn))
|
||||
resolvedQueryValue = queryValue.filter((v) => v !== null)
|
||||
}
|
||||
let constraint = adapter.operators[queryOperator](
|
||||
resolvedColumn,
|
||||
resolvedQueryValue,
|
||||
)
|
||||
if (orConditions.length) {
|
||||
orConditions.push(constraint)
|
||||
constraint = or(...orConditions)
|
||||
}
|
||||
constraints.push(constraint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export const traverseFields = ({
|
||||
adapter.payload.config.localization &&
|
||||
(isFieldLocalized || forceLocalized) &&
|
||||
field.type !== 'array' &&
|
||||
field.type !== 'blocks' &&
|
||||
(field.type !== 'blocks' || adapter.blocksAsJSON) &&
|
||||
(('hasMany' in field && field.hasMany !== true) || !('hasMany' in field))
|
||||
) {
|
||||
hasLocalizedField = true
|
||||
@@ -370,6 +370,17 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
case 'blocks': {
|
||||
if (adapter.blocksAsJSON) {
|
||||
targetTable[fieldName] = withDefault(
|
||||
{
|
||||
name: columnName,
|
||||
type: 'jsonb',
|
||||
},
|
||||
field,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
@@ -387,6 +398,7 @@ export const traverseFields = ({
|
||||
if (typeof blocksTableNameMap[blockTableName] === 'undefined') {
|
||||
blocksTableNameMap[blockTableName] = 1
|
||||
} else if (
|
||||
!adapter.rawTables[blockTableName] ||
|
||||
!validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
|
||||
@@ -7,14 +7,6 @@ export const withDefault = (column: RawColumn, field: FieldAffectingData): RawCo
|
||||
return column
|
||||
}
|
||||
|
||||
if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) {
|
||||
const escapedString = field.defaultValue.replaceAll("'", "''")
|
||||
return {
|
||||
...column,
|
||||
default: escapedString,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...column,
|
||||
default: field.defaultValue,
|
||||
|
||||
@@ -20,13 +20,21 @@ export const transformHasManyNumber = ({
|
||||
if (withinArrayOrBlockLocale) {
|
||||
result = numberRows.reduce((acc, { locale, number }) => {
|
||||
if (locale === withinArrayOrBlockLocale) {
|
||||
if (typeof number === 'string') {
|
||||
number = Number(number)
|
||||
}
|
||||
acc.push(number)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
} else {
|
||||
result = numberRows.map(({ number }) => number)
|
||||
result = numberRows.map(({ number }) => {
|
||||
if (typeof number === 'string') {
|
||||
number = Number(number)
|
||||
}
|
||||
return number
|
||||
})
|
||||
}
|
||||
|
||||
if (locale) {
|
||||
|
||||
@@ -98,6 +98,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
withinArrayOrBlockLocale,
|
||||
}: TraverseFieldsArgs): T => {
|
||||
const sanitizedPath = path ? `${path}.` : path
|
||||
const localeCodes =
|
||||
adapter.payload.config.localization && adapter.payload.config.localization.localeCodes
|
||||
|
||||
const formatted = fields.reduce((result, field) => {
|
||||
if (fieldIsVirtual(field)) {
|
||||
@@ -219,7 +221,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
return result
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
if (field.type === 'blocks' && !adapter.blocksAsJSON) {
|
||||
const blockFieldPath = `${sanitizedPath}${field.name}`
|
||||
const blocksByPath = blocks[blockFieldPath]
|
||||
|
||||
@@ -506,6 +508,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
if (field.type === 'text' && field?.hasMany) {
|
||||
const textPathMatch = texts[`${sanitizedPath}${field.name}`]
|
||||
if (!textPathMatch) {
|
||||
result[field.name] =
|
||||
isLocalized && localeCodes
|
||||
? Object.fromEntries(localeCodes.map((locale) => [locale, []]))
|
||||
: []
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -545,6 +551,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
if (field.type === 'number' && field.hasMany) {
|
||||
const numberPathMatch = numbers[`${sanitizedPath}${field.name}`]
|
||||
if (!numberPathMatch) {
|
||||
result[field.name] =
|
||||
isLocalized && localeCodes
|
||||
? Object.fromEntries(localeCodes.map((locale) => [locale, []]))
|
||||
: []
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -606,10 +616,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
if (isLocalized && Array.isArray(table._locales)) {
|
||||
if (!table._locales.length && adapter.payload.config.localization) {
|
||||
adapter.payload.config.localization.localeCodes.forEach((_locale) =>
|
||||
(table._locales as unknown[]).push({ _locale }),
|
||||
)
|
||||
if (!table._locales.length && localeCodes) {
|
||||
localeCodes.forEach((_locale) => (table._locales as unknown[]).push({ _locale }))
|
||||
}
|
||||
|
||||
table._locales.forEach((localeRow) => {
|
||||
@@ -725,8 +733,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
return result
|
||||
}, dataRef)
|
||||
|
||||
if (Array.isArray(table._locales)) {
|
||||
|
||||
@@ -3,7 +3,13 @@ import type { FlattenedArrayField } from 'payload'
|
||||
import { fieldShouldBeLocalized } from 'payload/shared'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
import type {
|
||||
ArrayRowToInsert,
|
||||
BlockRowToInsert,
|
||||
NumberToDelete,
|
||||
RelationshipToDelete,
|
||||
TextToDelete,
|
||||
} from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
@@ -20,6 +26,7 @@ type Args = {
|
||||
field: FlattenedArrayField
|
||||
locale?: string
|
||||
numbers: Record<string, unknown>[]
|
||||
numbersToDelete: NumberToDelete[]
|
||||
parentIsLocalized: boolean
|
||||
path: string
|
||||
relationships: Record<string, unknown>[]
|
||||
@@ -28,6 +35,7 @@ type Args = {
|
||||
[tableName: string]: Record<string, unknown>[]
|
||||
}
|
||||
texts: Record<string, unknown>[]
|
||||
textsToDelete: TextToDelete[]
|
||||
/**
|
||||
* Set to a locale code if this set of fields is traversed within a
|
||||
* localized array or block field
|
||||
@@ -45,12 +53,14 @@ export const transformArray = ({
|
||||
field,
|
||||
locale,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized,
|
||||
path,
|
||||
relationships,
|
||||
relationshipsToDelete,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
}: Args) => {
|
||||
const newRows: ArrayRowToInsert[] = []
|
||||
@@ -104,6 +114,7 @@ export const transformArray = ({
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentTableName: arrayTableName,
|
||||
path: `${path || ''}${field.name}.${i}.`,
|
||||
@@ -112,6 +123,7 @@ export const transformArray = ({
|
||||
row: newRow.row,
|
||||
selects,
|
||||
texts,
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user