Compare commits
68 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c19afcf91 | ||
|
|
0a15388edb | ||
|
|
a421503111 | ||
|
|
b86594f3f9 | ||
|
|
e3172f1e39 | ||
|
|
ee117bb616 | ||
|
|
dc111041cb | ||
|
|
f67761fe22 | ||
|
|
010ac2ac0c | ||
|
|
d20445b6f3 | ||
|
|
1f26237ba1 | ||
|
|
721ae79716 | ||
|
|
1584c41790 | ||
|
|
963fee83e0 | ||
|
|
b32c4defd9 | ||
|
|
a6e7305696 | ||
|
|
0cd83f0591 | ||
|
|
cd49783105 | ||
|
|
5b77653fe7 | ||
|
|
1d5d30391e | ||
|
|
2c0caab761 | ||
|
|
57e535e646 | ||
|
|
8c10a23fa2 | ||
|
|
80b69ac53d | ||
|
|
e2607d4faa | ||
|
|
1267aedfd3 | ||
|
|
e907724af7 | ||
|
|
320916f542 | ||
|
|
015580aa32 | ||
|
|
f1ba9ca82a | ||
|
|
f878e35cc7 | ||
|
|
f0f96e7558 | ||
|
|
0165ab8930 | ||
|
|
213b7c6fb6 | ||
|
|
7dc52567f1 | ||
|
|
a22c0e62fa | ||
|
|
147d28e62c | ||
|
|
f507305192 | ||
|
|
d42529055a | ||
|
|
4b4ecb386d | ||
|
|
becf56d582 | ||
|
|
8a5f6f044d | ||
|
|
93a55d1075 | ||
|
|
cdcefa88f2 | ||
|
|
5c049f7c9c | ||
|
|
ae6fb4dd1b | ||
|
|
9ce2ba6a3f | ||
|
|
f52b7c45c0 | ||
|
|
2eeed4a8ae | ||
|
|
c0335aa49e | ||
|
|
3ca203e08c | ||
|
|
50f3ca93ee | ||
|
|
4652e8d56e | ||
|
|
2175e5cdfb | ||
|
|
201d68663e | ||
|
|
ebd3c025b7 | ||
|
|
ddc9d9731a | ||
|
|
3e31b7aec9 | ||
|
|
e390835711 | ||
|
|
35b107a103 | ||
|
|
6b9f178fcb | ||
|
|
cca6746e1e | ||
|
|
4349b78a2b | ||
|
|
5b97ac1a67 | ||
|
|
f10a160462 | ||
|
|
59ff8c18f5 | ||
|
|
10d5a8f9ae | ||
|
|
48d2ac1fce |
8
.github/actions/triage/action.yml
vendored
8
.github/actions/triage/action.yml
vendored
@@ -17,9 +17,9 @@ inputs:
|
||||
reproduction-link-section:
|
||||
description: 'A regular expression string with "(.*)" matching a valid URL in the issue body. The result is trimmed. Example: "### Link to reproduction(.*)### To reproduce"'
|
||||
default: '### Link to reproduction(.*)### To reproduce'
|
||||
tag-only:
|
||||
description: Log and tag only. Do not perform closing or commenting actions.
|
||||
default: false
|
||||
actions-to-perform:
|
||||
description: 'Comma-separated list of actions to perform on the issue. Example: "tag,comment,close"'
|
||||
default: 'tag,comment,close'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
@@ -37,4 +37,4 @@ runs:
|
||||
'INPUT_REPRODUCTION_INVALID_LABEL': ${{inputs.reproduction-invalid-label}}
|
||||
'INPUT_REPRODUCTION_ISSUE_LABELS': ${{inputs.reproduction-issue-labels}}
|
||||
'INPUT_REPRODUCTION_LINK_SECTION': ${{inputs.reproduction-link-section}}
|
||||
'INPUT_TAG_ONLY': ${{inputs.tag-only}}
|
||||
'INPUT_ACTIONS_TO_PERFORM': ${{inputs.actions-to-perform}}
|
||||
|
||||
34068
.github/actions/triage/dist/index.js
vendored
Normal file
34068
.github/actions/triage/dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
50
.github/actions/triage/src/index.ts
vendored
50
.github/actions/triage/src/index.ts
vendored
@@ -8,6 +8,9 @@ import { join } from 'node:path'
|
||||
if (!process.env.GITHUB_TOKEN) throw new TypeError('No GITHUB_TOKEN provided')
|
||||
if (!process.env.GITHUB_WORKSPACE) throw new TypeError('Not a GitHub workspace')
|
||||
|
||||
const validActionsToPerform = ['tag', 'comment', 'close'] as const
|
||||
type ActionsToPerform = (typeof validActionsToPerform)[number]
|
||||
|
||||
// Define the configuration object
|
||||
interface Config {
|
||||
invalidLink: {
|
||||
@@ -17,7 +20,7 @@ interface Config {
|
||||
label: string
|
||||
linkSection: string
|
||||
}
|
||||
tagOnly: boolean
|
||||
actionsToPerform: ActionsToPerform[]
|
||||
token: string
|
||||
workspace: string
|
||||
}
|
||||
@@ -33,7 +36,16 @@ const config: Config = {
|
||||
linkSection:
|
||||
getInput('reproduction_link_section') || '### Link to reproduction(.*)### To reproduce',
|
||||
},
|
||||
tagOnly: getBooleanOrUndefined('tag_only') || false,
|
||||
actionsToPerform: (getInput('actions_to_perform') || validActionsToPerform.join(','))
|
||||
.split(',')
|
||||
.map((a) => {
|
||||
const action = a.trim().toLowerCase() as ActionsToPerform
|
||||
if (validActionsToPerform.includes(action)) {
|
||||
return action
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid action: ${action}`)
|
||||
}),
|
||||
token: process.env.GITHUB_TOKEN,
|
||||
workspace: process.env.GITHUB_WORKSPACE,
|
||||
}
|
||||
@@ -104,23 +116,31 @@ async function checkValidReproduction(): Promise<void> {
|
||||
await Promise.all(
|
||||
labelsToRemove.map((label) => client.issues.removeLabel({ ...common, name: label })),
|
||||
)
|
||||
info(`Issue #${issue.number} - validate label removed`)
|
||||
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
|
||||
info(`Issue #${issue.number} - labeled`)
|
||||
|
||||
// If tagOnly, do not close or comment
|
||||
if (config.tagOnly) {
|
||||
info('Tag-only enabled, no closing/commenting actions taken')
|
||||
return
|
||||
// Tag
|
||||
if (config.actionsToPerform.includes('tag')) {
|
||||
info(`Added label: ${config.invalidLink.label}`)
|
||||
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
|
||||
} else {
|
||||
info('Tag - skipped, not provided in actions to perform')
|
||||
}
|
||||
|
||||
// Perform closing and commenting actions
|
||||
await client.issues.update({ ...common, state: 'closed' })
|
||||
info(`Issue #${issue.number} - closed`)
|
||||
// Comment
|
||||
if (config.actionsToPerform.includes('comment')) {
|
||||
const comment = join(config.workspace, config.invalidLink.comment)
|
||||
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
|
||||
info(`Commented with invalid reproduction message`)
|
||||
} else {
|
||||
info('Comment - skipped, not provided in actions to perform')
|
||||
}
|
||||
|
||||
const comment = join(config.workspace, config.invalidLink.comment)
|
||||
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
|
||||
info(`Issue #${issue.number} - commented`)
|
||||
// Close
|
||||
if (config.actionsToPerform.includes('close')) {
|
||||
await client.issues.update({ ...common, state: 'closed' })
|
||||
info(`Closed issue #${issue.number}`)
|
||||
} else {
|
||||
info('Close - skipped, not provided in actions to perform')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
4
.github/comments/invalid-reproduction.md
vendored
4
.github/comments/invalid-reproduction.md
vendored
@@ -1,4 +1,6 @@
|
||||
We cannot recreate the issue with the provided information. **Please add a reproduction in order for us to be able to investigate.**
|
||||
**Please add a reproduction in order for us to be able to investigate.**
|
||||
|
||||
Depending on the quality of reproduction steps, this issue may be closed if no reproduction is provided.
|
||||
|
||||
### Why was this issue marked with the `invalid-reproduction` label?
|
||||
|
||||
|
||||
155
.github/workflows/main.yml
vendored
155
.github/workflows/main.yml
vendored
@@ -48,6 +48,7 @@ jobs:
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package.json'
|
||||
- 'templates/**'
|
||||
templates:
|
||||
- 'templates/**'
|
||||
- name: Log all filter results
|
||||
@@ -187,6 +188,7 @@ jobs:
|
||||
tests-int:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
name: int-${{ matrix.database }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -206,6 +208,19 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: localstack
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
services:
|
||||
postgres:
|
||||
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 }}
|
||||
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ env.POSTGRES_DB }}
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -230,15 +245,6 @@ jobs:
|
||||
- name: Start LocalStack
|
||||
run: pnpm docker:start
|
||||
|
||||
- name: Start PostgreSQL
|
||||
uses: CasperWA/postgresql-action@v1.2
|
||||
with:
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
postgresql db: ${{ env.POSTGRES_DB }}
|
||||
postgresql user: ${{ env.POSTGRES_USER }}
|
||||
postgresql password: ${{ env.POSTGRES_PASSWORD }}
|
||||
if: startsWith(matrix.database, 'postgres')
|
||||
|
||||
- name: Install Supabase CLI
|
||||
uses: supabase/setup-cli@v1
|
||||
with:
|
||||
@@ -251,10 +257,6 @@ jobs:
|
||||
supabase start
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Wait for PostgreSQL
|
||||
run: sleep 30
|
||||
if: startsWith(matrix.database, 'postgres')
|
||||
|
||||
- name: Configure PostgreSQL
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
|
||||
@@ -282,6 +284,7 @@ jobs:
|
||||
tests-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
name: ${{ matrix.suite }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -390,10 +393,33 @@ jobs:
|
||||
# report-tag: ${{ matrix.suite }}
|
||||
# job-summary: true
|
||||
|
||||
app-build-with-packed:
|
||||
if: false # Disable until package resolution in tgzs can be figured out
|
||||
# Build listed templates with packed local packages
|
||||
build-templates:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
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
|
||||
|
||||
name: ${{ matrix.template }}-${{ matrix.database }}
|
||||
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: payloadtests
|
||||
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
@@ -418,22 +444,35 @@ jobs:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Start PostgreSQL
|
||||
uses: CasperWA/postgresql-action@v1.2
|
||||
with:
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
postgresql db: ${{ env.POSTGRES_DB }}
|
||||
postgresql user: ${{ env.POSTGRES_USER }}
|
||||
postgresql password: ${{ env.POSTGRES_PASSWORD }}
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- name: Wait for PostgreSQL
|
||||
run: sleep 30
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- name: Configure PostgreSQL
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
|
||||
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
- name: Pack and build app
|
||||
- name: Build Template
|
||||
run: |
|
||||
set -ex
|
||||
pnpm run script:pack --dest templates/blank
|
||||
cd templates/blank
|
||||
cp .env.example .env
|
||||
ls -la
|
||||
pnpm add ./*.tgz --ignore-workspace
|
||||
pnpm install --ignore-workspace --no-frozen-lockfile
|
||||
cat package.json
|
||||
pnpm run build
|
||||
pnpm run script:pack --dest templates/${{ matrix.template }}
|
||||
pnpm runts scripts/build-template-with-local-pkgs.ts ${{ matrix.template }} $POSTGRES_URL
|
||||
|
||||
tests-type-generation:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -468,72 +507,6 @@ jobs:
|
||||
- name: Generate GraphQL schema file
|
||||
run: pnpm dev:generate-graphql-schema graphql-schema-gen
|
||||
|
||||
templates:
|
||||
needs: changes
|
||||
if: false # Disable until templates are updated for 3.0
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template: [blank, website, ecommerce]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
- name: Build Template
|
||||
run: |
|
||||
cd templates/${{ matrix.template }}
|
||||
cp .env.example .env
|
||||
yarn install
|
||||
yarn build
|
||||
yarn generate:types
|
||||
|
||||
generated-templates:
|
||||
needs: build
|
||||
if: false # Needs to pull in tgz files from build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build all generated templates
|
||||
run: pnpm tsx ./scripts/build-generated-templates.ts
|
||||
|
||||
all-green:
|
||||
name: All Green
|
||||
if: always()
|
||||
|
||||
2
.github/workflows/triage.yml
vendored
2
.github/workflows/triage.yml
vendored
@@ -99,4 +99,4 @@ jobs:
|
||||
reproduction-comment: '.github/comments/invalid-reproduction.md'
|
||||
reproduction-link-section: '### Link to the code that reproduces this issue(.*)### Reproduction Steps'
|
||||
reproduction-issue-labels: 'validate-reproduction'
|
||||
tag-only: 'true'
|
||||
actions-to-perform: 'tag,comment'
|
||||
|
||||
74
README.md
74
README.md
@@ -13,74 +13,70 @@
|
||||
</p>
|
||||
<hr/>
|
||||
<h4>
|
||||
<a target="_blank" href="https://payloadcms.com/docs/getting-started/what-is-payload" rel="dofollow"><strong>Explore the Docs</strong></a> · <a target="_blank" href="https://payloadcms.com/community-help" rel="dofollow"><strong>Community Help</strong></a> · <a target="_blank" href="https://demo.payloadcms.com/" rel="dofollow"><strong>Try Live Demo</strong></a> · <a target="_blank" href="https://github.com/payloadcms/payload/discussions/1539" rel="dofollow"><strong>Roadmap</strong></a> · <a target="_blank" href="https://www.g2.com/products/payload-cms/reviews#reviews" rel="dofollow"><strong>View G2 Reviews</strong></a>
|
||||
<a target="_blank" href="https://payloadcms.com/docs/beta/getting-started/what-is-payload" rel="dofollow"><strong>Explore the Docs</strong></a> · <a target="_blank" href="https://payloadcms.com/community-help" rel="dofollow"><strong>Community Help</strong></a> · <a target="_blank" href="https://github.com/payloadcms/payload/discussions/1539" rel="dofollow"><strong>Roadmap</strong></a> · <a target="_blank" href="https://www.g2.com/products/payload-cms/reviews#reviews" rel="dofollow"><strong>View G2 Reviews</strong></a>
|
||||
</h4>
|
||||
<hr/>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
|
||||
> 🚨 <strong>We're about to release 3.0 stable.</strong> Star this repo or keep an eye on it to follow along.
|
||||
|
||||
Payload is the first-ever Next.js native CMS that can install directly in your existing `/app` folder. It's the start of a new era for headless CMS.
|
||||
|
||||
<h3>Benefits over a regular CMS</h3>
|
||||
<ul>
|
||||
<li>Don’t hit some third-party SaaS API, hit your own API</li>
|
||||
<li>Use your own database and own your data</li>
|
||||
<li>It's just Express - do what you want outside of Payload</li>
|
||||
<li>No need to learn how Payload works - if you know JS, you know Payload</li>
|
||||
<li>Deploy anywhere, including serverless on Vercel for free</li>
|
||||
<li>Combine your front+backend in the same <code>/app</code> folder if you want</li>
|
||||
<li>Don't sign up for yet another SaaS - Payload is open source</li>
|
||||
<li>Query your database in React Server Components</li>
|
||||
<li>Both admin and backend are 100% extensible</li>
|
||||
<li>No vendor lock-in</li>
|
||||
<li>Avoid microservices hell - get everything (even auth) in one place</li>
|
||||
<li>Never touch ancient WP code again</li>
|
||||
<li>Build faster, never hit a roadblock</li>
|
||||
<li>Both admin and backend are 100% extensible</li>
|
||||
</ul>
|
||||
|
||||
## ☁️ Deploy instantly with Payload Cloud.
|
||||
## Quickstart
|
||||
|
||||
Create a cloud account, connect your GitHub, and [deploy in minutes](https://payloadcms.com/new).
|
||||
|
||||
## 🚀 Get started by self-hosting completely free, forever.
|
||||
|
||||
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
|
||||
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/beta/getting-started/installation).
|
||||
|
||||
```text
|
||||
npx create-payload-app@latest
|
||||
pnpx create-payload-app@beta
|
||||
```
|
||||
|
||||
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).
|
||||
**If you're new to Payload, you should start with the 3.0 beta website template** (`pnpx create-payload-app@beta -t website`). It shows how to do _everything_ - including custom Rich Text blocks, on-demand revalidation, live preview, and more. It comes with a frontend built with Tailwind all in one `/app` folder.
|
||||
|
||||
## 🖱️ One-click templates
|
||||
## One-click templates
|
||||
|
||||
Jumpstart your next project by starting with a pre-made template. These are production-ready, end-to-end solutions designed to get you to market as fast as possible.
|
||||
|
||||
### [🛒 E-Commerce](https://github.com/payloadcms/payload/tree/main/templates/ecommerce)
|
||||
### [🌐 Website](https://github.com/payloadcms/payload/tree/beta/templates/website)
|
||||
|
||||
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a fully functional front-end built with RSCs and Tailwind.
|
||||
|
||||
### [🛒 E-Commerce](https://github.com/payloadcms/payload/tree/beta/templates/ecommerce)
|
||||
|
||||
Eliminate the need to combine Shopify and a CMS, and instead do it all with Payload + Stripe. Comes with a beautiful, fully functional front-end complete with shopping cart, checkout, orders, and much more.
|
||||
|
||||
### [🌐 Website](https://github.com/payloadcms/payload/tree/main/templates/website)
|
||||
We're constantly adding more templates to our [Templates Directory](https://github.com/payloadcms/payload/tree/beta/templates). If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.
|
||||
|
||||
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a beautiful, fully functional front-end complete with posts, projects, comments, and much more.
|
||||
|
||||
We're constantly adding more templates to our [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates). If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.
|
||||
|
||||
- [Official Templates](https://github.com/payloadcms/payload/tree/main/templates)
|
||||
- [Official Templates](https://github.com/payloadcms/payload/tree/beta/templates)
|
||||
- [Community Templates](https://github.com/topics/payload-template)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- Completely free and open-source
|
||||
- [GraphQL](https://payloadcms.com/docs/graphql/overview), [REST](https://payloadcms.com/docs/rest-api/overview), and [Local](https://payloadcms.com/docs/local-api/overview) APIs
|
||||
- [Easily customizable ReactJS Admin](https://payloadcms.com/docs/admin/overview)
|
||||
- [Fully self-hosted](https://payloadcms.com/docs/production/deployment)
|
||||
- [Extensible Authentication](https://payloadcms.com/docs/authentication/overview)
|
||||
- [Local file storage & upload](https://payloadcms.com/docs/upload/overview)
|
||||
- [Version History and Drafts](https://payloadcms.com/docs/versions/overview)
|
||||
- [Field-based Localization](https://payloadcms.com/docs/configuration/localization)
|
||||
- [Block-based Layout Builder](https://payloadcms.com/docs/fields/blocks)
|
||||
- [Extensible SlateJS rich text editor](https://payloadcms.com/docs/fields/rich-text)
|
||||
- [Array field type](https://payloadcms.com/docs/fields/array)
|
||||
- [Field conditional logic](https://payloadcms.com/docs/fields/overview#conditional-logic)
|
||||
- Extremely granular [Access Control](https://payloadcms.com/docs/access-control/overview)
|
||||
- [Document and field-level hooks](https://payloadcms.com/docs/hooks/overview) for every action Payload provides
|
||||
- Built with Typescript & very Typescript-friendly
|
||||
- Next.js native, built to run inside _your_ `/app` folder
|
||||
- Use server components to extend Payload UI
|
||||
- Query your database directly in server components, no need for REST / GraphQL
|
||||
- Fully TypeScript with automatic types for your data
|
||||
- [Auth out of the box](https://payloadcms.com/docs/beta/authentication/overview)
|
||||
- [Versions and drafts](https://payloadcms.com/docs/beta/versions/overview)
|
||||
- [Localization](https://payloadcms.com/docs/beta/configuration/localization)
|
||||
- [Block-based kayout builder](https://payloadcms.com/docs/beta/fields/blocks)
|
||||
- [Customizable React admin](https://payloadcms.com/docs/beta/admin/overview)
|
||||
- [Lexical rich text editor](https://payloadcms.com/docs/beta/fields/rich-text)
|
||||
- [Conditional field logic](https://payloadcms.com/docs/beta/fields/overview#conditional-logic)
|
||||
- Extremely granular [Access Control](https://payloadcms.com/docs/beta/access-control/overview)
|
||||
- [Document and field-level hooks](https://payloadcms.com/docs/beta/hooks/overview) for every action Payload provides
|
||||
- Intensely fast API
|
||||
- Highly secure thanks to HTTP-only cookies, CSRF protection, and more
|
||||
|
||||
@@ -88,7 +84,7 @@ We're constantly adding more templates to our [Templates Directory](https://gith
|
||||
|
||||
## 🗒️ Documentation
|
||||
|
||||
Check out the [Payload website](https://payloadcms.com/docs/getting-started/what-is-payload) to find in-depth documentation for everything that Payload offers.
|
||||
Check out the [Payload website](https://payloadcms.com/docs/beta/getting-started/what-is-payload) to find in-depth documentation for everything that Payload offers.
|
||||
|
||||
Migrating from v1 to v2? Check out the [2.0 Release Notes](https://github.com/payloadcms/payload/releases/tag/v2.0.0) on how to do it.
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Document Locking
|
||||
label: Document Locking
|
||||
order: 90
|
||||
desc: Ensure your documents are locked while being edited, preventing concurrent edits from multiple users and preserving data integrity.
|
||||
desc: Ensure your documents are locked during editing to prevent concurrent changes from multiple users and maintain data integrity.
|
||||
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
|
||||
---
|
||||
|
||||
@@ -12,19 +12,19 @@ The lock is automatically triggered when a user begins editing a document within
|
||||
|
||||
## How it works
|
||||
|
||||
When a user starts editing a document, Payload locks the document for that user. If another user tries to access the same document, they will be notified that it is currently being edited and can choose one of the following options:
|
||||
When a user starts editing a document, Payload locks it for that user. If another user attempts to access the same document, they will be notified that it is currently being edited. They can then choose one of the following options:
|
||||
|
||||
- View in Read-Only Mode: View the document without making any changes.
|
||||
- Take Over Editing: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
|
||||
- View in Read-Only: View the document without the ability to make any changes.
|
||||
- Take Over: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
|
||||
- Return to Dashboard: Navigate away from the locked document and continue with other tasks.
|
||||
|
||||
The lock will automatically expire after a set period of inactivity, configurable using the duration property in the lockDocuments configuration, after which others can resume editing.
|
||||
The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing.
|
||||
|
||||
<Banner type="info"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
|
||||
<Banner type="info"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection or global by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
|
||||
|
||||
### Config Options
|
||||
|
||||
The lockDocuments property exists on both the Collection Config and the Global Config. By default, document locking is enabled for all collections and globals, but you can customize the lock duration or disable the feature entirely.
|
||||
The `lockDocuments` property exists on both the Collection Config and the Global Config. Document locking is enabled by default, but you can customize the lock duration or turn off the feature for any collection or global.
|
||||
|
||||
Here’s an example configuration for document locking:
|
||||
|
||||
@@ -55,13 +55,13 @@ export const Posts: CollectionConfig = {
|
||||
|
||||
### Impact on APIs
|
||||
|
||||
Document locking affects both the Local API and the REST API, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
|
||||
Document locking affects both the Local and REST APIs, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
|
||||
|
||||
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
|
||||
|
||||
#### Overriding Locks
|
||||
|
||||
For operations like update and delete, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
|
||||
For operations like `update` and `delete`, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
|
||||
|
||||
By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Email Verification allows users to verify their email address before they'
|
||||
keywords: authentication, email, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
[Authentication](./overview) ties directly into the [Email](../email) functionality that Payload provides. This allows you to send emails to users for verification, password resets, and more. While Payload provides default email templates for these actions, you can customize them to fit your brand.
|
||||
[Authentication](./overview) ties directly into the [Email](../email/overview) functionality that Payload provides. This allows you to send emails to users for verification, password resets, and more. While Payload provides default email templates for these actions, you can customize them to fit your brand.
|
||||
|
||||
## Email Verification
|
||||
|
||||
|
||||
@@ -67,6 +67,6 @@ You should prefer a relational DB like Postgres or SQLite if:
|
||||
|
||||
## Payload Differences
|
||||
|
||||
It's important to note that nearly every Payload feature is available in all of our officially supported Database Adapters, including [Localization](../configuration/localization), [Arrays](../fields/array), [Blocks](../fields/blocks), etc. The only thing that is not supported in Postgres yet is the [Point Field](/docs/fields/point), but that should be added soon.
|
||||
It's important to note that nearly every Payload feature is available in all of our officially supported Database Adapters, including [Localization](../configuration/localization), [Arrays](../fields/array), [Blocks](../fields/blocks), etc. The only thing that is not supported in SQLite yet is the [Point Field](/docs/fields/point), but that should be added soon.
|
||||
|
||||
It's up to you to choose which database you would like to use based on the requirements of your project. Payload has no opinion on which database you should ultimately choose.
|
||||
|
||||
@@ -16,7 +16,7 @@ By default, Payload will use transactions for all data changing operations, as l
|
||||
MongoDB requires a connection to a replicaset in order to make use of transactions.
|
||||
</Banner>
|
||||
|
||||
The initial request made to Payload will begin a new transaction and attach it to the `req.transactionID`. If you have a `hook` that interacts with the database, you can opt-in to using the same transaction by passing the `req` in the arguments. For example:
|
||||
The initial request made to Payload will begin a new transaction and attach it to the `req.transactionID`. If you have a `hook` that interacts with the database, you can opt in to using the same transaction by passing the `req` in the arguments. For example:
|
||||
|
||||
```ts
|
||||
const afterChange: CollectionAfterChangeHook = async ({ req }) => {
|
||||
@@ -65,9 +65,9 @@ When writing your own scripts or custom endpoints, you may wish to have direct c
|
||||
|
||||
The following functions can be used for managing transactions:
|
||||
|
||||
`payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls.
|
||||
`payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes.
|
||||
`payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes.
|
||||
- `payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls.
|
||||
- `payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes.
|
||||
- `payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes.
|
||||
|
||||
Payload uses the `req` object to pass the transaction ID through to the database adapter. If you are not using the `req` object, you can make a new object to pass the transaction ID directly to database adapter methods and local API calls.
|
||||
Example:
|
||||
|
||||
@@ -126,12 +126,13 @@ powerful Admin UI.
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`collection`** \* | The `slug`s having the relationship field. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`where`** \* | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
|
||||
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
|
||||
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
|
||||
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
|
||||
@@ -182,11 +183,11 @@ returning. This is useful for performance reasons when you don't need the relate
|
||||
|
||||
The following query options are supported:
|
||||
|
||||
| Property | Description |
|
||||
|-------------|--------------------------------------------------------------|
|
||||
| **`limit`** | The maximum related documents to be returned, default is 10. |
|
||||
| **`where`** | An optional `Where` query to filter joined documents. |
|
||||
| **`sort`** | A string used to order related results |
|
||||
| Property | Description |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------|
|
||||
| **`limit`** | The maximum related documents to be returned, default is 10. |
|
||||
| **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. |
|
||||
| **`sort`** | A string used to order related results |
|
||||
|
||||
These can be applied to the local API, GraphQL, and REST API.
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ For full details on Admin Options, see the [Field Admin Options](../admin/fields
|
||||
|
||||
## Custom ID Fields
|
||||
|
||||
All [Collections](../configuration/collections) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This will force users to provide a their own ID value when creating a record.
|
||||
All [Collections](../configuration/collections) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This field should either be required or have a hook to generate the ID dynamically.
|
||||
|
||||
To define a custom ID field, add a new field with the `name` property set to `id`:
|
||||
|
||||
@@ -368,6 +368,7 @@ export const MyCollection: CollectionConfig = {
|
||||
fields: [
|
||||
{
|
||||
name: 'id', // highlight-line
|
||||
required: true,
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -28,7 +28,7 @@ export const MyPointField: Field = {
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
The Point Field is currently only supported in MongoDB.
|
||||
The Point Field currently is not supported in SQLite.
|
||||
</Banner>
|
||||
|
||||
## Config
|
||||
|
||||
@@ -48,6 +48,9 @@ export const MyUploadField: Field = {
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
||||
| **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with hasMany. |
|
||||
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
---
|
||||
title: Jobs Queue
|
||||
label: Jobs Queue
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Payload provides all you need to run job queues, which are helpful to offload long-running processes into separate workers.
|
||||
keywords: jobs queue, application framework, typescript, node, react, nextjs
|
||||
---
|
||||
|
||||
## Defining tasks
|
||||
Payload's Jobs Queue gives you a simple, yet powerful way to offload large or future tasks to separate compute resources.
|
||||
|
||||
A task is a simple function that can be executed directly or within a workflow. The difference between tasks and functions is that tasks can be run in the background, and can be retried if they fail.
|
||||
For example, when building applications with Payload, you might run into a case where you need to perform some complex logic in a Payload [Hook](/docs/hooks/overview) but you don't want that hook to "block" or slow down the response returned from the Payload API.
|
||||
|
||||
Tasks can either be defined within the `jobs.tasks` array in your payload config, or they can be run inline within a workflow.
|
||||
Instead of running long or expensive logic in a Hook, you can instead create a Job and add it to a Queue. It can then be picked up by a separate worker which periodically checks the queue for new jobs, and then executes each job accordingly. This way, your Payload API responses can remain as fast as possible, and you can still perform logic as necessary without blocking or affecting your users' experience.
|
||||
|
||||
Jobs are also handy for delegating certain actions to take place in the future, such as scheduling a post to be published at a later date. In this example, you could create a Job that will automatically publish a post at a certain time.
|
||||
|
||||
#### How it works
|
||||
|
||||
There are a few concepts that you should become familiarized with before using Payload's Jobs Queue - [Tasks](#tasks), [Workflows](#workflows), [Jobs](#jobs), and finally [Queues](#queues).
|
||||
|
||||
## Tasks
|
||||
|
||||
<Banner type="default">
|
||||
A <strong>"Task"</strong> is a function definition that performs business logic and whose input and output are both strongly typed.
|
||||
</Banner>
|
||||
|
||||
You can register Tasks on the Payload config, and then create Jobs or Workflows that use them. Think of Tasks like tidy, isolated "functions that do one specific thing".
|
||||
|
||||
Payload Tasks can be configured to automatically retried if they fail, which makes them valuable for "durable" workflows like AI applications where LLMs can return non-deterministic results, and might need to be retried.
|
||||
|
||||
Tasks can either be defined within the `jobs.tasks` array in your payload config, or they can be defined inline within a workflow.
|
||||
|
||||
### Defining tasks in the config
|
||||
|
||||
@@ -25,10 +43,12 @@ Simply add a task to the `jobs.tasks` array in your Payload config. A task consi
|
||||
| `outputSchema` | Define the output field schema - payload will generate a type for this schema. |
|
||||
| `label` | Define a human-friendly label for this task. |
|
||||
| `onFail` | Function to be executed if the task fails. |
|
||||
| `onSuccess` | Function to be executed if the task fails. |
|
||||
| `onSuccess` | Function to be executed if the task succeeds. |
|
||||
| `retries` | Specify the number of times that this step should be retried if it fails. |
|
||||
|
||||
The handler is the function, or a path to the function, that will run once the job picks up this task. The handler function should return an object with an `output` key, which should contain the output of the task.
|
||||
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks picks up a Job that includes this task.
|
||||
|
||||
It should return an object with an `output` key, which should contain the output of the task as you've defined.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -38,8 +58,15 @@ export default buildConfig({
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
// Configure this task to automatically retry
|
||||
// up to two times
|
||||
retries: 2,
|
||||
|
||||
// This is a unique identifier for the task
|
||||
|
||||
slug: 'createPost',
|
||||
|
||||
// These are the arguments that your Task will accept
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -47,6 +74,8 @@ export default buildConfig({
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// These are the properties that the function should output
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'postID',
|
||||
@@ -54,6 +83,8 @@ export default buildConfig({
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// This is the function that is run when the task is invoked
|
||||
handler: async ({ input, job, req }) => {
|
||||
const newPost = await req.payload.create({
|
||||
collection: 'post',
|
||||
@@ -74,9 +105,11 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
### Example: defining external tasks
|
||||
In addition to defining handlers as functions directly provided to your Payload config, you can also pass an _absolute path_ to where the handler is defined. If your task has large dependencies, and you are planning on executing your jobs in a separate process that has access to the filesystem, this could be a handy way to make sure that your Payload + Next.js app remains quick to compile and has minimal dependencies.
|
||||
|
||||
payload.config.ts:
|
||||
In general, this is an advanced use case. Here's how this would look:
|
||||
|
||||
`payload.config.ts:`
|
||||
|
||||
```ts
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -86,26 +119,11 @@ const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
retries: 2,
|
||||
slug: 'createPost',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
// ...
|
||||
// The #createPostHandler is a named export within the `createPost.ts` file
|
||||
handler: path.resolve(dirname, 'src/tasks/createPost.ts') + '#createPostHandler',
|
||||
}
|
||||
]
|
||||
@@ -113,7 +131,9 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
src/tasks/createPost.ts:
|
||||
Then, the `createPost` file itself:
|
||||
|
||||
`src/tasks/createPost.ts:`
|
||||
|
||||
```ts
|
||||
import type { TaskHandler } from 'payload'
|
||||
@@ -134,18 +154,23 @@ export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job,
|
||||
}
|
||||
```
|
||||
|
||||
## Workflows
|
||||
|
||||
## Defining workflows
|
||||
<Banner type="default">
|
||||
A <strong>"Workflow"</strong> is an optional way to <em>combine multiple tasks together</em> in a way that can be gracefully retried from the point of failure.
|
||||
</Banner>
|
||||
|
||||
There are two types of workflows - JS-based workflows and JSON-based workflows.
|
||||
They're most helpful when you have multiple tasks in a row, and you want to configure each task to be able to be retried if they fail.
|
||||
|
||||
### Defining JS-based workflows
|
||||
If a task within a workflow fails, the Workflow will automatically "pick back up" on the task where it failed and **not re-execute any prior tasks that have already been executed**.
|
||||
|
||||
A JS-based function is a function in which you decide yourself when the tasks should run, by simply calling the `runTask` function. If the job, or any task within the job, fails, the entire function will re-run.
|
||||
#### Defining a workflow
|
||||
|
||||
Tasks that have successfully been completed will simply re-return the cached output without running again, and failed tasks will be re-run.
|
||||
The most important aspect of a Workflow is the `handler`, where you can declare when and how the tasks should run by simply calling the `runTask` function. If any task within the workflow, fails, the entire `handler` function will re-run.
|
||||
|
||||
Simply add a workflow to the `jobs.wokflows` array in your Payload config. A wokflow consists of the following fields:
|
||||
However, importantly, tasks that have successfully been completed will simply re-return the cached and saved output without running again. The Workflow will pick back up where it failed and only task from the failure point onward will be re-executed.
|
||||
|
||||
To define a JS-based workflow, simply add a workflow to the `jobs.wokflows` array in your Payload config. A workflow consists of the following fields:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
@@ -168,6 +193,8 @@ export default buildConfig({
|
||||
workflows: [
|
||||
{
|
||||
slug: 'createPostAndUpdate',
|
||||
|
||||
// The arguments that the workflow will accept
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -175,15 +202,26 @@ export default buildConfig({
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// The handler that defines the "control flow" of the workflow
|
||||
// Notice how it calls `runTask` to execute tasks
|
||||
handler: async ({ job, runTask }) => {
|
||||
|
||||
// This workflow first runs a task called `createPost`
|
||||
const output = await runTask({
|
||||
task: 'createPost',
|
||||
|
||||
// You need to define a unique ID for this task invocation
|
||||
// that will always be the same if this workflow fails
|
||||
// and is re-executed in the future
|
||||
id: '1',
|
||||
input: {
|
||||
title: job.input.title,
|
||||
},
|
||||
})
|
||||
|
||||
// Once the prior task completes, it will run a task
|
||||
// called `updatePost`
|
||||
await runTask({
|
||||
task: 'updatePost',
|
||||
id: '2',
|
||||
@@ -201,9 +239,11 @@ export default buildConfig({
|
||||
|
||||
#### Running tasks inline
|
||||
|
||||
In order to run tasks inline without predefining them, you can use the `runTaskInline` function.
|
||||
In the above example, our workflow was executing tasks that we already had defined in our Payload config. But, you can also run tasks without predefining them.
|
||||
|
||||
The drawbacks of this approach are that tasks cannot be re-used as easily, and the **task data stored in the job** will not be typed. In the following example, the inline task data will be stored on the job under `job.taskStatus.inline['2']` but completely untyped, as types for dynamic tasks like these cannot be generated beforehand.
|
||||
To do this, you can use the `runTaskInline` function.
|
||||
|
||||
The drawbacks of this approach are that tasks cannot be re-used across workflows as easily, and the **task data stored in the job** will not be typed. In the following example, the inline task data will be stored on the job under `job.taskStatus.inline['2']` but completely untyped, as types for dynamic tasks like these cannot be generated beforehand.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -225,6 +265,9 @@ export default buildConfig({
|
||||
},
|
||||
],
|
||||
handler: async ({ job, runTask }) => {
|
||||
// Here, we run a predefined task.
|
||||
// The `createPost` handler arguments and return type
|
||||
// are both strongly typed
|
||||
const output = await runTask({
|
||||
task: 'createPost',
|
||||
id: '1',
|
||||
@@ -233,11 +276,15 @@ export default buildConfig({
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// Here, this task is not defined in the Payload config
|
||||
// and is "inline". Its output will be stored on the Job in the database
|
||||
// however its arguments will be untyped.
|
||||
const { newPost } = await runTaskInline({
|
||||
task: async ({ req }) => {
|
||||
const newPost = await req.payload.update({
|
||||
collection: 'post',
|
||||
id: output.postID,
|
||||
id: '2',
|
||||
req,
|
||||
retries: 3,
|
||||
data: {
|
||||
@@ -259,28 +306,37 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
### Defining JSON-based workflows
|
||||
## Jobs
|
||||
|
||||
JSON-based workflows are a way to define the tasks the workflow should run in an array. The relationships between the tasks, their run order and their conditions are defined in the JSON object, which allows payload to statically analyze the workflow and will generate more helpful graphs.
|
||||
Now that we have covered Tasks and Workflows, we can tie them together with a concept called a Job.
|
||||
|
||||
This functionality is not available yet, but it will be available in the future.
|
||||
<Banner type="default">
|
||||
Whereas you define Workflows and Tasks, which control your business logic, a <strong>Job</strong> is an individual instance of either a Task or a Workflow which contains many tasks.
|
||||
</Banner>
|
||||
|
||||
## Queueing workflows and tasks
|
||||
For example, let's say we have a Workflow or Task that describes the logic to sync information from Payload to a third-party system. This is how you'd declare how to sync that info, but it wouldn't do anything on its own. In order to run that task or workflow, you'd create a Job that references the corresponding Task or Workflow.
|
||||
|
||||
In order to queue a workflow or a task (= create them and add them to the queue), you can use the `payload.jobs.queue` function.
|
||||
Jobs are stored in the Payload database in the `payload-jobs` collection, and you can decide to keep a running list of all jobs, or configure Payload to delete the job when it has been successfully executed.
|
||||
|
||||
Example: queueing workflows:
|
||||
#### Queuing a new job
|
||||
|
||||
In order to queue a job, you can use the `payload.jobs.queue` function.
|
||||
|
||||
Here's how you'd queue a new Job, which will run a `createPostAndUpdate` workflow:
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
workflows: 'createPostAndUpdate',
|
||||
// Pass the name of the workflow
|
||||
workflow: 'createPostAndUpdate',
|
||||
// The input type will be automatically typed
|
||||
// according to the input you've defined for this workflow
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Example: queueing tasks:
|
||||
In addition to being able to queue new Jobs based on Workflows, you can also queue a job for a single Task:
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
@@ -291,55 +347,51 @@ const createdJob = await payload.jobs.queue({
|
||||
})
|
||||
```
|
||||
|
||||
## Running workflows and tasks
|
||||
## Queues
|
||||
|
||||
Workflows and tasks added to the queue will not run unless a worker picks it up and runs it. This can be done in two ways:
|
||||
Now let's talk about how to _run these jobs_. Right now, all we've covered is how to queue up jobs to run, but so far, we aren't actually running any jobs. This is the final piece of the puzzle.
|
||||
|
||||
### Endpoint
|
||||
<Banner type="default">
|
||||
A <strong>Queue</strong> is a list of jobs that should be executed in order of when they were added.
|
||||
</Banner>
|
||||
|
||||
Make a fetch request to the `api/payload-jobs/run` endpoint:
|
||||
When you go to run jobs, Payload will query for any jobs that are added to the queue and then run them. By default, all queued jobs are added to the `default` queue.
|
||||
|
||||
**But, imagine if you wanted to have some jobs that run nightly, and other jobs which should run every five minutes.**
|
||||
|
||||
By specifying the `queue` name when you queue a new job using `payload.jobs.queue()`, you can queue certain jobs with `queue: 'nightly'`, and other jobs can be left as the default queue.
|
||||
|
||||
Then, you could configure two different runner strategies:
|
||||
|
||||
1. A `cron` that runs nightly, querying for jobs added to the `nightly` queue
|
||||
2. Another that runs any jobs that were added to the `default` queue every ~5 minutes or so
|
||||
|
||||
## Executing jobs
|
||||
|
||||
As mentioned above, you can queue jobs, but the jobs won't run unless a worker picks up your jobs and runs them. This can be done in two ways:
|
||||
|
||||
#### Endpoint
|
||||
|
||||
You can execute jobs by making a fetch request to the `/api/payload-jobs/run` endpoint:
|
||||
|
||||
```ts
|
||||
await fetch('/api/payload-jobs/run', {
|
||||
// Here, we're saying we want to run only 100 jobs for this invocation
|
||||
// and we want to pull jobs from the `nightly` queue:
|
||||
await fetch('/api/payload-jobs/run?limit=100&queue=nightly', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `JWT ${token}`,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Local API
|
||||
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.
|
||||
|
||||
Run the payload.jobs.run function:
|
||||
**Vercel Cron Example**
|
||||
|
||||
```ts
|
||||
const results = await payload.jobs.run()
|
||||
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.
|
||||
|
||||
// You can customize the queue name by passing it as an argument
|
||||
await payload.jobs.run({ queue: 'posts' })
|
||||
```
|
||||
|
||||
### Script
|
||||
|
||||
You can run the jobs:run script from the command line:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --queue default --limit 10
|
||||
```
|
||||
|
||||
#### Triggering jobs as cronjob
|
||||
|
||||
You can pass the --cron flag to the jobs:run script to run the jobs in a cronjob:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --cron "*/5 * * * *"
|
||||
```
|
||||
|
||||
### Vercel Cron
|
||||
|
||||
Vercel Cron allows scheduled tasks to be executed automatically by triggering specific endpoints. Below is a step-by-step guide to configuring Vercel Cron for running queued jobs on apps hosted on Vercel:
|
||||
|
||||
1. Add Vercel Cron Configuration: Place a vercel.json file at the root of your project with the following content:
|
||||
Here's an example of what this file will look like:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -352,13 +404,13 @@ Vercel Cron allows scheduled tasks to be executed automatically by triggering sp
|
||||
}
|
||||
```
|
||||
|
||||
This configuration schedules the endpoint `/api/payload-jobs/run` to be triggered every 5 minutes. This endpoint is added automatically by payload and is responsible for running the queued jobs.
|
||||
The configuration above schedules the endpoint `/api/payload-jobs/run` to be invoked every 5 minutes.
|
||||
|
||||
2. Environment Variable Setup: By default, the endpoint may require a JWT token for authorization. However, Vercel Cron jobs cannot pass JWT tokens. Instead, you can use an environment variable to secure the endpoint:
|
||||
The last step will be to secure your `run` endpoint so that only the proper users can invoke the runner.
|
||||
|
||||
Add a new environment variable named `CRON_SECRET` to your Vercel project settings. This should be a random string, ideally 16 characters or longer.
|
||||
To do this, you can set an environment variable on your Vercel project called `CRON_SECRET`, which should be a random string—ideally 16 characters or longer.
|
||||
|
||||
3. Modify Authentication for Job Running: Adjust the job running authorization logic in your project to accept the `CRON_SECRET` as a valid token. Modify your `payload.config.ts` file as follows:
|
||||
Then, you can modify the `access` function for running jobs by ensuring that only Vercel can invoke your runner.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
@@ -366,6 +418,12 @@ export default buildConfig({
|
||||
jobs: {
|
||||
access: {
|
||||
run: ({ req }: { req: PayloadRequest }): boolean => {
|
||||
// Allow logged in users to execute this endpoint (default)
|
||||
if (req.user) return true
|
||||
|
||||
// If there is no logged in user, then check
|
||||
// for the Vercel Cron secret to be present as an
|
||||
// Authorization header:
|
||||
const authHeader = req.headers.get('authorization');
|
||||
return authHeader === `Bearer ${process.env.CRON_SECRET}`;
|
||||
},
|
||||
@@ -375,8 +433,31 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
This code snippet ensures that the jobs can only be triggered if the correct `CRON_SECRET` is provided in the authorization header.
|
||||
|
||||
Vercel will automatically make the `CRON_SECRET` environment variable available to the endpoint when triggered by the Vercel Cron, ensuring that the jobs can be run securely.
|
||||
This works because Vercel automatically makes the `CRON_SECRET` environment variable available to the endpoint as the `Authorization` header when triggered by the Vercel Cron, ensuring that the jobs can be run securely.
|
||||
|
||||
After the project is deployed to Vercel, the Vercel Cron job will automatically trigger the `/api/payload-jobs/run` endpoint in the specified schedule, running the queued jobs in the background.
|
||||
|
||||
#### Local API
|
||||
|
||||
If you want to process jobs programmatically from your server-side code, you can use the Local API:
|
||||
|
||||
```ts
|
||||
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 })
|
||||
```
|
||||
|
||||
#### Bin script
|
||||
|
||||
Finally, you can process jobs via the bin script that comes with Payload out of the box.
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
```sh
|
||||
npx payload jobs:run --cron "*/5 * * * *"
|
||||
```
|
||||
|
||||
@@ -84,6 +84,7 @@ You can specify more options within the Local API vs. REST or GraphQL due to the
|
||||
| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. |
|
||||
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
|
||||
| `select` | Specify [select](../queries/select) to control which fields to include to the result. |
|
||||
| `populate` | Specify [populate](../queries/select#populate) to control which fields to include to the result from populated documents. |
|
||||
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
|
||||
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
|
||||
| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). |
|
||||
|
||||
@@ -128,3 +128,31 @@ export const Pages: CollectionConfig<'pages'> = {
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## `populate`
|
||||
|
||||
You can override `defaultPopulate` with the `populate` property in the Local and REST API
|
||||
|
||||
Local API:
|
||||
```ts
|
||||
const getPosts = async () => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
populate: {
|
||||
// Select only `text` from populated docs in the "pages" collection
|
||||
pages: {
|
||||
text: true,
|
||||
}, // highlight-line
|
||||
},
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
```
|
||||
|
||||
REST API:
|
||||
```ts
|
||||
fetch('https://localhost:3000/api/posts?populate[pages][text]=true') // highlight-line
|
||||
.then((res) => res.json())
|
||||
.then((data) => console.log(data))
|
||||
```
|
||||
|
||||
@@ -19,6 +19,7 @@ All Payload API routes are mounted and prefixed to your config's `routes.api` UR
|
||||
- [locale](/docs/configuration/localization#retrieving-localized-docs) - retrieves document(s) in a specific locale
|
||||
- [fallback-locale](/docs/configuration/localization#retrieving-localized-docs) - specifies a fallback locale if no locale value exists
|
||||
- [select](../queries/select) - specifies which fields to include to the result
|
||||
- [populate](../queries/select#populate) - specifies which fields to include to the result from populated documents
|
||||
|
||||
## Collections
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
{
|
||||
"name": "jest-payload",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "yarn build:server && yarn build:payload",
|
||||
"build:payload": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"dev": "nodemon",
|
||||
"generate:types": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"test": "jest --forceExit --detectOpenHandles"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.5",
|
||||
"@payloadcms/db-mongodb": "^1.1.0",
|
||||
"@payloadcms/richtext-slate": "^1.3.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"get-tsconfig": "^4.7.0",
|
||||
"get-tsconfig": "4.8.1",
|
||||
"payload": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -21,14 +30,5 @@
|
||||
"nodemon": "^3.0.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"generate:types": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"dev": "nodemon",
|
||||
"build:payload": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn build:server && yarn build:payload",
|
||||
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"test": "jest --forceExit --detectOpenHandles"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -132,8 +132,8 @@
|
||||
"create-payload-app": "workspace:*",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-kit": "0.26.2",
|
||||
"drizzle-orm": "0.35.1",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"execa": "5.1.1",
|
||||
"form-data": "3.0.1",
|
||||
@@ -162,7 +162,7 @@
|
||||
"sort-package-json": "^2.10.0",
|
||||
"swc-plugin-transform-remove-imports": "1.15.0",
|
||||
"tempy": "1.0.1",
|
||||
"tsx": "4.19.1",
|
||||
"tsx": "4.19.2",
|
||||
"turbo": "^2.1.3",
|
||||
"typescript": "5.6.3"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
8
packages/create-payload-app/src/lib/constants.ts
Normal file
8
packages/create-payload-app/src/lib/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(dirname, '../../package.json'), 'utf-8'))
|
||||
export const PACKAGE_VERSION = packageJson.version
|
||||
@@ -101,7 +101,7 @@ export async function createProject(args: {
|
||||
})
|
||||
|
||||
// Remove yarn.lock file. This is only desired in Payload Cloud.
|
||||
const lockPath = path.resolve(projectDir, 'yarn.lock')
|
||||
const lockPath = path.resolve(projectDir, 'pnpm-lock.yaml')
|
||||
if (fse.existsSync(lockPath)) {
|
||||
await fse.remove(lockPath)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ProjectTemplate } from '../types.js'
|
||||
|
||||
import { error, info } from '../utils/log.js'
|
||||
import { PACKAGE_VERSION } from './constants.js'
|
||||
|
||||
export function validateTemplate(templateName: string): boolean {
|
||||
const validTemplates = getValidTemplates()
|
||||
@@ -18,13 +19,13 @@ export function getValidTemplates(): ProjectTemplate[] {
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
description: 'Blank 3.0 Template',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank#beta',
|
||||
url: `https://github.com/payloadcms/payload/templates/blank#v${PACKAGE_VERSION}`,
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'starter',
|
||||
description: 'Website Template',
|
||||
url: 'https://github.com/payloadcms/payload/templates/website#beta',
|
||||
url: `https://github.com/payloadcms/payload/templates/website#v${PACKAGE_VERSION}`,
|
||||
},
|
||||
|
||||
// Remove these until they have been updated for 3.0
|
||||
|
||||
@@ -8,6 +8,7 @@ import path from 'path'
|
||||
import type { CliArgs } from './types.js'
|
||||
|
||||
import { configurePayloadConfig } from './lib/configure-payload-config.js'
|
||||
import { PACKAGE_VERSION } from './lib/constants.js'
|
||||
import { createProject } from './lib/create-project.js'
|
||||
import { generateSecret } from './lib/generate-secret.js'
|
||||
import { getPackageManager } from './lib/get-package-manager.js'
|
||||
@@ -18,7 +19,7 @@ import { selectDb } from './lib/select-db.js'
|
||||
import { getValidTemplates, validateTemplate } from './lib/templates.js'
|
||||
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
|
||||
import { writeEnvFile } from './lib/write-env-file.js'
|
||||
import { error, info } from './utils/log.js'
|
||||
import { debug, error, info } from './utils/log.js'
|
||||
import {
|
||||
feedbackOutro,
|
||||
helpMessage,
|
||||
@@ -79,6 +80,8 @@ export class Main {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const debugFlag = this.args['--debug']
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('\n')
|
||||
p.intro(chalk.bgCyan(chalk.black(' create-payload-app ')))
|
||||
@@ -201,6 +204,10 @@ export class Main {
|
||||
}
|
||||
}
|
||||
|
||||
if (debugFlag) {
|
||||
debug(`Using templates from git tag: ${PACKAGE_VERSION}`)
|
||||
}
|
||||
|
||||
const validTemplates = getValidTemplates()
|
||||
const template = await parseTemplate(this.args, validTemplates)
|
||||
if (!template) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -20,6 +20,10 @@ export const create: Create = async function create(
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
})
|
||||
|
||||
if (this.payload.collections[collection].customIDType) {
|
||||
sanitizedData._id = sanitizedData.id
|
||||
}
|
||||
|
||||
try {
|
||||
;[doc] = await Model.create([sanitizedData], options)
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,15 +2,14 @@ import type { CreateMigration, MigrationTemplateArgs } from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getPredefinedMigration } from 'payload'
|
||||
import { getPredefinedMigration, writeMigrationIndex } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const migrationTemplate = ({ downSQL, imports, upSQL }: MigrationTemplateArgs): string => `import {
|
||||
MigrateUpArgs,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
} from '@payloadcms/db-mongodb'
|
||||
${imports}
|
||||
|
||||
${imports ?? ''}
|
||||
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
|
||||
${upSQL ?? ` // Migration code`}
|
||||
}
|
||||
@@ -51,5 +50,8 @@ export const createMigration: CreateMigration = async function createMigration({
|
||||
const fileName = migrationName ? `${timestamp}_${formattedName}.ts` : `${timestamp}_migration.ts`
|
||||
const filePath = `${dir}/${fileName}`
|
||||
fs.writeFileSync(filePath, migrationFileContent)
|
||||
|
||||
writeMigrationIndex({ migrationsDir: payload.db.migrationDir })
|
||||
|
||||
payload.logger.info({ msg: `Migration created at ${filePath}` })
|
||||
}
|
||||
|
||||
@@ -118,7 +118,6 @@ export const find: Find = async function find(
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
query,
|
||||
})
|
||||
|
||||
@@ -353,6 +353,9 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['Point'],
|
||||
...(typeof field.defaultValue !== 'undefined' && {
|
||||
default: 'Point',
|
||||
}),
|
||||
},
|
||||
coordinates: {
|
||||
type: [Number],
|
||||
|
||||
@@ -115,7 +115,6 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
projection,
|
||||
query: versionQuery,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { PipelineStage } from 'mongoose'
|
||||
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
||||
|
||||
import { combineQueries } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { buildSortParam } from '../queries/buildSortParam.js'
|
||||
@@ -62,6 +64,10 @@ export const buildJoinAggregation = async ({
|
||||
continue
|
||||
}
|
||||
|
||||
if (joins?.[join.schemaPath] === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
const {
|
||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -49,9 +49,9 @@
|
||||
"dependencies": {
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.26.2",
|
||||
"drizzle-orm": "0.35.1",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
|
||||
@@ -100,6 +100,8 @@ export const connect: Connect = async function connect(
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await this.createExtensions()
|
||||
|
||||
// Only push schema if not in production
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
convertPathToJSONTraversal,
|
||||
countDistinct,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
defaultDrizzleSnapshot,
|
||||
@@ -75,15 +76,22 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
adapterSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
const extensions = (args.extensions ?? []).reduce((acc, name) => {
|
||||
acc[name] = true
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return createDatabaseAdapter<PostgresAdapter>({
|
||||
name: 'postgres',
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
defaultDrizzleSnapshot,
|
||||
disableCreateDatabase: args.disableCreateDatabase ?? false,
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
extensions,
|
||||
features: {
|
||||
json: true,
|
||||
},
|
||||
@@ -106,6 +114,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
sessions: {},
|
||||
tableNameMap: new Map<string, string>(),
|
||||
tables: {},
|
||||
tablesFilter: args.tablesFilter,
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export type Args = {
|
||||
* @default false
|
||||
*/
|
||||
disableCreateDatabase?: boolean
|
||||
extensions?: string[]
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
@@ -46,6 +47,7 @@ export type Args = {
|
||||
* @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle.
|
||||
*/
|
||||
schemaName?: string
|
||||
tablesFilter?: string[]
|
||||
transactionOptions?: false | PgTransactionConfig
|
||||
versionsSuffix?: string
|
||||
}
|
||||
@@ -60,10 +62,12 @@ declare module 'payload' {
|
||||
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
|
||||
DrizzleAdapter {
|
||||
afterSchemaInit: PostgresSchemaHook[]
|
||||
|
||||
beforeSchemaInit: PostgresSchemaHook[]
|
||||
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
|
||||
drizzle: PostgresDB
|
||||
enums: Record<string, GenericEnum>
|
||||
extensions: Record<string, boolean>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
@@ -88,6 +92,7 @@ declare module 'payload' {
|
||||
schema: Record<string, unknown>
|
||||
schemaName?: Args['schemaName']
|
||||
tableNameMap: Map<string, string>
|
||||
tablesFilter?: string[]
|
||||
versionsSuffix?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -47,9 +47,9 @@
|
||||
"dependencies": {
|
||||
"@libsql/client": "0.14.0",
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.26.2",
|
||||
"drizzle-orm": "0.35.1",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -49,9 +49,9 @@
|
||||
"dependencies": {
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
"@vercel/postgres": "^0.9.0",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.26.2",
|
||||
"drizzle-orm": "0.35.1",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
|
||||
@@ -64,6 +64,8 @@ export const connect: Connect = async function connect(
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await this.createExtensions()
|
||||
|
||||
// Only push schema if not in production
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
convertPathToJSONTraversal,
|
||||
countDistinct,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
defaultDrizzleSnapshot,
|
||||
@@ -75,15 +76,22 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
adapterSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
const extensions = (args.extensions ?? []).reduce((acc, name) => {
|
||||
acc[name] = true
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return createDatabaseAdapter<VercelPostgresAdapter>({
|
||||
name: 'postgres',
|
||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
defaultDrizzleSnapshot,
|
||||
disableCreateDatabase: args.disableCreateDatabase ?? false,
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
extensions,
|
||||
features: {
|
||||
json: true,
|
||||
},
|
||||
@@ -107,6 +115,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
sessions: {},
|
||||
tableNameMap: new Map<string, string>(),
|
||||
tables: {},
|
||||
tablesFilter: args.tablesFilter,
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export type Args = {
|
||||
* @default false
|
||||
*/
|
||||
disableCreateDatabase?: boolean
|
||||
extensions?: string[]
|
||||
idType?: 'serial' | 'uuid'
|
||||
localesSuffix?: string
|
||||
logger?: DrizzleConfig['logger']
|
||||
@@ -51,6 +52,7 @@ export type Args = {
|
||||
* @experimental This only works when there are not other tables or enums of the same name in the database under a different schema. Awaiting fix from Drizzle.
|
||||
*/
|
||||
schemaName?: string
|
||||
tablesFilter?: string[]
|
||||
transactionOptions?: false | PgTransactionConfig
|
||||
versionsSuffix?: string
|
||||
}
|
||||
@@ -69,6 +71,8 @@ declare module 'payload' {
|
||||
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
|
||||
drizzle: PostgresDB
|
||||
enums: Record<string, GenericEnum>
|
||||
extensions: Record<string, boolean>
|
||||
extensionsFilter: Set<string>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
@@ -93,6 +97,7 @@ declare module 'payload' {
|
||||
schema: Record<string, unknown>
|
||||
schemaName?: Args['schemaName']
|
||||
tableNameMap: Map<string, string>
|
||||
tablesFilter?: string[]
|
||||
versionsSuffix?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -45,8 +45,8 @@
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-orm": "0.35.1",
|
||||
"console-table-printer": "2.12.1",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
"uuid": "9.0.0"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { countDistinct } from '../postgres/countDistinct.js'
|
||||
export { createDatabase } from '../postgres/createDatabase.js'
|
||||
export { createExtensions } from '../postgres/createExtensions.js'
|
||||
export { convertPathToJSONTraversal } from '../postgres/createJSONQuery/convertPathToJSONTraversal.js'
|
||||
export { createJSONQuery } from '../postgres/createJSONQuery/index.js'
|
||||
export { createMigration } from '../postgres/createMigration.js'
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { Field, JoinQuery, SelectMode, SelectType, TabAsField } from 'payload'
|
||||
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { combineQueries } from 'payload'
|
||||
import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
@@ -389,6 +390,46 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
if (adapter.name === 'sqlite') {
|
||||
break
|
||||
}
|
||||
|
||||
const args = field.localized ? _locales : currentArgs
|
||||
if (!args.columns) {
|
||||
args.columns = {}
|
||||
}
|
||||
|
||||
if (!args.extras) {
|
||||
args.extras = {}
|
||||
}
|
||||
|
||||
const name = `${path}${field.name}`
|
||||
|
||||
// Drizzle handles that poorly. See https://github.com/drizzle-team/drizzle-orm/issues/2526
|
||||
// Additionally, this way we format the column value straight in the database using ST_AsGeoJSON
|
||||
args.columns[name] = false
|
||||
|
||||
let shouldSelect = false
|
||||
|
||||
if (select || selectAllOnCurrentLevel) {
|
||||
if (
|
||||
selectAllOnCurrentLevel ||
|
||||
(selectMode === 'include' && select[field.name] === true) ||
|
||||
(selectMode === 'exclude' && typeof select[field.name] === 'undefined')
|
||||
) {
|
||||
shouldSelect = true
|
||||
}
|
||||
} else {
|
||||
shouldSelect = true
|
||||
}
|
||||
|
||||
if (shouldSelect) {
|
||||
args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'join': {
|
||||
// when `joinsQuery` is false, do not join
|
||||
if (joinQuery === false) {
|
||||
@@ -402,11 +443,17 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
const joinSchemaPath = `${path.replaceAll('_', '.')}${field.name}`
|
||||
|
||||
if (joinQuery[joinSchemaPath] === false) {
|
||||
break
|
||||
}
|
||||
|
||||
const {
|
||||
limit: limitArg = field.defaultLimit ?? 10,
|
||||
sort = field.defaultSort,
|
||||
where,
|
||||
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
|
||||
} = joinQuery[joinSchemaPath] || {}
|
||||
let limit = limitArg
|
||||
|
||||
if (limit !== 0) {
|
||||
|
||||
@@ -20,6 +20,10 @@ export const migrate: DrizzleAdapter['migrate'] = async function migrate(
|
||||
return
|
||||
}
|
||||
|
||||
if ('createExtensions' in this && typeof this.createExtensions === 'function') {
|
||||
await this.createExtensions()
|
||||
}
|
||||
|
||||
let latestBatch = 0
|
||||
let migrationsInDB = []
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { PayloadRequest } from 'payload'
|
||||
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { DrizzleAdapter, Migration } from './types.js'
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { parseError } from './utilities/parseError.js'
|
||||
|
||||
@@ -48,6 +48,11 @@ export async function migrateFresh(
|
||||
})
|
||||
|
||||
const req = { payload } as PayloadRequest
|
||||
|
||||
if ('createExtensions' in this && typeof this.createExtensions === 'function') {
|
||||
await this.createExtensions()
|
||||
}
|
||||
|
||||
// Run all migrate up
|
||||
for (const migration of migrationFiles) {
|
||||
payload.logger.info({ msg: `Migrating: ${migration.name}` })
|
||||
|
||||
@@ -61,7 +61,7 @@ export const createDatabase = async function (this: BasePostgresAdapter, args: A
|
||||
|
||||
try {
|
||||
await managementClient.connect()
|
||||
await managementClient.query(`CREATE DATABASE ${dbName}`)
|
||||
await managementClient.query(`CREATE DATABASE "${dbName}"`)
|
||||
|
||||
this.payload.logger.info(`Created database "${dbName}"`)
|
||||
|
||||
|
||||
13
packages/drizzle/src/postgres/createExtensions.ts
Normal file
13
packages/drizzle/src/postgres/createExtensions.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { BasePostgresAdapter } from './types.js'
|
||||
|
||||
export const createExtensions = async function (this: BasePostgresAdapter): Promise<void> {
|
||||
for (const extension in this.extensions) {
|
||||
if (this.extensions[extension]) {
|
||||
try {
|
||||
await this.drizzle.execute(`CREATE EXTENSION IF NOT EXISTS "${extension}"`)
|
||||
} catch (err) {
|
||||
this.payload.logger.error({ err, msg: `Failed to create extension ${extension}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ export const defaultDrizzleSnapshot: DrizzleSnapshotJSON = {
|
||||
},
|
||||
dialect: 'postgresql',
|
||||
enums: {},
|
||||
policies: {},
|
||||
prevId: '00000000-0000-0000-0000-00000000000',
|
||||
roles: {},
|
||||
schemas: {},
|
||||
sequences: {},
|
||||
tables: {},
|
||||
|
||||
20
packages/drizzle/src/postgres/schema/geometryColumn.ts
Normal file
20
packages/drizzle/src/postgres/schema/geometryColumn.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Uses custom one instead of geometry() from drizzle-orm/pg-core because it's broken on pushDevSchema
|
||||
// Why?
|
||||
// It tries to give us a prompt "you're about to change.. from geometry(Point) to geometry(point)"
|
||||
import { customType } from 'drizzle-orm/pg-core'
|
||||
import { parseEWKB } from 'drizzle-orm/pg-core/columns/postgis_extension/utils'
|
||||
|
||||
type Point = [number, number]
|
||||
|
||||
export const geometryColumn = (name: string) =>
|
||||
customType<{ data: Point; driverData: string }>({
|
||||
dataType() {
|
||||
return `geometry(Point)`
|
||||
},
|
||||
fromDriver(value: string) {
|
||||
return parseEWKB(value)
|
||||
},
|
||||
toDriver(value: Point) {
|
||||
return `SRID=4326;point(${value[0]} ${value[1]})`
|
||||
},
|
||||
})(name)
|
||||
@@ -6,6 +6,7 @@ import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
boolean,
|
||||
foreignKey,
|
||||
geometry,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
@@ -35,6 +36,7 @@ import { hasLocalesTable } from '../../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { buildTable } from './build.js'
|
||||
import { createIndex } from './createIndex.js'
|
||||
import { geometryColumn } from './geometryColumn.js'
|
||||
import { idToUUID } from './idToUUID.js'
|
||||
import { parentIDColumnMap } from './parentIDColumnMap.js'
|
||||
import { withDefault } from './withDefault.js'
|
||||
@@ -156,7 +158,7 @@ export const traverseFields = ({
|
||||
|
||||
if (
|
||||
(field.unique || field.index || ['relationship', 'upload'].includes(field.type)) &&
|
||||
!['array', 'blocks', 'group', 'point'].includes(field.type) &&
|
||||
!['array', 'blocks', 'group'].includes(field.type) &&
|
||||
!('hasMany' in field && field.hasMany === true) &&
|
||||
!('relationTo' in field && Array.isArray(field.relationTo))
|
||||
) {
|
||||
@@ -261,6 +263,10 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
targetTable[fieldName] = withDefault(geometryColumn(columnName), field)
|
||||
if (!adapter.extensions.postgis) {
|
||||
adapter.extensions.postgis = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -14,5 +14,9 @@ export const withDefault = (
|
||||
return column.default(escapedString)
|
||||
}
|
||||
|
||||
if (field.type === 'point' && Array.isArray(field.defaultValue)) {
|
||||
return column.default(`SRID=4326;POINT(${field.defaultValue[0]} ${field.defaultValue[1]})`)
|
||||
}
|
||||
|
||||
return column.default(field.defaultValue)
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ export type BasePostgresAdapter = {
|
||||
beforeSchemaInit: PostgresSchemaHook[]
|
||||
countDistinct: CountDistinct
|
||||
createDatabase: CreateDatabase
|
||||
createExtensions: () => Promise<void>
|
||||
defaultDrizzleSnapshot: DrizzleSnapshotJSON
|
||||
deleteWhere: DeleteWhere
|
||||
disableCreateDatabase: boolean
|
||||
@@ -138,6 +139,7 @@ export type BasePostgresAdapter = {
|
||||
dropDatabase: DropDatabase
|
||||
enums: Record<string, GenericEnum>
|
||||
execute: Execute<unknown>
|
||||
extensions: Record<string, boolean>
|
||||
/**
|
||||
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
|
||||
* Used for returning properly formed errors from unique fields
|
||||
@@ -171,6 +173,7 @@ export type BasePostgresAdapter = {
|
||||
}
|
||||
tableNameMap: Map<string, string>
|
||||
tables: Record<string, GenericTable>
|
||||
tablesFilter?: string[]
|
||||
versionsSuffix?: string
|
||||
} & PostgresDrizzleAdapter
|
||||
|
||||
|
||||
@@ -48,10 +48,6 @@ export const operatorMap: Operators = {
|
||||
less_than_equal: lte,
|
||||
like: ilike,
|
||||
not_equals: ne,
|
||||
// TODO: geojson queries
|
||||
// intersects: intersects,
|
||||
// near: near,
|
||||
// within: within,
|
||||
// all: all,
|
||||
not_in: notInArray,
|
||||
or,
|
||||
|
||||
@@ -285,6 +285,39 @@ export function parseParams({
|
||||
break
|
||||
}
|
||||
|
||||
if (field.type === 'point' && adapter.name === 'postgres') {
|
||||
switch (operator) {
|
||||
case 'near': {
|
||||
const [lng, lat, maxDistance, minDistance] = queryValue as number[]
|
||||
|
||||
let constraint = sql`ST_DWithin(ST_Transform(${table[columnName]}, 3857), ST_Transform(ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326), 3857), ${maxDistance})`
|
||||
if (typeof minDistance === 'number' && !Number.isNaN(minDistance)) {
|
||||
constraint = sql`${constraint} AND ST_Distance(ST_Transform(${table[columnName]}, 3857), ST_Transform(ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326), 3857)) >= ${minDistance}`
|
||||
}
|
||||
constraints.push(constraint)
|
||||
break
|
||||
}
|
||||
|
||||
case 'within': {
|
||||
constraints.push(
|
||||
sql`ST_Within(${table[columnName]}, ST_GeomFromGeoJSON(${JSON.stringify(queryValue)}))`,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'intersects': {
|
||||
constraints.push(
|
||||
sql`ST_Intersects(${table[columnName]}, ST_GeomFromGeoJSON(${JSON.stringify(queryValue)}))`,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
constraints.push(
|
||||
adapter.operators[queryOperator](rawColumn || table[columnName], queryValue),
|
||||
)
|
||||
|
||||
@@ -212,10 +212,10 @@ export const sanitizeQueryValue = ({
|
||||
operator = 'equals'
|
||||
}
|
||||
|
||||
if (operator === 'near' || operator === 'within' || operator === 'intersects') {
|
||||
throw new APIError(
|
||||
`Querying with '${operator}' is not supported with the postgres database adapter.`,
|
||||
)
|
||||
if (operator === 'near' && field.type === 'point' && typeof formattedValue === 'string') {
|
||||
const [lng, lat, maxDistance, minDistance] = formattedValue.split(',')
|
||||
|
||||
formattedValue = [Number(lng), Number(lat), Number(maxDistance), Number(minDistance)]
|
||||
}
|
||||
|
||||
if (operator === 'contains') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
@@ -591,6 +592,9 @@ export const traverseFields = ({
|
||||
valuesToTransform.forEach(({ localeKey, ref, value }) => {
|
||||
if (typeof value !== 'undefined') {
|
||||
let formattedValue = value
|
||||
if (value && field.type === 'point' && adapter.name !== 'sqlite') {
|
||||
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
|
||||
}
|
||||
|
||||
if (field.type === 'date') {
|
||||
if (typeof value === 'number' && !Number.isNaN(value)) {
|
||||
|
||||
@@ -121,6 +121,8 @@ export type RequireDrizzleKit = () => {
|
||||
schema: Record<string, unknown>,
|
||||
drizzle: DrizzleAdapter['drizzle'],
|
||||
filterSchema?: string[],
|
||||
tablesFilter?: string[],
|
||||
extensionsFilter?: string[],
|
||||
) => Promise<{ apply; hasDataLoss; warnings }>
|
||||
}
|
||||
|
||||
@@ -164,6 +166,7 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
||||
dropDatabase: DropDatabase
|
||||
enums?: never | Record<string, unknown>
|
||||
execute: Execute<unknown>
|
||||
|
||||
features: {
|
||||
json?: boolean
|
||||
}
|
||||
@@ -187,6 +190,7 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
||||
requireDrizzleKit: RequireDrizzleKit
|
||||
resolveInitializing: () => void
|
||||
schema: Record<string, unknown>
|
||||
|
||||
schemaName?: string
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { BasePostgresAdapter } from '../postgres/types.js'
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
|
||||
/**
|
||||
@@ -11,11 +12,17 @@ import type { DrizzleAdapter } from '../types.js'
|
||||
export const pushDevSchema = async (adapter: DrizzleAdapter) => {
|
||||
const { pushSchema } = adapter.requireDrizzleKit()
|
||||
|
||||
const { extensions = {}, tablesFilter } = adapter as BasePostgresAdapter
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, warnings } = await pushSchema(
|
||||
adapter.schema,
|
||||
adapter.drizzle,
|
||||
adapter.schemaName ? [adapter.schemaName] : undefined,
|
||||
tablesFilter,
|
||||
// Drizzle extensionsFilter supports only postgis for now
|
||||
// https://github.com/drizzle-team/drizzle-orm/blob/83daf2d5cf023112de878bc2249ee2c41a2a5b1b/drizzle-kit/src/cli/validations/cli.ts#L26
|
||||
extensions.postgis ? ['postgis'] : undefined,
|
||||
)
|
||||
|
||||
if (warnings.length) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -45,8 +45,8 @@
|
||||
"dependencies": {
|
||||
"graphql-scalars": "1.22.2",
|
||||
"pluralize": "8.0.0",
|
||||
"ts-essentials": "10.0.2",
|
||||
"tsx": "4.19.1"
|
||||
"ts-essentials": "10.0.3",
|
||||
"tsx": "4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.123",
|
||||
"version": "3.0.0-beta.127",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -67,6 +67,7 @@ export const DocumentTabLink: React.FC<{
|
||||
<Link
|
||||
className={`${baseClass}__link`}
|
||||
href={!isActive || href !== pathname ? hrefWithLocale : ''}
|
||||
prefetch={false}
|
||||
{...(newTab && { rel: 'noopener noreferrer', target: '_blank' })}
|
||||
tabIndex={isActive ? -1 : 0}
|
||||
>
|
||||
|
||||
@@ -98,6 +98,7 @@ export const DefaultNavClient: React.FC = () => {
|
||||
href={href}
|
||||
id={id}
|
||||
key={i}
|
||||
prefetch={Link ? false : undefined}
|
||||
tabIndex={!navOpen ? -1 : undefined}
|
||||
>
|
||||
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
|
||||
|
||||
@@ -6,4 +6,5 @@ export {
|
||||
OPTIONS as REST_OPTIONS,
|
||||
PATCH as REST_PATCH,
|
||||
POST as REST_POST,
|
||||
PUT as REST_PUT,
|
||||
} from '../routes/rest/index.js'
|
||||
|
||||
@@ -6,6 +6,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const create: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
@@ -20,6 +21,7 @@ export const create: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
data: req.data,
|
||||
depth: isNumber(depth) ? depth : undefined,
|
||||
draft,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
@@ -8,12 +8,14 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, overrideLock, select, where } = req.query as {
|
||||
const { depth, overrideLock, populate, select, where } = req.query as {
|
||||
depth?: string
|
||||
overrideLock?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
where?: Where
|
||||
}
|
||||
@@ -22,6 +24,7 @@ export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) =>
|
||||
collection,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
populate: sanitizePopulate(populate),
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
where,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const deleteByID: CollectionRouteHandlerWithID = async ({
|
||||
@@ -28,6 +29,7 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({
|
||||
collection,
|
||||
depth: isNumber(depth) ? depth : undefined,
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const duplicate: CollectionRouteHandlerWithID = async ({
|
||||
@@ -30,6 +31,7 @@ export const duplicate: CollectionRouteHandlerWithID = async ({
|
||||
collection,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
@@ -8,15 +8,17 @@ import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, draft, joins, limit, page, select, sort, where } = req.query as {
|
||||
const { depth, draft, joins, limit, page, populate, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
draft?: string
|
||||
joins?: JoinQuery
|
||||
limit?: string
|
||||
page?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
@@ -29,6 +31,7 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
joins: sanitizeJoinParams(joins),
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
populate: sanitizePopulate(populate),
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findByID: CollectionRouteHandlerWithID = async ({
|
||||
@@ -31,6 +32,7 @@ export const findByID: CollectionRouteHandlerWithID = async ({
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: searchParams.get('draft') === 'true',
|
||||
joins: sanitizeJoinParams(req.query.joins as JoinQuery),
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersionByID: CollectionRouteHandlerWithID = async ({
|
||||
@@ -26,6 +27,7 @@ export const findVersionByID: CollectionRouteHandlerWithID = async ({
|
||||
id,
|
||||
collection,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
@@ -7,13 +7,15 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersions: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, limit, page, select, sort, where } = req.query as {
|
||||
const { depth, limit, page, populate, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
limit?: string
|
||||
page?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
@@ -24,6 +26,7 @@ export const findVersions: CollectionRouteHandler = async ({ collection, req })
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
populate: sanitizePopulate(populate),
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
|
||||
export const restoreVersion: CollectionRouteHandlerWithID = async ({
|
||||
id: incomingID,
|
||||
@@ -27,6 +28,7 @@ export const restoreVersion: CollectionRouteHandlerWithID = async ({
|
||||
collection,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: draft === 'true' ? true : undefined,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ import { isNumber } from 'payload/shared'
|
||||
import type { CollectionRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const update: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
const { depth, draft, limit, overrideLock, select, where } = req.query as {
|
||||
const { depth, draft, limit, overrideLock, populate, select, where } = req.query as {
|
||||
depth?: string
|
||||
draft?: string
|
||||
limit?: string
|
||||
overrideLock?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
where?: Where
|
||||
}
|
||||
@@ -27,6 +29,7 @@ export const update: CollectionRouteHandler = async ({ collection, req }) => {
|
||||
draft: draft === 'true',
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
populate: sanitizePopulate(populate),
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
where,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const updateByID: CollectionRouteHandlerWithID = async ({
|
||||
@@ -34,6 +35,7 @@ export const updateByID: CollectionRouteHandlerWithID = async ({
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft,
|
||||
overrideLock: Boolean(overrideLock === 'true'),
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
publishSpecificLocale,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
@@ -16,6 +17,7 @@ export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: searchParams.get('draft') === 'true',
|
||||
globalConfig,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConfig, req }) => {
|
||||
@@ -15,6 +16,7 @@ export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConf
|
||||
id,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
globalConfig,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
})
|
||||
|
||||
@@ -7,13 +7,15 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
const { depth, limit, page, select, sort, where } = req.query as {
|
||||
const { depth, limit, page, populate, select, sort, where } = req.query as {
|
||||
depth?: string
|
||||
limit?: string
|
||||
page?: string
|
||||
populate?: Record<string, unknown>
|
||||
select?: Record<string, unknown>
|
||||
sort?: string
|
||||
where?: Where
|
||||
@@ -24,6 +26,7 @@ export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) =>
|
||||
globalConfig,
|
||||
limit: isNumber(limit) ? Number(limit) : undefined,
|
||||
page: isNumber(page) ? Number(page) : undefined,
|
||||
populate: sanitizePopulate(populate),
|
||||
req,
|
||||
select: sanitizeSelect(select),
|
||||
sort: typeof sort === 'string' ? sort.split(',') : undefined,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandlerWithID } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
|
||||
export const restoreVersion: GlobalRouteHandlerWithID = async ({ id, globalConfig, req }) => {
|
||||
const { searchParams } = req
|
||||
@@ -16,6 +17,7 @@ export const restoreVersion: GlobalRouteHandlerWithID = async ({ id, globalConfi
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft: draft === 'true' ? true : undefined,
|
||||
globalConfig,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
req,
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
|
||||
import type { GlobalRouteHandler } from '../types.js'
|
||||
|
||||
import { headersWithCors } from '../../../utilities/headersWithCors.js'
|
||||
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
|
||||
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
|
||||
|
||||
export const update: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
@@ -21,6 +22,7 @@ export const update: GlobalRouteHandler = async ({ globalConfig, req }) => {
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft,
|
||||
globalConfig,
|
||||
populate: sanitizePopulate(req.query.populate),
|
||||
publishSpecificLocale,
|
||||
req,
|
||||
select: sanitizeSelect(req.query.select),
|
||||
|
||||
@@ -821,3 +821,87 @@ export const PATCH =
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const PUT =
|
||||
(config: Promise<SanitizedConfig> | SanitizedConfig) =>
|
||||
async (request: Request, { params: paramsPromise }: { params: Promise<{ slug: string[] }> }) => {
|
||||
const { slug } = await paramsPromise
|
||||
const [slug1] = slug
|
||||
let req: PayloadRequest
|
||||
let res: Response
|
||||
let collection: Collection
|
||||
|
||||
try {
|
||||
req = await createPayloadRequest({
|
||||
config,
|
||||
request,
|
||||
})
|
||||
collection = req.payload.collections?.[slug1]
|
||||
|
||||
const disableEndpoints = endpointsAreDisabled({
|
||||
endpoints: req.payload.config.endpoints,
|
||||
request,
|
||||
})
|
||||
if (disableEndpoints) {
|
||||
return disableEndpoints
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
req.routeParams.collection = slug1
|
||||
|
||||
const disableEndpoints = endpointsAreDisabled({
|
||||
endpoints: collection.config.endpoints,
|
||||
request,
|
||||
})
|
||||
if (disableEndpoints) {
|
||||
return disableEndpoints
|
||||
}
|
||||
|
||||
const customEndpointResponse = await handleCustomEndpoints({
|
||||
endpoints: collection.config.endpoints,
|
||||
entitySlug: slug1,
|
||||
req,
|
||||
})
|
||||
|
||||
if (customEndpointResponse) {
|
||||
return customEndpointResponse
|
||||
}
|
||||
}
|
||||
|
||||
if (res instanceof Response) {
|
||||
if (req.responseHeaders) {
|
||||
const mergedResponse = new Response(res.body, {
|
||||
headers: mergeHeaders(req.responseHeaders, res.headers),
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
})
|
||||
|
||||
return mergedResponse
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// root routes
|
||||
const customEndpointResponse = await handleCustomEndpoints({
|
||||
endpoints: req.payload.config.endpoints,
|
||||
req,
|
||||
})
|
||||
|
||||
if (customEndpointResponse) {
|
||||
return customEndpointResponse
|
||||
}
|
||||
|
||||
return RouteNotFoundResponse({
|
||||
slug,
|
||||
req,
|
||||
})
|
||||
} catch (error) {
|
||||
return routeError({
|
||||
collection,
|
||||
config,
|
||||
err: error,
|
||||
req: req || request,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
||||
// Or better yet, use a CDN like Google Fonts if ever supported
|
||||
fontData = fs.readFile(path.join(dirname, 'roboto-regular.woff'))
|
||||
} catch (e) {
|
||||
console.error(`Error reading font file or not readable: ${e.message}`) // eslint-disable-line no-console
|
||||
req.payload.logger.error(`Error reading font file or not readable: ${e.message}`)
|
||||
}
|
||||
|
||||
const fontFamily = 'Roboto, sans-serif'
|
||||
@@ -86,7 +86,7 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
||||
},
|
||||
)
|
||||
} catch (e: any) {
|
||||
console.error(`${e.message}`) // eslint-disable-line no-console
|
||||
req.payload.logger.error(`Error generating Open Graph image: ${e.message}`)
|
||||
return NextResponse.json({ error: `Internal Server Error: ${e.message}` }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,27 @@ import { isNumber } from 'payload/shared'
|
||||
export const sanitizeJoinParams = (
|
||||
joins:
|
||||
| {
|
||||
[schemaPath: string]: {
|
||||
limit?: unknown
|
||||
sort?: string
|
||||
where?: unknown
|
||||
}
|
||||
[schemaPath: string]:
|
||||
| {
|
||||
limit?: unknown
|
||||
sort?: string
|
||||
where?: unknown
|
||||
}
|
||||
| false
|
||||
}
|
||||
| false = {},
|
||||
): JoinQuery => {
|
||||
const joinQuery = {}
|
||||
|
||||
Object.keys(joins).forEach((schemaPath) => {
|
||||
joinQuery[schemaPath] = {
|
||||
limit: isNumber(joins[schemaPath]?.limit) ? Number(joins[schemaPath].limit) : undefined,
|
||||
sort: joins[schemaPath]?.sort ? joins[schemaPath].sort : undefined,
|
||||
where: joins[schemaPath]?.where ? joins[schemaPath].where : undefined,
|
||||
if (joins[schemaPath] === 'false' || joins[schemaPath] === false) {
|
||||
joinQuery[schemaPath] = false
|
||||
} else {
|
||||
joinQuery[schemaPath] = {
|
||||
limit: isNumber(joins[schemaPath]?.limit) ? Number(joins[schemaPath].limit) : undefined,
|
||||
sort: joins[schemaPath]?.sort ? joins[schemaPath].sort : undefined,
|
||||
where: joins[schemaPath]?.where ? joins[schemaPath].where : undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
15
packages/next/src/routes/rest/utilities/sanitizePopulate.ts
Normal file
15
packages/next/src/routes/rest/utilities/sanitizePopulate.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { PopulateType } from 'payload'
|
||||
|
||||
import { sanitizeSelect } from './sanitizeSelect.js'
|
||||
|
||||
export const sanitizePopulate = (unsanitizedPopulate: unknown): PopulateType | undefined => {
|
||||
if (!unsanitizedPopulate || typeof unsanitizedPopulate !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
for (const k in unsanitizedPopulate) {
|
||||
unsanitizedPopulate[k] = sanitizeSelect(unsanitizedPopulate[k])
|
||||
}
|
||||
|
||||
return unsanitizedPopulate as PopulateType
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user