Compare commits
9 Commits
chore/file
...
chore/stri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbee9c4c4a | ||
|
|
ba011b8933 | ||
|
|
193c051c67 | ||
|
|
e7749468c2 | ||
|
|
115af04406 | ||
|
|
ec115c6eca | ||
|
|
4b3f1b9c92 | ||
|
|
180ef3a49d | ||
|
|
1066b434c3 |
34
.github/CODEOWNERS
vendored
34
.github/CODEOWNERS
vendored
@@ -1,34 +0,0 @@
|
||||
# Order matters. The last matching pattern takes precedence
|
||||
|
||||
## Package Exports
|
||||
|
||||
**/exports/ @denolfe @DanRibbens
|
||||
|
||||
## Packages
|
||||
|
||||
/packages/create-payload-app/src/ @denolfe
|
||||
/packages/email-*/src/ @denolfe
|
||||
/packages/eslint-*/ @denolfe @AlessioGr
|
||||
/packages/plugin-cloud-storage/src/ @denolfe
|
||||
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
|
||||
/packages/richtext-*/src/ @AlessioGr
|
||||
/packages/storage-*/src/ @denolfe
|
||||
/packages/ui/src/ @jacobsfletch @AlessioGr @JarrodMFlesch
|
||||
|
||||
## Templates
|
||||
|
||||
/templates/_data/ @denolfe
|
||||
/templates/_template/ @denolfe
|
||||
|
||||
## Build Files
|
||||
|
||||
**/jest.config.js @denolfe @AlessioGr
|
||||
**/tsconfig*.json @denolfe @AlessioGr
|
||||
|
||||
## Root
|
||||
|
||||
/.github/ @denolfe
|
||||
/.husky/ @denolfe
|
||||
/.vscode/ @denolfe @AlessioGr
|
||||
/package.json @denolfe
|
||||
/tools/ @denolfe
|
||||
4
.github/reproduction-guide.md
vendored
4
.github/reproduction-guide.md
vendored
@@ -40,7 +40,7 @@ There are a couple ways run integration tests:
|
||||
|
||||
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/.github/assets/int-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/int-debug.png" />
|
||||
|
||||
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
|
||||
|
||||
@@ -57,7 +57,7 @@ The easiest way to run E2E tests is to install
|
||||
|
||||
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/.github/assets/e2e-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
|
||||
206
.github/workflows/main.yml
vendored
206
.github/workflows/main.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: ci
|
||||
name: build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -6,7 +6,6 @@ on:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- labeled
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -163,7 +162,6 @@ jobs:
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
name: int-${{ matrix.database }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -175,7 +173,6 @@ jobs:
|
||||
- supabase
|
||||
- sqlite
|
||||
- sqlite-uuid
|
||||
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
@@ -187,8 +184,7 @@ jobs:
|
||||
|
||||
services:
|
||||
postgres:
|
||||
# Custom postgres 17 docker image that supports both pg-vector and postgis: https://github.com/payloadcms/postgis-vector
|
||||
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'ghcr.io/payloadcms/postgis-vector:latest' || '' }}
|
||||
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'postgis/postgis:16-3.4' || '' }}
|
||||
env:
|
||||
# must specify password for PG Docker container image, see: https://registry.hub.docker.com/_/postgres?tab=description&page=1&name=10
|
||||
POSTGRES_USER: ${{ env.POSTGRES_USER }}
|
||||
@@ -261,7 +257,6 @@ jobs:
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
|
||||
name: e2e-${{ matrix.suite }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -316,144 +311,6 @@ jobs:
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-import-export
|
||||
- plugin-multi-tenant
|
||||
- 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:noturbo ${{ 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-${{ 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
|
||||
|
||||
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-multi-tenant
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- sort
|
||||
@@ -514,7 +371,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-turbo${{ matrix.suite }}
|
||||
name: test-results-${{ matrix.suite }}
|
||||
path: test/test-results/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
@@ -526,32 +383,24 @@ jobs:
|
||||
# report-tag: ${{ matrix.suite }}
|
||||
# job-summary: true
|
||||
|
||||
# Build listed templates with packed local packages and then runs their int and e2e tests
|
||||
build-and-test-templates:
|
||||
# Build listed templates with packed local packages
|
||||
build-templates:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
name: build-template-${{ matrix.template }}-${{ matrix.database }}
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- template: blank
|
||||
database: mongodb
|
||||
|
||||
- template: website
|
||||
database: mongodb
|
||||
|
||||
- template: with-payload-cloud
|
||||
database: mongodb
|
||||
|
||||
- template: with-vercel-mongodb
|
||||
database: mongodb
|
||||
|
||||
# Postgres
|
||||
- template: with-postgres
|
||||
database: postgres
|
||||
|
||||
- template: with-vercel-postgres
|
||||
database: postgres
|
||||
|
||||
@@ -561,6 +410,8 @@ jobs:
|
||||
# - template: with-vercel-website
|
||||
# database: postgres
|
||||
|
||||
name: ${{ matrix.template }}-${{ matrix.database }}
|
||||
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
@@ -624,45 +475,6 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
|
||||
- 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: Runs Template Int Tests
|
||||
run: pnpm --filter ${{ matrix.template }} run test:int
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
PAYLOAD_DATABASE: ${{ matrix.database }}
|
||||
POSTGRES_URL: ${{ env.POSTGRES_URL }}
|
||||
MONGODB_URL: mongodb://localhost:27017/payloadtests
|
||||
|
||||
- name: Runs Template E2E Tests
|
||||
run: PLAYWRIGHT_JSON_OUTPUT_NAME=results_${{ matrix.template }}.json pnpm --filter ${{ matrix.template }} test:e2e
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
PAYLOAD_DATABASE: ${{ matrix.database }}
|
||||
POSTGRES_URL: ${{ env.POSTGRES_URL }}
|
||||
MONGODB_URL: mongodb://localhost:27017/payloadtests
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
tests-type-generation:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [changes, build]
|
||||
@@ -698,7 +510,7 @@ jobs:
|
||||
needs:
|
||||
- lint
|
||||
- build
|
||||
- build-and-test-templates
|
||||
- build-templates
|
||||
- tests-unit
|
||||
- tests-int
|
||||
- tests-e2e
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -6,8 +6,6 @@
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"eslint.rules.customizations": [
|
||||
// Silence some warnings that will get auto-fixed
|
||||
{ "rule": "perfectionist/*", "severity": "off", "fixable": true },
|
||||
|
||||
@@ -45,7 +45,7 @@ There are a couple ways to do this:
|
||||
|
||||
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/.github/assets/int-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/int-debug.png" />
|
||||
|
||||
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
|
||||
|
||||
@@ -62,7 +62,7 @@ The easiest way to run E2E tests is to install
|
||||
|
||||
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/.github/assets/e2e-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
|
||||
@@ -32,18 +32,18 @@ The Admin Panel serves as the entire HTTP layer for Payload, providing a full CR
|
||||
Once you [install Payload](../getting-started/installation), the following files and directories will be created in your app:
|
||||
|
||||
```plaintext
|
||||
app
|
||||
├─ (payload)
|
||||
├── admin
|
||||
├─── [[...segments]]
|
||||
app/
|
||||
├─ (payload)/
|
||||
├── admin/
|
||||
├─── [[...segments]]/
|
||||
├──── page.tsx
|
||||
├──── not-found.tsx
|
||||
├── api
|
||||
├─── [...slug]
|
||||
├── api/
|
||||
├─── [...slug]/
|
||||
├──── route.ts
|
||||
├── graphql
|
||||
├── graphql/
|
||||
├──── route.ts
|
||||
├── graphql-playground
|
||||
├── graphql-playground/
|
||||
├──── route.ts
|
||||
├── custom.scss
|
||||
├── layout.tsx
|
||||
@@ -84,30 +84,30 @@ import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
admin: {
|
||||
// highlight-line
|
||||
// ...
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| `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. |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `meta` | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| `suppressHydrationWarning` | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| `theme` | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| `timezones` | Configure the timezone settings for the admin panel. [More details](#timezones) |
|
||||
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
| Option | Description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
|
||||
| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](../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. |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| **`timezones`** | Configure the timezone settings for the admin panel. [More details](#timezones) |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
<Banner type="success">
|
||||
**Reminder:** These are the _root-level_ options for the Admin Panel. You can
|
||||
@@ -187,12 +187,6 @@ The following options are available:
|
||||
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
|
||||
| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:** Changing Root-level Routes also requires a change to [Project
|
||||
Structure](#project-structure) to match the new route. [More
|
||||
details](#customizing-root-level-routes).
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** You can easily add _new_ routes to the Admin Panel through [Custom
|
||||
Endpoints](../rest-api/overview#custom-endpoints) and [Custom
|
||||
@@ -203,29 +197,13 @@ The following options are available:
|
||||
|
||||
You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application.
|
||||
|
||||
This change, however, also requires a change to your [Project Structure](#project-structure) to match the new route.
|
||||
|
||||
For example, if you set `routes.admin` to `/`:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
routes: {
|
||||
admin: '/', // highlight-line
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then you would need to completely remove the `admin` directory from the project structure:
|
||||
Changing Root-level Routes also requires a change to [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`, you would need to completely remove the `admin` directory from the project structure:
|
||||
|
||||
```plaintext
|
||||
app
|
||||
├─ (payload)
|
||||
├── [[...segments]]
|
||||
app/
|
||||
├─ (payload)/
|
||||
├── [[...segments]]/
|
||||
├──── ...
|
||||
├── layout.tsx
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
|
||||
@@ -23,9 +23,6 @@ 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',
|
||||
|
||||
@@ -180,22 +180,19 @@ As Payload sets HTTP-only cookies, logging out cannot be done by just removing a
|
||||
**Example REST API logout**:
|
||||
|
||||
```ts
|
||||
const res = await fetch(
|
||||
'http://localhost:3000/api/[collection-slug]/logout?allSessions=false',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
const res = await fetch('http://localhost:3000/api/[collection-slug]/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
**Example GraphQL Mutation**:
|
||||
|
||||
```
|
||||
mutation {
|
||||
logoutUser(allSessions: false)
|
||||
logout[collection-singular-label]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -206,10 +203,6 @@ mutation {
|
||||
docs](../local-api/server-functions#reusable-payload-server-functions).
|
||||
</Banner>
|
||||
|
||||
#### Logging out with sessions enabled
|
||||
|
||||
By default, logging out will only end the session pertaining to the JWT that was used to log out with. However, you can pass `allSessions: true` to the logout operation in order to end all sessions for the user logging out.
|
||||
|
||||
## Refresh
|
||||
|
||||
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
|
||||
|
||||
@@ -91,7 +91,6 @@ The following options are available:
|
||||
| **`strategies`** | Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](./custom-strategies). |
|
||||
| **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. |
|
||||
| **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). |
|
||||
| **`useSessions`** | True by default. Set to `false` to use stateless JWTs for authentication instead of sessions. |
|
||||
| **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](./email#email-verification). |
|
||||
|
||||
### Login With Username
|
||||
@@ -179,7 +178,7 @@ All auth-related operations are available via Payload's REST, Local, and GraphQL
|
||||
|
||||
## Strategies
|
||||
|
||||
Out of the box Payload ships with three powerful Authentication strategies:
|
||||
Out of the box Payload ships with a three powerful Authentication strategies:
|
||||
|
||||
- [HTTP-Only Cookies](./cookies)
|
||||
- [JSON Web Tokens (JWT)](./jwt)
|
||||
@@ -202,43 +201,3 @@ API Keys can be enabled on auth collections. These are particularly useful when
|
||||
### Custom Strategies
|
||||
|
||||
There are cases where these may not be enough for your application. Payload is extendable by design so you can wire up your own strategy when you need to. [More details](./custom-strategies).
|
||||
|
||||
### Access Control
|
||||
|
||||
Default auth fields including `email`, `username`, and `password` can be overridden by defining a custom field with the same name in your collection config. This allows you to customize the field — including access control — while preserving the underlying auth functionality. For example, you might want to restrict the `email` field from being updated once it is created, or only allow it to be read by certain user roles. You can achieve this by redefining the field and setting access rules accordingly.
|
||||
|
||||
Here's an example of how to restrict access to default auth fields:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Auth: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'email', // or 'username'
|
||||
type: 'text',
|
||||
access: {
|
||||
create: () => true,
|
||||
read: () => false,
|
||||
update: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'password', // this will be applied to all password-related fields including new password, confirm password.
|
||||
type: 'text',
|
||||
hidden: true, // needed only for the password field to prevent duplication in the Admin panel
|
||||
access: {
|
||||
update: () => false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**Note:**
|
||||
|
||||
- Access functions will apply across the application — I.e. if `read` access is disabled on `email`, it will not appear in the Admin panel UI or API.
|
||||
- Restricting `read` on the `email` or `username` disables the **Unlock** action in the Admin panel as this function requires access to a user-identifying field.
|
||||
- When overriding the `password` field, you may need to include `hidden: true` to prevent duplicate fields being displayed in the Admin panel.
|
||||
|
||||
@@ -85,7 +85,6 @@ 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._
|
||||
|
||||
@@ -195,15 +194,13 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| 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). |
|
||||
| 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). |
|
||||
|
||||
<Banner type="success">
|
||||
**Note:** For details on how to build Custom Components, see [Building Custom
|
||||
|
||||
@@ -51,7 +51,7 @@ For more granular control, pass a configuration object instead. Payload exposes
|
||||
| Property | Description |
|
||||
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Component` \* | Pass in the component path that should be rendered when a user navigates to this route. |
|
||||
| `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. Must begin with a forward slash (`/`). |
|
||||
| `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
|
||||
| `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||
| `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||
| `sensitive` | When true, will match if the path is case sensitive. |
|
||||
|
||||
@@ -30,6 +30,7 @@ export const MyCollectionOrGlobalConfig: CollectionConfig = {
|
||||
// - api
|
||||
// - versions
|
||||
// - version
|
||||
// - livePreview
|
||||
// - [key: string]
|
||||
// See below for more details
|
||||
},
|
||||
@@ -87,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.
|
||||
|
||||
@@ -106,8 +107,8 @@ export const MyCollection: CollectionConfig = {
|
||||
components: {
|
||||
views: {
|
||||
edit: {
|
||||
myCustomView: {
|
||||
Component: '/path/to/MyCustomView',
|
||||
myCustomTab: {
|
||||
Component: '/path/to/MyCustomTab',
|
||||
path: '/my-custom-tab',
|
||||
// highlight-start
|
||||
tab: {
|
||||
@@ -115,14 +116,13 @@ export const MyCollection: CollectionConfig = {
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
anotherCustomView: {
|
||||
anotherCustomTab: {
|
||||
Component: '/path/to/AnotherCustomView',
|
||||
path: '/another-custom-view',
|
||||
// highlight-start
|
||||
tab: {
|
||||
label: 'Another Custom View',
|
||||
href: '/another-custom-view',
|
||||
order: '100',
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
@@ -143,7 +143,6 @@ 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,16 +101,15 @@ export const MyCollection: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| 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). |
|
||||
| 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). |
|
||||
|
||||
#### Globals
|
||||
|
||||
@@ -135,15 +134,14 @@ export const MyGlobal: GlobalConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| 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). |
|
||||
| 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). |
|
||||
|
||||
### SaveButton
|
||||
|
||||
@@ -262,92 +260,6 @@ 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,7 +94,6 @@ 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() {
|
||||
|
||||
@@ -41,7 +41,6 @@ export default buildConfig({
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
|
||||
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
|
||||
| `disableFallbackSort` | Set to `true` to disable the adapter adding a fallback sort when sorting by non-unique fields, this can affect performance in some cases but it ensures a consistent order of results. |
|
||||
|
||||
## Access to Mongoose models
|
||||
|
||||
|
||||
@@ -80,8 +80,6 @@ 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,7 +50,6 @@ 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
|
||||
|
||||
|
||||
@@ -305,23 +305,6 @@ 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.
|
||||
@@ -351,6 +334,7 @@ import {
|
||||
code,
|
||||
date,
|
||||
email,
|
||||
group,
|
||||
json,
|
||||
number,
|
||||
point,
|
||||
|
||||
@@ -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) |
|
||||
|
||||
|
||||
@@ -23,12 +23,6 @@ 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
|
||||
@@ -88,16 +82,7 @@ To enable folders on a collection, you need to set the `admin.folders` property
|
||||
```ts
|
||||
// Type definition
|
||||
|
||||
type CollectionFoldersConfiguration =
|
||||
| boolean
|
||||
| {
|
||||
/**
|
||||
* If true, the collection will be included in the browse by folder view
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
browseByFolder?: boolean
|
||||
}
|
||||
type CollectionFoldersConfiguration = boolean
|
||||
```
|
||||
|
||||
```ts
|
||||
|
||||
@@ -16,15 +16,14 @@ The labels you provide for your Collections and Globals are used to name the Gra
|
||||
|
||||
At the top of your Payload Config you can define all the options to manage GraphQL.
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
|
||||
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground in production environments, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
|
||||
| `disableIntrospectionInProduction` | A boolean that if false will enable the GraphQL introspection in production environments, defaults to true. |
|
||||
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
|
||||
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
|
||||
| Option | Description |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
|
||||
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
|
||||
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
|
||||
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
|
||||
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
|
||||
|
||||
## Collections
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ The following arguments are provided to the `beforeValidate` hook:
|
||||
|
||||
### beforeChange
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```ts
|
||||
import type { CollectionBeforeChangeHook } from 'payload'
|
||||
@@ -160,7 +160,6 @@ 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,18 +128,20 @@ 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 module` 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` syntax.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```ts
|
||||
import { RequestContext as OriginalRequestContext } from 'payload'
|
||||
|
||||
declare module 'payload' {
|
||||
// Augment the RequestContext interface to include your custom properties
|
||||
export interface RequestContext {
|
||||
// Create a new interface that merges your additional fields with the original one
|
||||
export interface RequestContext extends OriginalRequestContext {
|
||||
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 module 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 type augmentation can mess up your types if you do it wrong.
|
||||
|
||||
@@ -128,7 +128,6 @@ 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,26 +68,3 @@ 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,9 +30,7 @@ As mentioned above, you can queue jobs, but the jobs won't run unless a worker p
|
||||
|
||||
### 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**:
|
||||
You can use the `jobs.autoRun` property to configure cron jobs:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
@@ -82,12 +80,6 @@ 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.
|
||||
@@ -145,15 +137,11 @@ 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' } },
|
||||
@@ -170,22 +158,10 @@ 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. By default, this script will run jobs from the `default` queue, with a limit of 10 jobs per invocation:
|
||||
Finally, you can process jobs via the bin script that comes with Payload out of the box.
|
||||
|
||||
```sh
|
||||
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
|
||||
npx payload jobs:run --queue default --limit 10
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
@@ -393,7 +393,7 @@ export default function LoginForm() {
|
||||
|
||||
### Logout
|
||||
|
||||
Logs out the current user by clearing the authentication cookie and current sessions.
|
||||
Logs out the current user by clearing the authentication cookie.
|
||||
|
||||
#### Importing the `logout` function
|
||||
|
||||
@@ -401,7 +401,7 @@ Logs out the current user by clearing the authentication cookie and current sess
|
||||
import { logout } from '@payloadcms/next/auth'
|
||||
```
|
||||
|
||||
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below. To ensure all sessions are cleared, set `allSessions: true` in the options, if you wish to logout but keep current sessions active, you can set this to `false` or leave it `undefined`.
|
||||
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below.
|
||||
|
||||
```ts
|
||||
'use server'
|
||||
@@ -411,7 +411,7 @@ import config from '@payload-config'
|
||||
|
||||
export async function logoutAction() {
|
||||
try {
|
||||
return await logout({ allSessions: true, config })
|
||||
return await logout({ config })
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
@@ -434,7 +434,7 @@ export default function LogoutButton() {
|
||||
|
||||
### Refresh
|
||||
|
||||
Refreshes the authentication token and current session for the logged-in user.
|
||||
Refreshes the authentication token for the logged-in user.
|
||||
|
||||
#### Importing the `refresh` function
|
||||
|
||||
@@ -453,6 +453,7 @@ import config from '@payload-config'
|
||||
export async function refreshAction() {
|
||||
try {
|
||||
return await refresh({
|
||||
collection: 'users', // pass your collection slug
|
||||
config,
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -470,31 +470,6 @@ 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.
|
||||
|
||||
@@ -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'
|
||||
import { createBreadcrumbsField } from '@payloadcms/plugin-nested-docs'
|
||||
import { createParentField } from '@payloadcms/plugin-nested-docs/fields'
|
||||
import { createBreadcrumbsField } from '@payloadcms/plugin-nested-docs/fields'
|
||||
|
||||
const examplePageConfig: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
|
||||
@@ -74,32 +74,16 @@ import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
const config = buildConfig({
|
||||
collections: [Pages, Media],
|
||||
plugins: [sentryPlugin({ Sentry })],
|
||||
plugins: [
|
||||
sentryPlugin({
|
||||
Sentry,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
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,7 +115,6 @@ 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`,
|
||||
|
||||
@@ -6,6 +6,8 @@ desc: You don't want to have a DB connection while building your Docker containe
|
||||
keywords: deployment, production, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
# Building without a DB connection
|
||||
|
||||
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.
|
||||
|
||||
@@ -207,4 +207,4 @@ const config = buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
The `filterConstraints` function receives the same arguments as [`filterOptions`](../fields/select#filteroptions) in the [Select field](../fields/select).
|
||||
The `filterConstraints` function receives the same arguments as [`filterOptions`](../fields/select#filterOptions) in the [Select field](../fields/select).
|
||||
|
||||
@@ -336,17 +336,3 @@ 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,7 +81,6 @@ The default `elements` available in Payload are:
|
||||
- `link`
|
||||
- `ol`
|
||||
- `ul`
|
||||
- `li`
|
||||
- `textAlign`
|
||||
- `indent`
|
||||
- [`relationship`](#relationship-element)
|
||||
|
||||
@@ -95,7 +95,6 @@ _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). |
|
||||
@@ -109,7 +108,6 @@ _An asterisk denotes that an option is required._
|
||||
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
|
||||
| **`pasteURL`** | Controls whether files can be uploaded from remote URLs by pasting them into the Upload field. **Enabled by default.** Accepts `false` to disable or an object with an `allowList` of valid remote URLs. [More](#uploading-files-from-remote-urls) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`skipSafeFetch`** | Set to an `allowList` to skip the safe fetch check when fetching external files. Set to `true` to skip the safe fetch for all documents in this collection. Defaults to `false`. |
|
||||
| **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug |
|
||||
| **`trimOptions`** | An object passed to the the Sharp image library to trim the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize#trim) |
|
||||
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
|
||||
@@ -436,24 +434,6 @@ export const Media: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
You can also adjust server-side fetching at the upload level as well, this does not effect the `CORS` policy like the `pasteURL` option does, but it allows you to skip the safe fetch check for specific URLs.
|
||||
|
||||
```
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
upload: {
|
||||
skipSafeFetch: [
|
||||
{
|
||||
hostname: 'example.com',
|
||||
pathname: '/images/*',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
##### Accepted Values for `pasteURL`
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -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. 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.
|
||||
- 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.
|
||||
|
||||
```ts
|
||||
import { s3Storage } from '@payloadcms/storage-s3'
|
||||
@@ -100,14 +100,6 @@ 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,37 +51,12 @@ 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:
|
||||
|
||||
```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) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
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`.
|
||||
|
||||
**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,12 +1,14 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const GET = async (request: Request) => {
|
||||
export const GET = async () => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
message: 'This is an example of a custom route.',
|
||||
const data = await payload.find({
|
||||
collection: 'users',
|
||||
})
|
||||
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "astro-website",
|
||||
"name": "website",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,12 +14,12 @@ export const Header = () => {
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-dark.svg"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
|
||||
@@ -27,12 +27,12 @@ export const Header = async () => {
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-dark.svg"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { cn } from '@/utilities/ui'
|
||||
import useClickableCard from '@/utilities/useClickableCard'
|
||||
import Link from 'next/link'
|
||||
import { useLocale } from 'next-intl'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Post } from '@/payload-types'
|
||||
@@ -17,7 +16,6 @@ export const Card: React.FC<{
|
||||
showCategories?: boolean
|
||||
title?: string
|
||||
}> = (props) => {
|
||||
const locale = useLocale()
|
||||
const { card, link } = useClickableCard({})
|
||||
const { className, doc, relationTo, showCategories, title: titleFromProps } = props
|
||||
|
||||
@@ -27,7 +25,7 @@ export const Card: React.FC<{
|
||||
const hasCategories = categories && Array.isArray(categories) && categories.length > 0
|
||||
const titleToUse = titleFromProps || title
|
||||
const sanitizedDescription = description?.replace(/\s/g, ' ') // replace non-breaking space with white space
|
||||
const href = `/${locale}/${relationTo}/${slug}`
|
||||
const href = `/${relationTo}/${slug}`
|
||||
|
||||
return (
|
||||
<article
|
||||
|
||||
@@ -6,7 +6,7 @@ export const Logo = () => {
|
||||
<img
|
||||
alt="Payload Logo"
|
||||
className="max-w-[9.375rem] invert dark:invert-0"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function Footer({ locale }: { locale: TypedLocale }) {
|
||||
<img
|
||||
alt="Payload Logo"
|
||||
className="max-w-[6rem] invert-0"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
|
||||
@@ -53,7 +53,7 @@ export default buildConfig({
|
||||
admin: {
|
||||
components: {
|
||||
// The `BeforeLogin` component renders a message that you see while logging into your admin panel.
|
||||
// Feel free to delete this at any time. Simply remove the line below.
|
||||
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
|
||||
beforeLogin: ['@/components/BeforeLogin'],
|
||||
afterDashboard: ['@/components/AfterDashboard'],
|
||||
},
|
||||
|
||||
@@ -14,12 +14,9 @@ export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
|
||||
const requestedTenant = req?.data?.tenant
|
||||
|
||||
if (requestedTenant && adminTenantAccessIDs.includes(requestedTenant)) {
|
||||
return true
|
||||
return {
|
||||
tenant: {
|
||||
in: getUserTenantIDs(req.user, 'tenant-admin'),
|
||||
},
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
import type { Tenant, User } from '../../../payload-types'
|
||||
import type { User } from '../../../payload-types'
|
||||
|
||||
import { isSuperAdmin } from '../../../access/isSuperAdmin'
|
||||
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
|
||||
@@ -14,20 +14,9 @@ export const createAccess: Access<User> = ({ req }) => {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isSuperAdmin(req.user) && req.data?.roles?.includes('super-admin')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
|
||||
|
||||
const requestedTenants: Tenant['id'][] =
|
||||
req.data?.tenants?.map((t: { tenant: Tenant['id'] }) => t.tenant) ?? []
|
||||
|
||||
const hasAccessToAllRequestedTenants = requestedTenants.every((tenantID) =>
|
||||
adminTenantAccessIDs.includes(tenantID),
|
||||
)
|
||||
|
||||
if (hasAccessToAllRequestedTenants) {
|
||||
if (adminTenantAccessIDs.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const GET = async (request: Request) => {
|
||||
export const GET = async () => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
message: 'This is an example of a custom route.',
|
||||
const data = await payload.find({
|
||||
collection: 'users',
|
||||
})
|
||||
|
||||
return Response.json(data)
|
||||
}
|
||||
|
||||
42
package.json
42
package.json
@@ -1,21 +1,17 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.45.0",
|
||||
"version": "3.40.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"test/*"
|
||||
],
|
||||
"scripts": {
|
||||
"bf": "pnpm run build:force",
|
||||
"build": "pnpm run build:core",
|
||||
"build:admin-bar": "turbo build --filter \"@payloadcms/admin-bar\"",
|
||||
"build:all": "turbo build --filter \"!blank\" --filter \"!website\"",
|
||||
"build:all": "turbo build",
|
||||
"build:app": "next build",
|
||||
"build:app:analyze": "cross-env ANALYZE=true next build",
|
||||
"build:clean": "pnpm clean:build",
|
||||
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\" --filter \"!@payloadcms/storage-*\" --filter \"!blank\" --filter \"!website\"",
|
||||
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\" --filter \"!@payloadcms/storage-*\"",
|
||||
"build:core:force": "pnpm clean:build && pnpm build:core --no-cache --force",
|
||||
"build:create-payload-app": "turbo build --filter create-payload-app",
|
||||
"build:db-mongodb": "turbo build --filter \"@payloadcms/db-mongodb\"",
|
||||
@@ -64,12 +60,11 @@
|
||||
"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 --max-old-space-size=16384\" tsx ./test/dev.ts",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation 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",
|
||||
@@ -79,9 +74,9 @@
|
||||
"docker:start": "docker compose -f test/docker-compose.yml up -d",
|
||||
"docker:stop": "docker compose -f test/docker-compose.yml down",
|
||||
"force:build": "pnpm run build:core:force",
|
||||
"lint": "turbo run lint --log-order=grouped --continue --filter \"!blank\" --filter \"!website\"",
|
||||
"lint": "turbo run lint --log-order=grouped --continue",
|
||||
"lint-staged": "lint-staged",
|
||||
"lint:fix": "turbo run lint:fix --log-order=grouped --continue --filter \"!blank\" --filter \"!website\"",
|
||||
"lint:fix": "turbo run lint:fix --log-order=grouped --continue",
|
||||
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
"prepare": "husky",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
@@ -102,16 +97,16 @@
|
||||
"test:e2e": "pnpm runts ./test/runE2E.ts",
|
||||
"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:e2e:prod:ci:noturbo": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod --no-turbo",
|
||||
"test:e2e:prod:ci:turbo": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod --turbo",
|
||||
"test:e2e:turbo": "pnpm runts ./test/runE2E.ts --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 --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"
|
||||
"translateNewKeys": "pnpm --filter translations run translateNewKeys"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/package.json": "sort-package-json",
|
||||
@@ -122,7 +117,7 @@
|
||||
],
|
||||
"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.base.json": "node scripts/reset-tsconfig.js"
|
||||
"tsconfig.json": "node scripts/reset-tsconfig.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
@@ -141,7 +136,7 @@
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/shelljs": "0.8.15",
|
||||
@@ -151,9 +146,7 @@
|
||||
"create-payload-app": "workspace:*",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"execa": "5.1.1",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
@@ -162,11 +155,10 @@
|
||||
"jest": "29.7.0",
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "10.1.4",
|
||||
"mongodb-memory-server": "^10",
|
||||
"next": "15.3.2",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"pg": "8.16.3",
|
||||
"playwright": "1.50.0",
|
||||
"playwright-core": "1.50.0",
|
||||
"prettier": "3.5.3",
|
||||
@@ -181,7 +173,7 @@
|
||||
"tempy": "1.0.1",
|
||||
"tstyche": "^3.1.1",
|
||||
"tsx": "4.19.2",
|
||||
"turbo": "^2.5.4",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
@@ -200,5 +192,9 @@
|
||||
"react-dom": "$react-dom",
|
||||
"typescript": "$typescript"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaces:": [
|
||||
"packages/*",
|
||||
"test/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.45.0",
|
||||
"version": "3.40.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.45.0",
|
||||
"version": "3.40.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -77,7 +77,7 @@
|
||||
"@types/esprima": "^4.0.6",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "22.15.30"
|
||||
"@types/node": "22.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ import path from 'path'
|
||||
|
||||
import type { CliArgs, DbType, ProjectExample, ProjectTemplate } from '../types.js'
|
||||
|
||||
import { createProject, updatePackageJSONDependencies } from './create-project.js'
|
||||
import { createProject } from './create-project.js'
|
||||
import { dbReplacements } from './replacements.js'
|
||||
import { getValidTemplates } from './templates.js'
|
||||
|
||||
@@ -179,37 +179,5 @@ describe('createProject', () => {
|
||||
expect(content).toContain(dbReplacement.configReplacement().join('\n'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('updates package.json', () => {
|
||||
it('updates package name and bumps workspace versions', async () => {
|
||||
const latestVersion = '3.0.0'
|
||||
const initialJSON = {
|
||||
name: 'test-project',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'@payloadcms/db-mongodb': 'workspace:*',
|
||||
payload: 'workspace:*',
|
||||
'@payloadcms/ui': 'workspace:*',
|
||||
},
|
||||
}
|
||||
|
||||
const correctlyModifiedJSON = {
|
||||
name: 'test-project',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'@payloadcms/db-mongodb': `${latestVersion}`,
|
||||
payload: `${latestVersion}`,
|
||||
'@payloadcms/ui': `${latestVersion}`,
|
||||
},
|
||||
}
|
||||
|
||||
updatePackageJSONDependencies({
|
||||
latestVersion,
|
||||
packageJson: initialJSON,
|
||||
})
|
||||
|
||||
expect(initialJSON).toEqual(correctlyModifiedJSON)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -129,11 +129,7 @@ export async function createProject(
|
||||
const spinner = p.spinner()
|
||||
spinner.start('Checking latest Payload version...')
|
||||
|
||||
const payloadVersion = await getLatestPackageVersion({ packageName: 'payload' })
|
||||
|
||||
spinner.stop(`Found latest version of Payload ${payloadVersion}`)
|
||||
|
||||
await updatePackageJSON({ latestVersion: payloadVersion, projectDir, projectName })
|
||||
await updatePackageJSON({ projectDir, projectName })
|
||||
|
||||
if ('template' in args) {
|
||||
if (args.template.type === 'plugin') {
|
||||
@@ -181,105 +177,17 @@ export async function createProject(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the package.json file into an object and then does the following:
|
||||
* - Sets the `name` property to the provided `projectName`.
|
||||
* - Bumps the payload packages from workspace:* to the latest version.
|
||||
* - Writes the updated object back to the package.json file.
|
||||
*/
|
||||
export async function updatePackageJSON(args: {
|
||||
/**
|
||||
* The latest version of Payload to use in the package.json.
|
||||
*/
|
||||
latestVersion: string
|
||||
projectDir: string
|
||||
/**
|
||||
* The name of the project to set in package.json.
|
||||
*/
|
||||
projectName: string
|
||||
}): Promise<void> {
|
||||
const { latestVersion, projectDir, projectName } = args
|
||||
const { projectDir, projectName } = args
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
try {
|
||||
const packageObj = await fse.readJson(packageJsonPath)
|
||||
packageObj.name = projectName
|
||||
|
||||
updatePackageJSONDependencies({
|
||||
latestVersion,
|
||||
packageJson: packageObj,
|
||||
})
|
||||
|
||||
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
|
||||
} catch (err: unknown) {
|
||||
warning(`Unable to update name in package.json. ${err instanceof Error ? err.message : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively updates a JSON object to replace all instances of `workspace:` with the latest version pinned.
|
||||
*
|
||||
* Does not return and instead modifies the `packageJson` object in place.
|
||||
*/
|
||||
export function updatePackageJSONDependencies(args: {
|
||||
latestVersion: string
|
||||
packageJson: Record<string, unknown>
|
||||
}): void {
|
||||
const { latestVersion, packageJson } = args
|
||||
|
||||
const updatedDependencies = Object.entries(packageJson.dependencies || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (typeof value === 'string' && value.startsWith('workspace:')) {
|
||||
acc[key] = `${latestVersion}`
|
||||
} else if (key === 'payload' || key.startsWith('@payloadcms')) {
|
||||
acc[key] = `${latestVersion}`
|
||||
} else {
|
||||
acc[key] = value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
)
|
||||
packageJson.dependencies = updatedDependencies
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest version of a package from the NPM registry.
|
||||
*
|
||||
* Used in determining the latest version of Payload to use in the generated templates.
|
||||
*/
|
||||
async function getLatestPackageVersion({
|
||||
packageName = 'payload',
|
||||
}: {
|
||||
/**
|
||||
* Package name to fetch the latest version for based on the NPM registry URL
|
||||
*
|
||||
* Eg. for `'payload'`, it will fetch the version from `https://registry.npmjs.org/payload`
|
||||
*
|
||||
* @default 'payload'
|
||||
*/
|
||||
packageName?: string
|
||||
}): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`)
|
||||
const data = await response.json()
|
||||
|
||||
// Monster chaining for type safety just checking for data.latest
|
||||
const latestVersion =
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'latest' in data &&
|
||||
data.latest &&
|
||||
typeof data.latest === 'string'
|
||||
? data.latest
|
||||
: null
|
||||
|
||||
if (!latestVersion) {
|
||||
throw new Error(`No latest version found for package: ${packageName}`)
|
||||
}
|
||||
|
||||
return latestVersion
|
||||
} catch (error) {
|
||||
console.error('Error fetching Payload version:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function downloadTemplate({
|
||||
}) {
|
||||
const branchOrTag = template.url.split('#')?.[1] || 'latest'
|
||||
const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${branchOrTag}`
|
||||
const filter = `payload-${branchOrTag.replace(/^v/, '').replaceAll('/', '-')}/templates/${template.name}/`
|
||||
const filter = `payload-${branchOrTag.replace(/^v/, '')}/templates/${template.name}/`
|
||||
|
||||
if (debug) {
|
||||
debugLog(`Using template url: ${template.url}`)
|
||||
|
||||
@@ -7,7 +7,7 @@ export async function getExamples({ branch }: { branch: string }): Promise<Proje
|
||||
|
||||
const response = await fetch(url)
|
||||
|
||||
const examplesResponseList = (await response.json()) as { name: string; path: string }[]
|
||||
const examplesResponseList: { name: string; path: string }[] = await response.json()
|
||||
|
||||
const examples: ProjectExample[] = examplesResponseList.map((example) => ({
|
||||
name: example.name,
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function getLatestPackageVersion({
|
||||
}) {
|
||||
try {
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}`)
|
||||
const data = (await response.json()) as { 'dist-tags': { latest: string } }
|
||||
const data = await response.json()
|
||||
const latestVersion = data['dist-tags'].latest
|
||||
|
||||
if (debug) {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"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.45.0",
|
||||
"version": "3.40.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -47,7 +47,7 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongoose": "8.15.1",
|
||||
"mongoose": "8.9.5",
|
||||
"mongoose-paginate-v2": "1.8.5",
|
||||
"prompts": "2.4.2",
|
||||
"uuid": "10.0.0"
|
||||
@@ -57,8 +57,8 @@
|
||||
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
|
||||
"@types/prompts": "^2.4.5",
|
||||
"@types/uuid": "10.0.0",
|
||||
"mongodb": "6.16.0",
|
||||
"mongodb-memory-server": "10.1.4",
|
||||
"mongodb": "6.12.0",
|
||||
"mongodb-memory-server": "^10",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -40,9 +40,6 @@ 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))
|
||||
}
|
||||
|
||||
@@ -53,7 +50,7 @@ export const connect: Connect = async function connect(
|
||||
this.beginTransaction = defaultBeginTransaction()
|
||||
}
|
||||
|
||||
if (!hotReload) {
|
||||
if (!this.mongoMemoryServer && !hotReload) {
|
||||
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
|
||||
this.payload.logger.info('---- DROPPING DATABASE ----')
|
||||
await mongoose.connection.dropDatabase()
|
||||
|
||||
@@ -1,49 +1,29 @@
|
||||
import type { DeleteVersions, FlattenedField } from 'payload'
|
||||
|
||||
import { APIError, buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import { buildVersionCollectionFields, type DeleteVersions } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
import type { CollectionModel } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection, getGlobal } from './utilities/getEntity.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteVersions: DeleteVersions = async function deleteVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection: collectionSlug, globalSlug, locale, req, where },
|
||||
{ collection: collectionSlug, locale, req, where },
|
||||
) {
|
||||
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 { collectionConfig, Model } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
await VersionsModel.deleteMany(query, { session })
|
||||
await Model.deleteMany(query, { session })
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ import mongoose from 'mongoose'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
export const destroy: Destroy = async function destroy(this: MongooseAdapter) {
|
||||
await mongoose.disconnect()
|
||||
if (this.mongoMemoryServer) {
|
||||
await this.mongoMemoryServer.stop()
|
||||
} else {
|
||||
await mongoose.disconnect()
|
||||
}
|
||||
|
||||
Object.keys(mongoose.models).map((model) => mongoose.deleteModel(model))
|
||||
}
|
||||
|
||||
@@ -118,13 +118,6 @@ export interface Args {
|
||||
*/
|
||||
useFacet?: boolean
|
||||
} & ConnectOptions
|
||||
/**
|
||||
* We add a secondary sort based on `createdAt` to ensure that results are always returned in the same order when sorting by a non-unique field.
|
||||
* This is because MongoDB does not guarantee the order of results, however in very large datasets this could affect performance.
|
||||
*
|
||||
* Set to `true` to disable this behaviour.
|
||||
*/
|
||||
disableFallbackSort?: boolean
|
||||
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
|
||||
disableIndexHints?: boolean
|
||||
/**
|
||||
@@ -138,7 +131,6 @@ export interface Args {
|
||||
*/
|
||||
mongoMemoryServer?: MongoMemoryReplSet
|
||||
prodMigrations?: Migration[]
|
||||
|
||||
transactionOptions?: false | TransactionOptions
|
||||
|
||||
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
|
||||
@@ -206,7 +198,6 @@ export function mongooseAdapter({
|
||||
autoPluralization = true,
|
||||
collectionsSchemaOptions = {},
|
||||
connectOptions,
|
||||
disableFallbackSort = false,
|
||||
disableIndexHints = false,
|
||||
ensureIndexes = false,
|
||||
migrationDir: migrationDirArg,
|
||||
@@ -260,7 +251,6 @@ export function mongooseAdapter({
|
||||
deleteOne,
|
||||
deleteVersions,
|
||||
destroy,
|
||||
disableFallbackSort,
|
||||
find,
|
||||
findGlobal,
|
||||
findGlobalVersions,
|
||||
|
||||
@@ -245,13 +245,10 @@ export async function buildSearchParam({
|
||||
value: { $in },
|
||||
}
|
||||
} else {
|
||||
const nextSubPath = pathsToQuery[i + 1]?.path
|
||||
if (nextSubPath) {
|
||||
relationshipQuery = {
|
||||
value: {
|
||||
[nextSubPath]: { $in },
|
||||
},
|
||||
}
|
||||
relationshipQuery = {
|
||||
value: {
|
||||
_id: { $in },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import type { Config, SanitizedConfig } from 'payload'
|
||||
|
||||
import { sanitizeConfig } from 'payload'
|
||||
|
||||
import { buildSortParam } from './buildSortParam.js'
|
||||
import { MongooseAdapter } from '../index.js'
|
||||
|
||||
let config: SanitizedConfig
|
||||
|
||||
describe('builds sort params', () => {
|
||||
beforeAll(async () => {
|
||||
config = await sanitizeConfig({
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
locales: ['en', 'es'],
|
||||
},
|
||||
} as Config)
|
||||
})
|
||||
it('adds a fallback on non-unique field', () => {
|
||||
const result = buildSortParam({
|
||||
config,
|
||||
parentIsLocalized: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
locale: 'en',
|
||||
sort: 'order',
|
||||
timestamps: true,
|
||||
adapter: {
|
||||
disableFallbackSort: false,
|
||||
} as MongooseAdapter,
|
||||
})
|
||||
|
||||
expect(result).toStrictEqual({ order: 'asc', createdAt: 'desc' })
|
||||
})
|
||||
|
||||
it('adds a fallback when sort isnt provided', () => {
|
||||
const result = buildSortParam({
|
||||
config,
|
||||
parentIsLocalized: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
locale: 'en',
|
||||
sort: undefined,
|
||||
timestamps: true,
|
||||
adapter: {
|
||||
disableFallbackSort: false,
|
||||
} as MongooseAdapter,
|
||||
})
|
||||
|
||||
expect(result).toStrictEqual({ createdAt: 'desc' })
|
||||
})
|
||||
|
||||
it('does not add a fallback on non-unique field when disableFallbackSort is true', () => {
|
||||
const result = buildSortParam({
|
||||
config,
|
||||
parentIsLocalized: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
locale: 'en',
|
||||
sort: 'order',
|
||||
timestamps: true,
|
||||
adapter: {
|
||||
disableFallbackSort: true,
|
||||
} as MongooseAdapter,
|
||||
})
|
||||
|
||||
expect(result).toStrictEqual({ order: 'asc' })
|
||||
})
|
||||
|
||||
// This test should be true even when disableFallbackSort is false
|
||||
it('does not add a fallback on unique field', () => {
|
||||
const result = buildSortParam({
|
||||
config,
|
||||
parentIsLocalized: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'order',
|
||||
type: 'number',
|
||||
unique: true, // Marking this field as unique
|
||||
},
|
||||
],
|
||||
locale: 'en',
|
||||
sort: 'order',
|
||||
timestamps: true,
|
||||
adapter: {
|
||||
disableFallbackSort: false,
|
||||
} as MongooseAdapter,
|
||||
})
|
||||
|
||||
expect(result).toStrictEqual({ order: 'asc' })
|
||||
})
|
||||
})
|
||||
@@ -19,7 +19,7 @@ type Args = {
|
||||
fields: FlattenedField[]
|
||||
locale?: string
|
||||
parentIsLocalized?: boolean
|
||||
sort?: Sort
|
||||
sort: Sort
|
||||
sortAggregation?: PipelineStage[]
|
||||
timestamps: boolean
|
||||
versions?: boolean
|
||||
@@ -77,9 +77,6 @@ const relationshipSort = ({
|
||||
) {
|
||||
const relationshipPath = segments.slice(0, i + 1).join('.')
|
||||
let sortFieldPath = segments.slice(i + 1, segments.length).join('.')
|
||||
if (sortFieldPath.endsWith('.id')) {
|
||||
sortFieldPath = sortFieldPath.split('.').slice(0, -1).join('.')
|
||||
}
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
throw new APIError('Not supported')
|
||||
}
|
||||
@@ -153,12 +150,6 @@ export const buildSortParam = ({
|
||||
sort = [sort]
|
||||
}
|
||||
|
||||
// We use this flag to determine if the sort is unique or not to decide whether to add a fallback sort.
|
||||
const isUniqueSort = sort.some((item) => {
|
||||
const field = getFieldByPath({ fields, path: item })
|
||||
return field?.field?.unique
|
||||
})
|
||||
|
||||
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
|
||||
// So we add a fallback sort to ensure that the results are always in the same order.
|
||||
let fallbackSort = '-id'
|
||||
@@ -167,12 +158,7 @@ export const buildSortParam = ({
|
||||
fallbackSort = '-createdAt'
|
||||
}
|
||||
|
||||
const includeFallbackSort =
|
||||
!adapter.disableFallbackSort &&
|
||||
!isUniqueSort &&
|
||||
!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))
|
||||
|
||||
if (includeFallbackSort) {
|
||||
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
|
||||
sort.push(fallbackSort)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { Job, UpdateJobs, Where } from 'payload'
|
||||
import type { BaseJob, 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: Job[] = []
|
||||
let result: BaseJob[] = []
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
|
||||
@@ -55,7 +55,6 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
try {
|
||||
if (returning === false) {
|
||||
await Model.updateOne(query, data, options)
|
||||
transform({ adapter: this, data, fields, operation: 'read' })
|
||||
return null
|
||||
} else {
|
||||
result = await Model.findOneAndUpdate(query, data, options)
|
||||
|
||||
@@ -277,9 +277,7 @@ const stripFields = ({
|
||||
continue
|
||||
}
|
||||
|
||||
let hasNull = false
|
||||
for (let i = 0; i < localeData.length; i++) {
|
||||
const data = localeData[i]
|
||||
for (const data of localeData) {
|
||||
let fields: FlattenedField[] | null = null
|
||||
|
||||
if (field.type === 'array') {
|
||||
@@ -288,17 +286,11 @@ const stripFields = ({
|
||||
let maybeBlock: FlattenedBlock | undefined = undefined
|
||||
|
||||
if (field.blockReferences) {
|
||||
const maybeBlockReference = field.blockReferences.find((each) => {
|
||||
const slug = typeof each === 'string' ? each : each.slug
|
||||
return slug === data.blockType
|
||||
})
|
||||
|
||||
if (maybeBlockReference) {
|
||||
if (typeof maybeBlockReference === 'object') {
|
||||
maybeBlock = maybeBlockReference
|
||||
} else {
|
||||
maybeBlock = config.blocks?.find((each) => each.slug === maybeBlockReference)
|
||||
}
|
||||
const maybeBlockReference = field.blockReferences.find(
|
||||
(each) => typeof each === 'object' && each.slug === data.blockType,
|
||||
)
|
||||
if (maybeBlockReference && typeof maybeBlockReference === 'object') {
|
||||
maybeBlock = maybeBlockReference
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,9 +300,6 @@ const stripFields = ({
|
||||
|
||||
if (maybeBlock) {
|
||||
fields = maybeBlock.flattenedFields
|
||||
} else {
|
||||
localeData[i] = null
|
||||
hasNull = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,10 +310,6 @@ const stripFields = ({
|
||||
stripFields({ config, data, fields, reservedKeys })
|
||||
}
|
||||
|
||||
if (hasNull) {
|
||||
fieldData[localeKey] = localeData.filter(Boolean)
|
||||
}
|
||||
|
||||
continue
|
||||
} else {
|
||||
stripFields({ config, data: localeData, fields: field.flattenedFields, reservedKeys })
|
||||
@@ -338,10 +323,7 @@ const stripFields = ({
|
||||
continue
|
||||
}
|
||||
|
||||
let hasNull = false
|
||||
|
||||
for (let i = 0; i < fieldData.length; i++) {
|
||||
const data = fieldData[i]
|
||||
for (const data of fieldData) {
|
||||
let fields: FlattenedField[] | null = null
|
||||
|
||||
if (field.type === 'array') {
|
||||
@@ -350,17 +332,12 @@ const stripFields = ({
|
||||
let maybeBlock: FlattenedBlock | undefined = undefined
|
||||
|
||||
if (field.blockReferences) {
|
||||
const maybeBlockReference = field.blockReferences.find((each) => {
|
||||
const slug = typeof each === 'string' ? each : each.slug
|
||||
return slug === data.blockType
|
||||
})
|
||||
const maybeBlockReference = field.blockReferences.find(
|
||||
(each) => typeof each === 'object' && each.slug === data.blockType,
|
||||
)
|
||||
|
||||
if (maybeBlockReference) {
|
||||
if (typeof maybeBlockReference === 'object') {
|
||||
maybeBlock = maybeBlockReference
|
||||
} else {
|
||||
maybeBlock = config.blocks?.find((each) => each.slug === maybeBlockReference)
|
||||
}
|
||||
if (maybeBlockReference && typeof maybeBlockReference === 'object') {
|
||||
maybeBlock = maybeBlockReference
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,9 +347,6 @@ const stripFields = ({
|
||||
|
||||
if (maybeBlock) {
|
||||
fields = maybeBlock.flattenedFields
|
||||
} else {
|
||||
fieldData[i] = null
|
||||
hasNull = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,10 +357,6 @@ const stripFields = ({
|
||||
stripFields({ config, data, fields, reservedKeys })
|
||||
}
|
||||
|
||||
if (hasNull) {
|
||||
data[field.name] = fieldData.filter(Boolean)
|
||||
}
|
||||
|
||||
continue
|
||||
} else {
|
||||
stripFields({ config, data: fieldData, fields: field.flattenedFields, reservedKeys })
|
||||
@@ -417,7 +387,7 @@ export const transform = ({
|
||||
|
||||
if (operation === 'read') {
|
||||
delete data['__v']
|
||||
data.id = data._id || data.id
|
||||
data.id = data._id
|
||||
delete data['_id']
|
||||
|
||||
if (data.id instanceof Types.ObjectId) {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"references": [{ "path": "../payload" }, { "path": "../translations" }],
|
||||
"compilerOptions": {
|
||||
// Do not include DOM and DOM.Iterable as this is a server-only package.
|
||||
"lib": ["ES2022"],
|
||||
}
|
||||
"references": [{ "path": "../payload" }, { "path": "../translations" }]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.45.0",
|
||||
"version": "3.40.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -78,9 +78,9 @@
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"pg": "8.16.3",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Connect, Migration } from 'payload'
|
||||
import type { Connect, Migration, Payload } 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,
|
||||
pool,
|
||||
payload,
|
||||
reconnect = false,
|
||||
}: {
|
||||
adapter: PostgresAdapter
|
||||
pool: PostgresAdapter['pool']
|
||||
payload: Payload
|
||||
reconnect?: boolean
|
||||
}) {
|
||||
let result
|
||||
|
||||
if (!reconnect) {
|
||||
result = await pool.connect()
|
||||
result = await adapter.pool.connect()
|
||||
} else {
|
||||
try {
|
||||
result = await pool.connect()
|
||||
result = await adapter.pool.connect()
|
||||
} catch (ignore) {
|
||||
setTimeout(() => {
|
||||
adapter.payload.logger.info('Reconnecting to postgres')
|
||||
void connectWithReconnect({ adapter, pool, reconnect: true })
|
||||
payload.logger.info('Reconnecting to postgres')
|
||||
void connectWithReconnect({ adapter, payload, reconnect: true })
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
@@ -36,7 +35,7 @@ const connectWithReconnect = async function ({
|
||||
result.prependListener('error', (err) => {
|
||||
try {
|
||||
if (err.code === 'ECONNRESET') {
|
||||
void connectWithReconnect({ adapter, pool, reconnect: true })
|
||||
void connectWithReconnect({ adapter, payload, reconnect: true })
|
||||
}
|
||||
} catch (ignore) {
|
||||
// swallow error
|
||||
@@ -55,29 +54,12 @@ export const connect: Connect = async function connect(
|
||||
try {
|
||||
if (!this.pool) {
|
||||
this.pool = new this.pg.Pool(this.poolOptions)
|
||||
await connectWithReconnect({ adapter: this, pool: this.pool })
|
||||
await connectWithReconnect({ adapter: this, payload: this.payload })
|
||||
}
|
||||
|
||||
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'}) ----`)
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
columnToCodeConverter,
|
||||
@@ -100,7 +99,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
allowIDOnCreate,
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
createMigration: buildCreateMigration({
|
||||
@@ -141,7 +139,6 @@ 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: {},
|
||||
@@ -208,7 +205,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
upsert: updateOne,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,13 @@ import type {
|
||||
GenericEnum,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
PostgresDB,
|
||||
PostgresSchemaHook,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { DrizzleConfig, ExtractTablesWithRelations } from 'drizzle-orm'
|
||||
import type { DrizzleConfig } from 'drizzle-orm'
|
||||
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
import type {
|
||||
PgDatabase,
|
||||
PgQueryResultHKT,
|
||||
PgSchema,
|
||||
PgTableFn,
|
||||
PgTransactionConfig,
|
||||
PgWithReplicas,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
|
||||
import type { Pool, PoolConfig } from 'pg'
|
||||
|
||||
type PgDependency = typeof import('pg')
|
||||
@@ -41,10 +35,6 @@ 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
|
||||
@@ -65,7 +55,6 @@ export type Args = {
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push?: boolean
|
||||
readReplicas?: string[]
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
@@ -85,16 +74,7 @@ type ResolveSchemaType<T> = 'schema' extends keyof T
|
||||
? T['schema']
|
||||
: GeneratedDatabaseSchema['schemaUntyped']
|
||||
|
||||
type Drizzle =
|
||||
| NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
|
||||
| PgWithReplicas<
|
||||
PgDatabase<
|
||||
PgQueryResultHKT,
|
||||
Record<string, unknown>,
|
||||
ExtractTablesWithRelations<Record<string, unknown>>
|
||||
>
|
||||
>
|
||||
|
||||
type Drizzle = NodePgDatabase<ResolveSchemaType<GeneratedDatabaseSchema>>
|
||||
export type PostgresAdapter = {
|
||||
drizzle: Drizzle
|
||||
pg: PgDependency
|
||||
|
||||
@@ -10,9 +10,5 @@
|
||||
{
|
||||
"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.45.0",
|
||||
"version": "3.40.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -76,8 +76,8 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
} from '@payloadcms/drizzle'
|
||||
import { like, notLike } from 'drizzle-orm'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
@@ -90,7 +89,6 @@ 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,
|
||||
@@ -190,7 +188,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
updateGlobalVersion,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
upsert: updateOne,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -50,10 +50,6 @@ 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,9 +10,5 @@
|
||||
{
|
||||
"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.45.0",
|
||||
"version": "3.40.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -78,9 +78,9 @@
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@vercel/postgres": "^0.9.0",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.31.4",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"pg": "8.16.3",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "10.0.0"
|
||||
|
||||
@@ -4,7 +4,6 @@ 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'
|
||||
@@ -47,19 +46,6 @@ 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'}) ----`)
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
columnToCodeConverter,
|
||||
@@ -96,7 +95,6 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
allowIDOnCreate,
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
defaultDrizzleSnapshot,
|
||||
@@ -176,7 +174,6 @@ 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,
|
||||
@@ -203,7 +200,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert,
|
||||
upsert: updateOne,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,6 @@ 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.
|
||||
@@ -68,7 +64,6 @@ export type Args = {
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
push?: boolean
|
||||
readReplicas?: string[]
|
||||
relationshipsSuffix?: string
|
||||
/**
|
||||
* The schema name to use for the database
|
||||
|
||||
@@ -10,9 +10,5 @@
|
||||
{
|
||||
"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.45.0",
|
||||
"version": "3.40.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -55,7 +55,7 @@
|
||||
"dependencies": {
|
||||
"console-table-printer": "2.12.1",
|
||||
"dequal": "2.0.3",
|
||||
"drizzle-orm": "0.44.2",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
@@ -64,6 +64,7 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/prompts": "^2.4.5",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import type { Count, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
import type { Count } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const count: Count = async function count(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, locale, req, where: whereArg },
|
||||
{ collection: collectionSlug, locale, req, where: whereArg = {} },
|
||||
) {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const db = await getTransaction(this, req)
|
||||
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import type { CountGlobalVersions, SanitizedGlobalConfig } from 'payload'
|
||||
import type { CountGlobalVersions } from 'payload'
|
||||
|
||||
import { buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: DrizzleAdapter,
|
||||
{ global, locale, req, where: whereArg },
|
||||
{ global: globalSlug, locale, req, where: whereArg = {} },
|
||||
) {
|
||||
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
|
||||
({ slug }) => slug === global,
|
||||
)
|
||||
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const { globalConfig, tableName } = getGlobal({ adapter: this, globalSlug, versions: true })
|
||||
|
||||
const db = await getTransaction(this, req)
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { CountVersions, SanitizedCollectionConfig } from 'payload'
|
||||
import type { CountVersions } from 'payload'
|
||||
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, locale, req, where: whereArg },
|
||||
{ collection: collectionSlug, locale, req, where: whereArg = {} },
|
||||
) {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const { collectionConfig, tableName } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const db = await getTransaction(this, req)
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Create } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
@@ -12,15 +11,13 @@ export const create: Create = async function create(
|
||||
{ collection: collectionSlug, data, req, returning, select },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const result = await upsertRow({
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
operation: 'create',
|
||||
req,
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import type { CreateGlobalArgs } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function createGlobal<T extends Record<string, unknown>>(
|
||||
this: DrizzleAdapter,
|
||||
{ slug, data, req, returning }: CreateGlobalArgs,
|
||||
{ slug: globalSlug, data, req, returning }: CreateGlobalArgs,
|
||||
): Promise<T> {
|
||||
const db = await getTransaction(this, req)
|
||||
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
|
||||
const { globalConfig, tableName } = getGlobal({ adapter: this, globalSlug })
|
||||
|
||||
data.createdAt = new Date().toISOString()
|
||||
|
||||
@@ -30,10 +27,10 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
return null as unknown as T
|
||||
}
|
||||
|
||||
result.globalType = slug
|
||||
result.globalType = globalSlug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { CreateGlobalVersionArgs, TypeWithID, TypeWithVersion } from 'paylo
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function createGlobalVersion<T extends TypeWithID>(
|
||||
@@ -25,9 +25,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
}: CreateGlobalVersionArgs,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
|
||||
|
||||
const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`)
|
||||
const { globalConfig, tableName } = getGlobal({ adapter: this, globalSlug, versions: true })
|
||||
|
||||
const result = await upsertRow<TypeWithVersion<T>>({
|
||||
adapter: this,
|
||||
@@ -41,7 +39,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
version: versionData,
|
||||
},
|
||||
db,
|
||||
fields: buildVersionGlobalFields(this.payload.config, global, true),
|
||||
fields: buildVersionGlobalFields(this.payload.config, globalConfig, true),
|
||||
ignoreResult: returning === false ? 'idOnly' : false,
|
||||
operation: 'create',
|
||||
req,
|
||||
@@ -50,7 +48,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
})
|
||||
|
||||
const table = this.tables[tableName]
|
||||
if (global.versions.drafts) {
|
||||
if (globalConfig.versions.drafts) {
|
||||
await this.execute({
|
||||
db,
|
||||
sql: sql`
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { CreateVersionArgs, TypeWithID, TypeWithVersion } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function createVersion<T extends TypeWithID>(
|
||||
@@ -26,12 +26,13 @@ export async function createVersion<T extends TypeWithID>(
|
||||
}: CreateVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const defaultTableName = toSnakeCase(collection.slug)
|
||||
const { collectionConfig, tableName } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const tableName = this.tableNameMap.get(`_${defaultTableName}${this.versionsSuffix}`)
|
||||
|
||||
const version = { ...versionData }
|
||||
const version: Partial<TypeWithID> = { ...versionData }
|
||||
if (version.id) {
|
||||
delete version.id
|
||||
}
|
||||
@@ -51,7 +52,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collection, true),
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
operation: 'create',
|
||||
req,
|
||||
select,
|
||||
@@ -60,7 +61,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
|
||||
const table = this.tables[tableName]
|
||||
|
||||
if (collection.versions.drafts) {
|
||||
if (collectionConfig.versions.drafts) {
|
||||
await this.execute({
|
||||
db,
|
||||
sql: sql`
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import type { DeleteMany } from 'payload'
|
||||
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { findMany } from './find/findMany.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteMany: DeleteMany = async function deleteMany(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, req, where },
|
||||
{ collection: collectionSlug, req, where },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const result = await findMany({
|
||||
adapter: this,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
joins: false,
|
||||
limit: 0,
|
||||
locale: req?.locale,
|
||||
locale: req?.locale ?? undefined,
|
||||
page: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
@@ -30,9 +28,9 @@ export const deleteMany: DeleteMany = async function deleteMany(
|
||||
where,
|
||||
})
|
||||
|
||||
const ids = []
|
||||
const ids: (number | string)[] = []
|
||||
|
||||
result.docs.forEach((data) => {
|
||||
result.docs.forEach((data: any) => {
|
||||
ids.push(data.id)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { DeleteOne } from 'payload'
|
||||
|
||||
import { eq } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
@@ -9,6 +8,7 @@ import { buildFindManyArgs } from './find/buildFindManyArgs.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { transform } from './transform/read/index.js'
|
||||
import { getCollection, getTableQuery } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
@@ -16,16 +16,14 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
{ collection: collectionSlug, req, returning, select, where: whereArg },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
|
||||
let docToDelete: Record<string, unknown>
|
||||
let docToDelete: Record<string, unknown> | undefined = undefined
|
||||
|
||||
const { joins, selectFields, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.flattenedFields,
|
||||
locale: req?.locale,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
locale: req?.locale ?? undefined,
|
||||
tableName,
|
||||
where: whereArg,
|
||||
})
|
||||
@@ -40,15 +38,17 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const queryTable = getTableQuery({ adapter: this, tableName })
|
||||
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
docToDelete = await db.query[tableName].findFirst({
|
||||
docToDelete = await queryTable.findFirst({
|
||||
where: eq(this.tables[tableName].id, selectDistinctResult[0].id),
|
||||
})
|
||||
} else {
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter: this,
|
||||
depth: 0,
|
||||
fields: collection.flattenedFields,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
joinQuery: false,
|
||||
select,
|
||||
tableName,
|
||||
@@ -56,7 +56,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
|
||||
findManyArgs.where = where
|
||||
|
||||
docToDelete = await db.query[tableName].findFirst(findManyArgs)
|
||||
docToDelete = await queryTable.findFirst(findManyArgs)
|
||||
}
|
||||
|
||||
if (!docToDelete) {
|
||||
@@ -70,7 +70,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
data: docToDelete,
|
||||
fields: collection.flattenedFields,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
joinQuery: false,
|
||||
tableName,
|
||||
})
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
import type { DeleteVersions, FlattenedField, SanitizedCollectionConfig } from 'payload'
|
||||
import type { DeleteVersions } from 'payload'
|
||||
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import { APIError, buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { findMany } from './find/findMany.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteVersions: DeleteVersions = async function deleteVersion(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, globalSlug, locale, req, where: where },
|
||||
{ collection: collectionSlug, locale, req, where: where },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const { collectionConfig, tableName } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
let tableName: string
|
||||
let fields: FlattenedField[]
|
||||
|
||||
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 fields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
|
||||
const { docs } = await findMany({
|
||||
adapter: this,
|
||||
@@ -46,9 +35,9 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
|
||||
where,
|
||||
})
|
||||
|
||||
const ids = []
|
||||
const ids: (number | string)[] = []
|
||||
|
||||
docs.forEach((doc) => {
|
||||
docs.forEach((doc: any) => {
|
||||
ids.push(doc.id)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { Find, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
import type { Find } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { findMany } from './find/findMany.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
|
||||
export const find: Find = async function find(
|
||||
this: DrizzleAdapter,
|
||||
{
|
||||
collection,
|
||||
collection: collectionSlug,
|
||||
draftsEnabled,
|
||||
joins,
|
||||
limit,
|
||||
@@ -22,11 +21,9 @@ export const find: Find = async function find(
|
||||
where,
|
||||
},
|
||||
) {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collectionConfig.defaultSort
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
|
||||
|
||||
return findMany({
|
||||
adapter: this,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
|
||||
@@ -44,7 +44,7 @@ export const buildFindManyArgs = ({
|
||||
select,
|
||||
tableName,
|
||||
versions,
|
||||
}: BuildFindQueryArgs): Record<string, unknown> => {
|
||||
}: BuildFindQueryArgs): any => {
|
||||
const result: Result = {
|
||||
extras: {},
|
||||
with: {},
|
||||
|
||||
@@ -80,7 +80,7 @@ export const findMany = async function find({
|
||||
if (orderBy) {
|
||||
for (const key in selectFields) {
|
||||
const column = selectFields[key]
|
||||
if (!column || column.primary) {
|
||||
if (column.primary) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -252,20 +252,6 @@ 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]}`}`
|
||||
@@ -513,7 +499,7 @@ export const traverseFields = ({
|
||||
const subQueryAlias = `${columnName}_subquery`
|
||||
|
||||
let sqlWhere = eq(
|
||||
sql.raw(`"${currentTableName}"."id"`),
|
||||
adapter.tables[currentTableName].id,
|
||||
sql.raw(`"${subQueryAlias}"."${onPath}"`),
|
||||
)
|
||||
|
||||
@@ -577,23 +563,19 @@ export const traverseFields = ({
|
||||
|
||||
let joinQueryWhere: Where
|
||||
|
||||
const currentIDRaw = sql.raw(
|
||||
`"${getNameFromDrizzleTable(currentIDColumn.table)}"."${currentIDColumn.name}"`,
|
||||
)
|
||||
|
||||
if (Array.isArray(field.targetField.relationTo)) {
|
||||
joinQueryWhere = {
|
||||
[field.on]: {
|
||||
equals: {
|
||||
relationTo: collectionSlug,
|
||||
value: rawConstraint(currentIDRaw),
|
||||
value: rawConstraint(currentIDColumn),
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
joinQueryWhere = {
|
||||
[field.on]: {
|
||||
equals: rawConstraint(currentIDRaw),
|
||||
equals: rawConstraint(currentIDColumn),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user