Compare commits
115 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6baff8a3ba | ||
|
|
ced79be591 | ||
|
|
5b9cee67c0 | ||
|
|
bcbca0e44a | ||
|
|
cd95daf029 | ||
|
|
9da85430a5 | ||
|
|
f4d526d6e5 | ||
|
|
3b55458c0d | ||
|
|
51dc3f06b1 | ||
|
|
d6282221db | ||
|
|
f264c8087a | ||
|
|
1b16730b20 | ||
|
|
f6bdc0aaf6 | ||
|
|
a8e3095e45 | ||
|
|
5ac4e73991 | ||
|
|
9ee6425761 | ||
|
|
8c2fc71149 | ||
|
|
88bef2e140 | ||
|
|
a1c99c8b45 | ||
|
|
d3cd9baa9b | ||
|
|
64967e4ca6 | ||
|
|
e0309a1dd0 | ||
|
|
6bb4067bb3 | ||
|
|
a3ebf51d6e | ||
|
|
280448dd02 | ||
|
|
09c41d5c86 | ||
|
|
def595e645 | ||
|
|
8dd7e989ef | ||
|
|
7619592fb6 | ||
|
|
432741bca3 | ||
|
|
97cffa51f8 | ||
|
|
7cd805adb9 | ||
|
|
48d0faecae | ||
|
|
4f6651433c | ||
|
|
8a67098f6c | ||
|
|
03291472d6 | ||
|
|
3298113a93 | ||
|
|
b878daf27a | ||
|
|
23907e432e | ||
|
|
a30eeaf644 | ||
|
|
df764dbbef | ||
|
|
6899a3cc27 | ||
|
|
7261faac57 | ||
|
|
7767c94bd8 | ||
|
|
2ad991759f | ||
|
|
9c559d7304 | ||
|
|
d8391389ab | ||
|
|
570c610eed | ||
|
|
9dbf1b7279 | ||
|
|
71db10d68f | ||
|
|
c96fa613bc | ||
|
|
3e954f45c7 | ||
|
|
9a970d21a9 | ||
|
|
8a20231d40 | ||
|
|
26691377d2 | ||
|
|
8201a6cacd | ||
|
|
d7fc944792 | ||
|
|
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 |
@@ -28,3 +28,6 @@ fb7d1be2f3325d076b7c967b1730afcef37922c2
|
||||
|
||||
# Prettier and lint remaining db packages
|
||||
7fd736ea5b2e9fc4ef936e9dc9e5e3d722f6d8bf
|
||||
|
||||
# Bump all eslint deps, lint and format
|
||||
03291472d6e427ff94e61fca0616cca7796a3a95
|
||||
|
||||
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}}
|
||||
|
||||
58
.github/actions/triage/dist/index.js
vendored
58
.github/actions/triage/dist/index.js
vendored
@@ -33843,6 +33843,7 @@ if (!process.env.GITHUB_TOKEN)
|
||||
throw new TypeError('No GITHUB_TOKEN provided');
|
||||
if (!process.env.GITHUB_WORKSPACE)
|
||||
throw new TypeError('Not a GitHub workspace');
|
||||
var validActionsToPerform = ['tag', 'comment', 'close'];
|
||||
var config = {
|
||||
invalidLink: {
|
||||
comment: (0,core.getInput)('reproduction_comment') || '.github/invalid-reproduction.md',
|
||||
@@ -33853,7 +33854,15 @@ var config = {
|
||||
label: (0,core.getInput)('reproduction_invalid_label') || 'invalid-reproduction',
|
||||
linkSection: (0,core.getInput)('reproduction_link_section') || '### Link to reproduction(.*)### To reproduce',
|
||||
},
|
||||
tagOnly: getBooleanOrUndefined('tag_only') || false,
|
||||
actionsToPerform: ((0,core.getInput)('actions_to_perform') || validActionsToPerform.join(','))
|
||||
.split(',')
|
||||
.map(function (a) {
|
||||
var action = a.trim().toLowerCase();
|
||||
if (validActionsToPerform.includes(action)) {
|
||||
return action;
|
||||
}
|
||||
throw new TypeError("Invalid action: ".concat(action));
|
||||
}),
|
||||
token: process.env.GITHUB_TOKEN,
|
||||
workspace: process.env.GITHUB_WORKSPACE,
|
||||
};
|
||||
@@ -33870,7 +33879,7 @@ function tryParse(json) {
|
||||
// Retrieves a boolean input or undefined based on environment variables
|
||||
function getBooleanOrUndefined(value) {
|
||||
var variable = process.env["INPUT_".concat(value.toUpperCase())];
|
||||
return variable === undefined || variable === '' ? undefined : (0,core.getBooleanInput)(value);
|
||||
return variable === undefined || variable === '' ? undefined : getBooleanInput(value);
|
||||
}
|
||||
// Returns the appropriate label match type
|
||||
function getLabelMatch(value) {
|
||||
@@ -33910,36 +33919,47 @@ function checkValidReproduction() {
|
||||
case 3:
|
||||
(0,core.info)("Invalid reproduction, issue will be closed/labeled/commented...");
|
||||
// Adjust labels
|
||||
return [4 /*yield*/, Promise.all(labelsToRemove.map(function (label) { return client.issues.removeLabel(__assign(__assign({}, common), { name: label })); }))];
|
||||
return [4 /*yield*/, Promise.all(labelsToRemove.map(function (label) { return client.issues.removeLabel(__assign(__assign({}, common), { name: label })); }))
|
||||
// Tag
|
||||
];
|
||||
case 4:
|
||||
// Adjust labels
|
||||
_f.sent();
|
||||
(0,core.info)("Issue #".concat(issue.number, " - validate label removed"));
|
||||
if (!!config.actionsToPerform.includes('tag')) return [3 /*break*/, 6];
|
||||
(0,core.info)("Added label: ".concat(config.invalidLink.label));
|
||||
return [4 /*yield*/, client.issues.addLabels(__assign(__assign({}, common), { labels: [config.invalidLink.label] }))];
|
||||
case 5:
|
||||
_f.sent();
|
||||
(0,core.info)("Issue #".concat(issue.number, " - labeled"));
|
||||
// If tagOnly, do not close or comment
|
||||
if (config.tagOnly) {
|
||||
(0,core.info)('Tag-only enabled, no closing/commenting actions taken');
|
||||
return [2 /*return*/];
|
||||
}
|
||||
// Perform closing and commenting actions
|
||||
return [4 /*yield*/, client.issues.update(__assign(__assign({}, common), { state: 'closed' }))];
|
||||
return [3 /*break*/, 7];
|
||||
case 6:
|
||||
// Perform closing and commenting actions
|
||||
_f.sent();
|
||||
(0,core.info)("Issue #".concat(issue.number, " - closed"));
|
||||
(0,core.info)('Tag - skipped, not provided in actions to perform');
|
||||
_f.label = 7;
|
||||
case 7:
|
||||
if (!config.actionsToPerform.includes('comment')) return [3 /*break*/, 10];
|
||||
comment = (0,external_node_path_namespaceObject.join)(config.workspace, config.invalidLink.comment);
|
||||
_c = (_b = client.issues).createComment;
|
||||
_d = [__assign({}, common)];
|
||||
_e = {};
|
||||
return [4 /*yield*/, getCommentBody(comment)];
|
||||
case 7: return [4 /*yield*/, _c.apply(_b, [__assign.apply(void 0, _d.concat([(_e.body = _f.sent(), _e)]))])];
|
||||
case 8:
|
||||
case 8: return [4 /*yield*/, _c.apply(_b, [__assign.apply(void 0, _d.concat([(_e.body = _f.sent(), _e)]))])];
|
||||
case 9:
|
||||
_f.sent();
|
||||
(0,core.info)("Issue #".concat(issue.number, " - commented"));
|
||||
return [2 /*return*/];
|
||||
(0,core.info)("Commented with invalid reproduction message");
|
||||
return [3 /*break*/, 11];
|
||||
case 10:
|
||||
(0,core.info)('Comment - skipped, not provided in actions to perform');
|
||||
_f.label = 11;
|
||||
case 11:
|
||||
if (!config.actionsToPerform.includes('close')) return [3 /*break*/, 13];
|
||||
return [4 /*yield*/, client.issues.update(__assign(__assign({}, common), { state: 'closed' }))];
|
||||
case 12:
|
||||
_f.sent();
|
||||
(0,core.info)("Closed issue #".concat(issue.number));
|
||||
return [3 /*break*/, 14];
|
||||
case 13:
|
||||
(0,core.info)('Close - skipped, not provided in actions to perform');
|
||||
_f.label = 14;
|
||||
case 14: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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?
|
||||
|
||||
|
||||
157
.github/workflows/main.yml
vendored
157
.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()
|
||||
@@ -552,6 +525,7 @@ jobs:
|
||||
publish-canary:
|
||||
name: Publish Canary
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.all-green.result == 'success' && github.ref_name == 'beta' }}
|
||||
needs:
|
||||
- all-green
|
||||
|
||||
@@ -560,4 +534,3 @@ jobs:
|
||||
- run: |
|
||||
echo github.ref: ${{ github.ref }}
|
||||
echo isBeta: ${{ github.ref == 'refs/heads/beta' }}
|
||||
echo isMain: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
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'
|
||||
|
||||
76
README.md
76
README.md
@@ -1,4 +1,4 @@
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/beta/packages/payload/src/assets/images/github-banner-nextjs-native.jpg" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
@@ -7,80 +7,74 @@
|
||||
|
||||
<a href="https://discord.gg/payload"><img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" /></a>
|
||||
|
||||
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/dw/payload?style=flat-square" /></a>
|
||||
|
||||
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" /></a>
|
||||
|
||||
<a href="https://twitter.com/payloadcms"><img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" /></a>
|
||||
</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)
|
||||
|
||||
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.
|
||||
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a fully functional front-end built with RSCs and Tailwind.
|
||||
|
||||
### [🌐 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 +82,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,8 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
@@ -12,8 +14,17 @@ type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ The following options are available:
|
||||
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
|
||||
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
|
||||
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
|
||||
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#components). |
|
||||
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
|
||||
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
|
||||
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this Collection. |
|
||||
| **`enableRichTextLink`** | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
@@ -39,11 +39,12 @@ The following options are available:
|
||||
| **`meta`** | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](./metadata). |
|
||||
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](#preview). |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#components). |
|
||||
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
|
||||
| **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
|
||||
| **`pagination`** | Set pagination-specific options for this Collection. [More details](#pagination). |
|
||||
| **`baseListFilter`** | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
|
||||
|
||||
### Components
|
||||
### Custom Components
|
||||
|
||||
Collections can set their own [Custom Components](./components) which only apply to [Collection](../configuration/collections)-specific UI within the [Admin Panel](./overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Fully customize your Admin Panel by swapping in your own React components.
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Payload [Admin Panel](./overview) is designed to be as minimal and straightforward as possible to allow for both easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](../configuration/overview).
|
||||
The Payload [Admin Panel](./overview) is designed to be as minimal and straightforward as possible to allow for easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](../configuration/overview).
|
||||
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api/overview) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
|
||||
|
||||
@@ -18,51 +18,45 @@ All Custom Components in Payload are [React Server Components](https://react.dev
|
||||
There are four main types of Custom Components in Payload:
|
||||
|
||||
- [Root Components](#root-components)
|
||||
- [Collection Components](./collections#components)
|
||||
- [Global Components](./globals#components)
|
||||
- [Field Components](./fields)
|
||||
- [Collection Components](./collections#custom-components)
|
||||
- [Global Components](./globals#custom-components)
|
||||
- [Field Components](./fields#custom-components)
|
||||
|
||||
To swap in your own Custom Component, consult the list of available components. Determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-components) accordingly.
|
||||
To swap in your own Custom Component, first consult the list of available components, determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-components) accordingly.
|
||||
|
||||
## Defining Custom Components
|
||||
|
||||
## Defining Custom Components in the Payload Config
|
||||
As Payload compiles the Admin Panel, it checks your config for Custom Components. When detected, Payload either replaces its own default component with yours, or if none exists by default, renders yours outright. While are many places where Custom Components are supported in Payload, each is defined in the same way using [Component Paths](#component-paths).
|
||||
|
||||
In the Payload Config, you can define custom React Components to enhance the admin interface. However, these components should not be imported directly into the server-only Payload Config to avoid including client-side code. Instead, you specify the path to the component. Here’s how you can do it:
|
||||
To add a Custom Component, point to its file path in your Payload Config:
|
||||
|
||||
|
||||
src/components/Logout.tsx
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<button>Click me!</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
payload.config.ts:
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
admin: {
|
||||
components: {
|
||||
logout: {
|
||||
Button: '/src/components/Logout#MyComponent'
|
||||
Button: '/src/components/Logout#MyComponent' // highlight-line
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In the path `/src/components/Logout#MyComponent`, `/src/components/Logout` is the file path, and `MyComponent` is the named export. If the component is the default export, the export name can be omitted. Path and export name are separated by a `#`.
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
All Custom Components can be either Server Components or Client Components, depending on the presence of the `use client` directive at the top of the file.
|
||||
</Banner>
|
||||
|
||||
### Configuring the Base Directory
|
||||
### Component Paths
|
||||
|
||||
Component paths, by default, are relative to your working directory - this is usually where your Next.js config lies. To simplify component paths, you have the option to configure the *base directory* using the `admin.baseDir.baseDir` property:
|
||||
In order to ensure the Payload Config is fully Node.js compatible and as lightweight as possible, components are not directly imported into your config. Instead, they are identified by their file path for the Admin Panel to resolve on its own.
|
||||
|
||||
Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in `config.admin.baseDir`. To simplify Component Paths, you can also configure the base directory using the `admin.importMap.baseDir` property.
|
||||
|
||||
Components using named exports are identified either by appending `#` followed by the export name, or using the `exportName` property. If the component is the default export, this can be omitted.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -73,137 +67,72 @@ const dirname = path.dirname(filename)
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname, 'src'),
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname, 'src'), // highlight-line
|
||||
},
|
||||
components: {
|
||||
logout: {
|
||||
Button: '/components/Logout#MyComponent'
|
||||
Button: '/components/Logout#MyComponent' // highlight-line
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In this example, we set the base directory to the `src` directory - thus we can omit the `/src/` part of our component path string.
|
||||
In this example, we set the base directory to the `src` directory, and omit the `/src/` part of our component path string.
|
||||
|
||||
### Passing Props
|
||||
### Config Options
|
||||
|
||||
Each React Component in the Payload Config is typed as `PayloadComponent`. This usually is a string, but can also be an object containing the following properties:
|
||||
While Custom Components are usually defined as a string, you can also pass in an object with additional options:
|
||||
|
||||
| Property | Description |
|
||||
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `clientProps` | Props to be passed to the React Component if it's a Client Component |
|
||||
| `exportName` | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
|
||||
| `path` | Path to the React Component. Named exports can be appended to the end of the path, separated by a `#` |
|
||||
| `serverProps` | Props to be passed to the React Component if it's a Server Component |
|
||||
|
||||
To pass in props from the config, you can use the `clientProps` and/or `serverProps` properties. This alleviates the need to use an HOC (Higher-Order-Component) to declare a React Component with props passed in.
|
||||
|
||||
Here is an example:
|
||||
|
||||
src/components/Logout.tsx
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = ({ text }: { text: string }) => {
|
||||
return (
|
||||
<button>Click me! {text}</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
payload.config.ts:
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
admin: {
|
||||
components: {
|
||||
logout: {
|
||||
// highlight-start
|
||||
Button: {
|
||||
path: '/src/components/Logout',
|
||||
clientProps: {
|
||||
text: 'Some Text.'
|
||||
},
|
||||
exportName: 'MyComponent'
|
||||
exportName: 'MyComponent',
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Import Maps
|
||||
The following options are available:
|
||||
|
||||
It's essential to understand how `PayloadComponent` paths function behind the scenes. Directly importing React Components into your Payload Config using import statements can introduce client-only modules like CSS into your server-only config. This could error when attempting to load the Payload Config in server-only environments and unnecessarily increase the size of the Payload Config, which should remain streamlined and efficient for server use.
|
||||
| Property | Description |
|
||||
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`clientProps`** | Props to be passed to the Custom Components if it's a Client Component. [More details](#custom-props). |
|
||||
| **`exportName`** | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
|
||||
| **`path`** | File path to the Custom Component. Named exports can be appended to the end of the path, separated by a `#`. |
|
||||
| **`serverProps`** | Props to be passed to the Custom Component if it's a Server Component. [More details](#custom-props). |
|
||||
|
||||
Instead, we utilize component paths to reference React Components. This method enhances the Payload Config with actual React Component imports on the client side, without affecting server-side usage. A script is deployed to scan the Payload Config, collecting all component paths and creating an `importMap.js`. This file, located in app/(payload)/admin/importMap.js, must be statically imported by your Next.js root page and layout. The script imports all the React Components from the specified paths into a Map, associating them with their respective paths (the ones you defined).
|
||||
For more details on how to build Custom Components, see [Building Custom Components](#building-custom-components).
|
||||
|
||||
When constructing the `ClientConfig`, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the `PayloadComponent` with a `MappedComponent`. A `MappedComponent` includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the `<RenderComponent />` component within the Payload Admin Panel.
|
||||
### Import Map
|
||||
|
||||
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map. If you encounter any errors running this command, see the [Troubleshooting](../local-api/outside-nextjs#troubleshooting) section.
|
||||
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
|
||||
|
||||
### Component paths in external packages
|
||||
The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run `payload generate:importmap` to manually regenerate it.
|
||||
|
||||
Component paths are resolved relative to your project's base directory, which is either your current working directory or the directory specified in `config.admin.baseDir`. When using custom components from external packages, you can't use relative paths. Instead, use an import path that's accessible as if you were writing an import statement in your project's base directory.
|
||||
#### Custom Imports
|
||||
|
||||
For example, to export a field with a custom component from an external package named `my-external-package`:
|
||||
If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location.
|
||||
|
||||
To add a custom import to the Import Map, use the `admin.dependencies` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
export const MyCustomField: Field = {
|
||||
type: 'text',
|
||||
name: 'MyField',
|
||||
admin: {
|
||||
components: {
|
||||
Field: 'my-external-package/client#MyFieldComponent'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
Despite `MyFieldComponent` living in `src/components/MyFieldComponent.tsx` in `my-external-package`, this will not be accessible from the consuming project. Instead, we recommend exporting all custom components from one file in the external package. For example, you can define a `src/client.ts file in `my-external-package`:
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
export { MyFieldComponent } from './components/MyFieldComponent'
|
||||
```
|
||||
|
||||
Then, update the package.json of `my-external-package:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"exports": {
|
||||
"./client": {
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts",
|
||||
"default": "./dist/client.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This setup allows you to specify the component path as `my-external-package/client#MyFieldComponent` as seen above. The import map will generate:
|
||||
|
||||
```ts
|
||||
import { MyFieldComponent } from 'my-external-package/client'
|
||||
```
|
||||
|
||||
which is a valid way to access MyFieldComponent that can be resolved by the consuming project.
|
||||
|
||||
### Custom Components from unknown locations
|
||||
|
||||
By default, any component paths from known locations are added to the import map. However, if you need to add any components from unknown locations to the import map, you can do so by adding them to the `admin.dependencies` array in your Payload Config. This is mostly only relevant for plugin authors and not for regular Payload users.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
// ...
|
||||
@@ -220,117 +149,12 @@ export default {
|
||||
}
|
||||
```
|
||||
|
||||
This way, `TestComponent` is added to the import map, no matter if it's referenced in a known location or not. On the client, you can then use the component like this:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { RenderComponent, useConfig } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const CustomView = () => {
|
||||
const { config } = useConfig()
|
||||
return (
|
||||
<div>
|
||||
<RenderComponent mappedComponent={config.admin.dependencies?.myTestComponent} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Root Components
|
||||
|
||||
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.
|
||||
|
||||
To override Root Components, use the `admin.components` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
components: {
|
||||
// ...
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](#building-custom-components)._
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Nav`** | Contains the sidebar / mobile menu in its entirety. |
|
||||
| **`beforeNavLinks`** | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. |
|
||||
| **`afterNavLinks`** | An array of Custom Components to inject into the built-in Nav, _after_ the links. |
|
||||
| **`beforeDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
|
||||
| **`afterDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. |
|
||||
| **`beforeLogin`** | An array of Custom Components to inject into the built-in Login, _before_ the default login form. |
|
||||
| **`afterLogin`** | An array of Custom Components to inject into the built-in Login, _after_ the default login form. |
|
||||
| **`logout.Button`** | The button displayed in the sidebar that logs the user out. |
|
||||
| **`graphics.Icon`** | The simplified logo used in contexts like the the `Nav` component. |
|
||||
| **`graphics.Logo`** | The full logo used in contexts like the `Login` view. |
|
||||
| **`providers`** | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire Admin Panel. [More details](#custom-providers). |
|
||||
| **`actions`** | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. |
|
||||
| **`header`** | An array of Custom Components to be injected above the Payload header. |
|
||||
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
You can also use set [Collection Components](./collections#components) and [Global Components](./globals#components) in their respective configs.
|
||||
</Banner>
|
||||
|
||||
### Custom Providers
|
||||
|
||||
As you add more and more Custom Components to your [Admin Panel](./overview), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s). Payload allows you to inject your own context providers in your app so you can export your own custom hooks, etc.
|
||||
|
||||
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
providers: ['/path/to/MyProvider'], // highlight-line
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then build your Custom Provider as follows:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
const MyCustomContext = React.createContext(myCustomValue)
|
||||
|
||||
export const MyProvider: React.FC = ({ children }) => {
|
||||
return (
|
||||
<MyCustomContext.Provider value={myCustomValue}>
|
||||
{children}
|
||||
</MyCustomContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useMyCustomContext = () => useContext(MyCustomContext)
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong> Custom Providers are by definition Client Components. This means they must include the `use client` directive at the top of their files and cannot use server-only code.
|
||||
</Banner>
|
||||
|
||||
## Building Custom Components
|
||||
|
||||
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, with the exception of [Custom Providers](#custom-providers). This enables the use of the [Local API](../local-api/overview) directly on the front-end, among other things.
|
||||
|
||||
### Default Props
|
||||
|
||||
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class and the [`i18n`](../configuration/i18n) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself.
|
||||
|
||||
Here is an example:
|
||||
@@ -359,12 +183,46 @@ Each Custom Component receives the following props by default:
|
||||
| `payload` | The [Payload](../local-api/overview) class. |
|
||||
| `i18n` | The [i18n](../configuration/i18n) object. |
|
||||
|
||||
Custom Components also receive various other props that are specific to the context in which the Custom Component is being rendered. For example, [Custom Views](./views) receive the `user` prop. For a full list of available props, consult the documentation related to the specific component you are working with.
|
||||
|
||||
<Banner type="success">
|
||||
See [Root Components](#root-components), [Collection Components](#collection-components), [Global Components](#global-components), or [Field Components](#custom-field-components) for a complete list of all available components.
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
All Custom Components also receive various other props that are specific component being rendered. See [Root Components](#root-components), [Collection Components](#collection-components), [Global Components](#global-components), or [Field Components](#custom-field-components) for a complete list of all default props per component.
|
||||
</Banner>
|
||||
|
||||
### Custom Props
|
||||
|
||||
To pass in custom props from the config, you can use either the `clientProps` or `serverProps` properties depending on whether your prop is [serializable](https://react.dev/reference/rsc/use-client#serializable-types), and whether your component is a Server or Client Component.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
components: {
|
||||
logout: {
|
||||
Button: {
|
||||
path: '/src/components/Logout#MyComponent',
|
||||
clientProps: {
|
||||
myCustomProp: 'Hello, World!' // highlight-line
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = ({ myCustomProp }: { myCustomProp: string }) => {
|
||||
return (
|
||||
<button>{myCustomProp}</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Client Components
|
||||
|
||||
When [Building Custom Components](#building-custom-components), it's still possible to use client-side code such as `useState` or the `window` object. To do this, simply add the `use client` directive at the top of your file. Payload will automatically detect and remove all default, [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component.
|
||||
@@ -414,6 +272,7 @@ But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/us
|
||||
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](./hooks#useconfig) hook:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useConfig } from '@payloadcms/ui'
|
||||
|
||||
@@ -432,14 +291,13 @@ export const MyClientComponent: React.FC = () => {
|
||||
See [Using Hooks](#using-hooks) for more details.
|
||||
</Banner>
|
||||
|
||||
All [Field Components](./fields) automatically receive their respective Field Config through a common [`field`](./fields#the-field-prop) prop:
|
||||
All [Field Components](./fields) automatically receive their respective Field Config through props.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { TextFieldClientComponent } from 'payload'
|
||||
import type { TextFieldServerComponent } from 'payload'
|
||||
|
||||
export const MyClientFieldComponent: TextFieldClientComponent = ({ field: { name } }) => {
|
||||
export const MyClientFieldComponent: TextFieldServerComponent = ({ field: { name } }) => {
|
||||
return (
|
||||
<p>
|
||||
{`This field's name is ${name}`}
|
||||
@@ -448,28 +306,6 @@ export const MyClientFieldComponent: TextFieldClientComponent = ({ field: { name
|
||||
}
|
||||
```
|
||||
|
||||
### Using Hooks
|
||||
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { slug } = useDocumentInfo() // highlight-line
|
||||
|
||||
return (
|
||||
<p>{`Entity slug: ${slug}`}</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Getting the Current Language
|
||||
|
||||
All Custom Components can support multiple languages to be consistent with Payload's [Internationalization](../configuration/i18n). To do this, first add your translation resources to the [I18n Config](../configuration/i18n).
|
||||
@@ -492,6 +328,7 @@ export default async function MyServerComponent({ i18n }) {
|
||||
The best way to do this within a Client Component is to import the `useTranslation` hook from `@payloadcms/ui`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
|
||||
@@ -535,6 +372,7 @@ export default async function MyServerComponent({ payload, locale }) {
|
||||
The best way to do this within a Client Component is to import the `useLocale` hook from `@payloadcms/ui`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useLocale } from '@payloadcms/ui'
|
||||
|
||||
@@ -556,7 +394,29 @@ const Greeting: React.FC = () => {
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Styling Custom Components
|
||||
### Using Hooks
|
||||
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can one of the many hooks available depending on your needs.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { slug } = useDocumentInfo() // highlight-line
|
||||
|
||||
return (
|
||||
<p>{`Entity slug: ${slug}`}</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
See the [Hooks](./hooks) documentation for a full list of available hooks.
|
||||
</Banner>
|
||||
|
||||
### Adding Styles
|
||||
|
||||
Payload has a robust [CSS Library](./customizing-css) that you can use to style your Custom Components similarly to Payload's built-in styling. This will ensure that your Custom Components match the existing design system, and so that they automatically adapt to any theme changes that might occur.
|
||||
|
||||
@@ -592,10 +452,99 @@ Payload also exports its [SCSS](https://sass-lang.com) library for reuse which i
|
||||
background-color: var(--theme-elevation-900);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
You can also drill into Payload's own component styles, or easily apply global, app-wide CSS. More on that [here](./customizing-css).
|
||||
</Banner>
|
||||
|
||||
|
||||
## Root Components
|
||||
|
||||
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.
|
||||
|
||||
To override Root Components, use the `admin.components` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
// highlight-start
|
||||
components: {
|
||||
// ...
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](#building-custom-components)._
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Nav`** | Contains the sidebar / mobile menu in its entirety. |
|
||||
| **`beforeNavLinks`** | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. |
|
||||
| **`afterNavLinks`** | An array of Custom Components to inject into the built-in Nav, _after_ the links. |
|
||||
| **`beforeDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
|
||||
| **`afterDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. |
|
||||
| **`beforeLogin`** | An array of Custom Components to inject into the built-in Login, _before_ the default login form. |
|
||||
| **`afterLogin`** | An array of Custom Components to inject into the built-in Login, _after_ the default login form. |
|
||||
| **`logout.Button`** | The button displayed in the sidebar that logs the user out. |
|
||||
| **`graphics.Icon`** | The simplified logo used in contexts like the the `Nav` component. |
|
||||
| **`graphics.Logo`** | The full logo used in contexts like the `Login` view. |
|
||||
| **`providers`** | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire Admin Panel. [More details](#custom-providers). |
|
||||
| **`actions`** | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. |
|
||||
| **`header`** | An array of Custom Components to be injected above the Payload header. |
|
||||
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
You can also use set [Collection Components](./collections#custom-components) and [Global Components](./globals#custom-components) in their respective configs.
|
||||
</Banner>
|
||||
|
||||
### Custom Providers
|
||||
|
||||
As you add more and more Custom Components to your [Admin Panel](./overview), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s). Payload allows you to inject your own context providers in your app so you can export your own custom hooks, etc.
|
||||
|
||||
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
providers: ['/path/to/MyProvider'], // highlight-line
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Then build your Custom Provider as follows:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
|
||||
const MyCustomContext = React.createContext(myCustomValue)
|
||||
|
||||
export const MyProvider: React.FC = ({ children }) => {
|
||||
return (
|
||||
<MyCustomContext.Provider value={myCustomValue}>
|
||||
{children}
|
||||
</MyCustomContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useMyCustomContext = () => useContext(MyCustomContext)
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong> Custom Providers are by definition Client Components. This means they must include the `use client` directive at the top of their files and cannot use server-only code.
|
||||
</Banner>
|
||||
|
||||
@@ -6,7 +6,7 @@ desc:
|
||||
keywords:
|
||||
---
|
||||
|
||||
[Fields](../fields/overview) within the [Admin Panel](./overview) can be endlessly customized in their appearance and behavior without affecting their underlying data structure. Fields are designed to withstand heavy modification or even complete replacement through the use of [Custom Field Components](#field-components), [Conditional Logic](#conditional-logic), [Custom Validations](../fields/overview#validation), and more.
|
||||
[Fields](../fields/overview) within the [Admin Panel](./overview) can be endlessly customized in their appearance and behavior without affecting their underlying data structure. Fields are designed to withstand heavy modification or even complete replacement through the use of [Custom Field Components](#custom-components), [Conditional Logic](#conditional-logic), [Custom Validations](../fields/overview#validation), and more.
|
||||
|
||||
For example, your app might need to render a specific interface that Payload does not inherently support, such as a color picker. To do this, you could replace the default [Text Field](../fields/text) input with your own user-friendly component that formats the data into a valid color value.
|
||||
|
||||
@@ -56,335 +56,7 @@ The following options are available:
|
||||
| **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
|
||||
| **`hidden`** | Will transform the field into a `hidden` input type. Its value will still submit with requests in the Admin Panel, but the field itself will not be visible to editors. |
|
||||
|
||||
## Field Components
|
||||
|
||||
Within the [Admin Panel](./overview), fields are rendered in three distinct places:
|
||||
|
||||
- [Field](#the-field-component) - The actual form field rendered in the Edit View.
|
||||
- [Cell](#the-cell-component) - The table cell component rendered in the List View.
|
||||
- [Filter](#the-filter-component) - The filter component rendered in the List View.
|
||||
|
||||
To easily swap in Field Components with your own, use the `admin.components` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Component | Description |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Field`** | The form field rendered of the Edit View. [More details](#the-field-component). |
|
||||
| **`Cell`** | The table cell rendered of the List View. [More details](#the-cell-component). |
|
||||
| **`Filter`** | The filter component rendered in the List View. [More details](#the-filter-component). || Component | Description |
|
||||
| **`Label`** | Override the default Label of the Field Component. [More details](#the-label-component). |
|
||||
| **`Error`** | Override the default Error of the Field Component. [More details](#the-error-component). |
|
||||
| **`Description`** | Override the default Description of the Field Component. [More details](#the-description-component). |
|
||||
| **`beforeInput`** | An array of elements that will be added before the input of the Field Component. [More details](#afterinput-and-beforeinput).|
|
||||
| **`afterInput`** | An array of elements that will be added after the input of the Field Component. [More details](#afterinput-and-beforeinput). |
|
||||
|
||||
_\* **`beforeInput`** and **`afterInput`** are only supported in fields that do not contain other fields, such as [`Text`](../fields/text), and [`Textarea`](../fields/textarea)._
|
||||
|
||||
### The Field Component
|
||||
|
||||
The Field Component is the actual form field rendered in the Edit View. This is the input that user's will interact with when editing a document.
|
||||
|
||||
To easily swap in your own Field Component, use the `admin.components.Field` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/path/to/MyFieldComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
<Banner type="warning">
|
||||
Instead of replacing the entire Field Component, you can alternately replace or slot-in only specific parts by using the [`Label`](#the-label-component), [`Error`](#the-error-component), [`beforeInput`](#afterinput-and-beforinput), and [`afterInput`](#afterinput-and-beforinput) properties.
|
||||
</Banner>
|
||||
|
||||
All Field Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`docPreferences`** | An object that contains the [Preferences](./preferences) for the document.
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
|
||||
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
|
||||
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
|
||||
| **`validate`** | A function that can be used to validate the field. |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
#### Sending and receiving values from the form
|
||||
|
||||
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
|
||||
|
||||
To do so, import the [`useField`](./hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useField } from '@payloadcms/ui'
|
||||
|
||||
export const CustomTextField: React.FC = () => {
|
||||
const { value, setValue } = useField() // highlight-line
|
||||
|
||||
return (
|
||||
<input
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](./hooks) documentation. For additional help, see [Building Custom Components](./components#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Field Components, you can import the component type to ensure type safety. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview) and for every client/server environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientComponent`:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldClientComponent,
|
||||
TextFieldServerComponent,
|
||||
TextFieldClientProps,
|
||||
TextFieldServerProps,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### The `field` Prop
|
||||
|
||||
All Field Components are passed their own Field Config through a common `field` prop. Within Server Components, this is the original Field Config as written within your Payload Config. Within Client Components, however, this is a "Client Config", which is a sanitized, client-friendly version of the Field Config. This is because the original Field Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types), meaning it cannot be passed into Client Components without first being transformed.
|
||||
|
||||
The Client Field Config is an exact copy of the original Field Config, minus all non-serializable properties, plus all evaluated functions such as field labels, [Custom Components](../components), etc.
|
||||
|
||||
Server Component:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import type { TextFieldServerComponent } from 'payload'
|
||||
import { TextField } from '@payloadcms/ui'
|
||||
|
||||
export const MyServerField: TextFieldServerComponent = ({ clientField }) => {
|
||||
return <TextField field={clientField} />
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Tip:</strong>
|
||||
Server Components can still access the original Field Config through the `field` prop.
|
||||
</Banner>
|
||||
|
||||
Client Component:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { TextFieldClientComponent } from 'payload'
|
||||
import { TextField } from '@payloadcms/ui'
|
||||
|
||||
export const MyTextField: TextFieldClientComponent = ({ field }) => {
|
||||
return <TextField field={field} />
|
||||
}
|
||||
```
|
||||
|
||||
The following additional properties are also provided to the `field` prop:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`_isPresentational`** | A boolean indicating that the field is purely visual and does not directly affect data or change data shape, i.e. the [UI Field](../fields/ui). |
|
||||
| **`_path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray[0].myField`. |
|
||||
| **`_schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `myGroup.myArray.myField` |
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Note:</strong>
|
||||
These properties are underscored to denote that they are not part of the original Field Config, and instead are attached during client sanitization to make fields easier to work with on the front-end.
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Field Components, you can import the client field props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientComponent`:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldClientComponent,
|
||||
TextFieldServerComponent,
|
||||
TextFieldClientProps,
|
||||
TextFieldServerProps,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### The Cell Component
|
||||
|
||||
The Cell Component is rendered in the table of the List View. It represents the value of the field when displayed in a table cell.
|
||||
|
||||
To easily swap in your own Cell Component, use the `admin.components.Cell` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: '/path/to/MyCustomCellComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
All Cell Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | ----------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
|
||||
| **`onClick`** | A function that is called when the cell is clicked. |
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Tip:</strong>
|
||||
Use the [`useTableCell`](./hooks#usetablecell) hook to subscribe to the field's `cellData` and `rowData`.
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
### The Label Component
|
||||
|
||||
The Label Component is rendered anywhere a field needs to be represented by a label. This is typically used in the Edit View, but can also be used in the List View and elsewhere.
|
||||
|
||||
To easily swap in your own Label Component, use the `admin.components.Label` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Label: '/path/to/MyCustomLabelComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
Custom Label Components receive all [Field Component](#the-field-component) props, plus the following props:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Label Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `LabelServerComponent` or `LabelClientComponent` to the type of field, i.e. `TextFieldLabelClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldLabelServerComponent,
|
||||
TextFieldLabelClientComponent,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### The Error Component
|
||||
|
||||
The Error Component is rendered when a field fails validation. It is typically displayed beneath the field input in a visually-compelling style.
|
||||
|
||||
To easily swap in your own Error Component, use the `admin.components.Error` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Error: '/path/to/MyCustomErrorComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
Custom Error Components receive all [Field Component](#the-field-component) props, plus the following props:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `ErrorServerComponent` or `ErrorClientComponent` to the type of field, i.e. `TextFieldErrorClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldErrorServerComponent,
|
||||
TextFieldErrorClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### The Description Property
|
||||
## Field Descriptions
|
||||
|
||||
Field Descriptions are used to provide additional information to the editor about a field, such as special instructions. Their placement varies from field to field, but typically are displayed with subtle style differences beneath the field inputs.
|
||||
|
||||
@@ -392,7 +64,7 @@ A description can be configured in three ways:
|
||||
|
||||
- As a string.
|
||||
- As a function which returns a string. [More details](#description-functions).
|
||||
- As a React component. [More details](#the-description-component).
|
||||
- As a React component. [More details](#description).
|
||||
|
||||
To easily add a Custom Description to a field, use the `admin.description` property in your [Field Config](../fields/overview):
|
||||
|
||||
@@ -416,7 +88,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Reminder:</strong>
|
||||
To replace the Field Description with a [Custom Component](./components), use the `admin.components.Description` property. [More details](#the-description-component).
|
||||
To replace the Field Description with a [Custom Component](./components), use the `admin.components.Description` property. [More details](#description).
|
||||
</Banner>
|
||||
|
||||
#### Description Functions
|
||||
@@ -449,89 +121,6 @@ All Description Functions receive the following arguments:
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`t`** | The `t` function used to internationalize the Admin Panel. [More details](../configuration/i18n) |
|
||||
|
||||
### The Description Component
|
||||
|
||||
Alternatively to the [Description Property](#the-description-property), you can also use a [Custom Component](./components) as the Field Description. This can be useful when you need to provide more complex feedback to the user, such as rendering dynamic field values or other interactive elements.
|
||||
|
||||
To easily add a Description Component to a field, use the `admin.components.Description` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Description: '/path/to/MyCustomDescriptionComponent', // highlight-line
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build a Custom Description, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
Custom Description Components receive all [Field Component](#the-field-component) props, plus the following props:
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`field`** | In Server Components, this is the original Field Config. In Client Components, this is the sanitized Client Field Config. [More details](#the-field-prop). |
|
||||
| **`clientField`** | Server components receive the Client Field Config through this prop. [More details](#the-field-prop). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `DescriptionServerComponent` or `DescriptionClientComponent` to the type of field, i.e. `TextFieldDescriptionClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldDescriptionServerComponent,
|
||||
TextFieldDescriptionClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### afterInput and beforeInput
|
||||
|
||||
With these properties you can add multiple components _before_ and _after_ the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component.
|
||||
|
||||
To add components before and after the input element, use the `admin.components.beforeInput` and `admin.components.afterInput` properties in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
// highlight-start
|
||||
beforeInput: ['/path/to/MyCustomComponent'],
|
||||
afterInput: ['/path/to/MyOtherCustomComponent'],
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
## Conditional Logic
|
||||
|
||||
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes three arguments:
|
||||
@@ -570,3 +159,322 @@ The `condition` function should return a boolean that will control if the field
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Components
|
||||
|
||||
Within the [Admin Panel](./overview), fields are represented in three distinct places:
|
||||
|
||||
- [Field](#field) - The actual form field rendered in the Edit View.
|
||||
- [Cell](#cell) - The table cell component rendered in the List View.
|
||||
- [Filter](#filter) - The filter component rendered in the List View.
|
||||
|
||||
To easily swap in Field Components with your own, use the `admin.components` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: { // highlight-line
|
||||
// ...
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Component | Description |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Field`** | The form field rendered of the Edit View. [More details](#field). |
|
||||
| **`Cell`** | The table cell rendered of the List View. [More details](#cell). |
|
||||
| **`Filter`** | The filter component rendered in the List View. [More details](#filter). |
|
||||
| **`Label`** | Override the default Label of the Field Component. [More details](#label). |
|
||||
| **`Error`** | Override the default Error of the Field Component. [More details](#error). |
|
||||
| **`Description`** | Override the default Description of the Field Component. [More details](#description). |
|
||||
| **`beforeInput`** | An array of elements that will be added before the input of the Field Component. [More details](#afterinput-and-beforeinput).|
|
||||
| **`afterInput`** | An array of elements that will be added after the input of the Field Component. [More details](#afterinput-and-beforeinput). |
|
||||
|
||||
### Field
|
||||
|
||||
The Field Component is the actual form field rendered in the Edit View. This is the input that user's will interact with when editing a document.
|
||||
|
||||
To easily swap in your own Field Component, use the `admin.components.Field` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/path/to/MyFieldComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
|
||||
|
||||
<Banner type="warning">
|
||||
Instead of replacing the entire Field Component, you can alternately replace or slot-in only specific parts by using the [`Label`](#label), [`Error`](#error), [`beforeInput`](#afterinput-and-beforinput), and [`afterInput`](#afterinput-and-beforinput) properties.
|
||||
</Banner>
|
||||
|
||||
#### Default Props
|
||||
|
||||
All Field Components receive the following props by default:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`docPreferences`** | An object that contains the [Preferences](./preferences) for the document.
|
||||
| **`field`** | In Client Components, this is the sanitized Client Field Config. In Server Components, this is the original Field Config. Server Components will also receive the sanitized field config through the`clientField` prop (see below). |
|
||||
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
|
||||
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
|
||||
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
|
||||
| **`validate`** | A function that can be used to validate the field. |
|
||||
| **`path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray.0.myField`. |
|
||||
| **`schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `posts.myGroup.myArray.myField`. |
|
||||
| **`indexPath`** | A hyphen-notated string representing the path to the field _within the nearest named ancestor field_, i.e. `0-0` |
|
||||
|
||||
In addition to the above props, all Server Components will also receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`clientField`** | The serializable Client Field Config. |
|
||||
| **`field`** | The Field Config. [More details](../fields/overview). |
|
||||
| **`data`** | The current document being edited. |
|
||||
| **`i18n`** | The [i18n](../configuration/i18n) object.
|
||||
| **`payload`** | The [Payload](../local-api/overview) class. |
|
||||
| **`permissions`** | The field permissions based on the currently authenticated user. |
|
||||
| **`siblingData`** | The data of the field's siblings. |
|
||||
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
|
||||
| **`value`** | The value of the field at render-time. |
|
||||
|
||||
#### Sending and receiving values from the form
|
||||
|
||||
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
|
||||
|
||||
To do so, import the [`useField`](./hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useField } from '@payloadcms/ui'
|
||||
|
||||
export const CustomTextField: React.FC = () => {
|
||||
const { value, setValue } = useField() // highlight-line
|
||||
|
||||
return (
|
||||
<input
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](./hooks) documentation. For additional help, see [Building Custom Components](./components#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Field Components, you can import the client field props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientComponent`:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldClientComponent,
|
||||
TextFieldServerComponent,
|
||||
TextFieldClientProps,
|
||||
TextFieldServerProps,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### Cell
|
||||
|
||||
The Cell Component is rendered in the table of the List View. It represents the value of the field when displayed in a table cell.
|
||||
|
||||
To easily swap in your own Cell Component, use the `admin.components.Cell` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: '/path/to/MyCustomCellComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
All Cell Components receive the same [Default Field Component Props](#field), plus the following:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | ----------------------------------------------------------------- |
|
||||
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
|
||||
| **`onClick`** | A function that is called when the cell is clicked. |
|
||||
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
|
||||
|
||||
### Label
|
||||
|
||||
The Label Component is rendered anywhere a field needs to be represented by a label. This is typically used in the Edit View, but can also be used in the List View and elsewhere.
|
||||
|
||||
To easily swap in your own Label Component, use the `admin.components.Label` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Label: '/path/to/MyCustomLabelComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
All Custom Label Components receive the same [Default Field Component Props](#field).
|
||||
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Label Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `LabelServerComponent` or `LabelClientComponent` to the type of field, i.e. `TextFieldLabelClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldLabelServerComponent,
|
||||
TextFieldLabelClientComponent,
|
||||
// ...and so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### Description
|
||||
|
||||
Alternatively to the [Description Property](#the-description-property), you can also use a [Custom Component](./components) as the Field Description. This can be useful when you need to provide more complex feedback to the user, such as rendering dynamic field values or other interactive elements.
|
||||
|
||||
To easily add a Description Component to a field, use the `admin.components.Description` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Description: '/path/to/MyCustomDescriptionComponent', // highlight-line
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All Custom Description Components receive the same [Default Field Component Props](#field).
|
||||
|
||||
For details on how to build a Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `DescriptionServerComponent` or `DescriptionClientComponent` to the type of field, i.e. `TextFieldDescriptionClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldDescriptionServerComponent,
|
||||
TextFieldDescriptionClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### Error
|
||||
|
||||
The Error Component is rendered when a field fails validation. It is typically displayed beneath the field input in a visually-compelling style.
|
||||
|
||||
To easily swap in your own Error Component, use the `admin.components.Error` property in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const myField: Field = {
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Error: '/path/to/MyCustomErrorComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
All Error Components receive the [Default Field Component Props](#field).
|
||||
|
||||
For details on how to build Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Error Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `ErrorServerComponent` or `ErrorClientComponent` to the type of field, i.e. `TextFieldErrorClientComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
TextFieldErrorServerComponent,
|
||||
TextFieldErrorClientComponent,
|
||||
// And so on for each Field Type
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### afterInput and beforeInput
|
||||
|
||||
With these properties you can add multiple components _before_ and _after_ the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component.
|
||||
|
||||
To add components before and after the input element, use the `admin.components.beforeInput` and `admin.components.afterInput` properties in your [Field Config](../fields/overview):
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
fields: [
|
||||
// ...
|
||||
{
|
||||
name: 'myField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
// highlight-start
|
||||
beforeInput: ['/path/to/MyCustomComponent'],
|
||||
afterInput: ['/path/to/MyOtherCustomComponent'],
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
All `afterInput` and `beforeInput` Components receive the same [Default Field Component Props](#field).
|
||||
|
||||
For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components).
|
||||
|
||||
@@ -29,13 +29,13 @@ The following options are available:
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`group`** | Text used as a label for grouping Collection and Global links together in the navigation. |
|
||||
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Global from navigation and admin routing. |
|
||||
| **`components`** | Swap in your own React components to be used within this Global. [More details](#components). |
|
||||
| **`components`** | Swap in your own React components to be used within this Global. [More details](#custom-components). |
|
||||
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](#preview). |
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
| **`meta`** | Page metadata overrides to apply to this Global within the Admin Panel. [More details](./metadata). |
|
||||
|
||||
### Components
|
||||
### Custom Components
|
||||
|
||||
Globals can set their own [Custom Components](./components) which only apply to [Global](../configuration/globals)-specific UI within the [Admin Panel](./overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ The `useField` hook accepts the following arguments:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `path` | If you do not provide a `path` or a `name`, this hook will look for one using the [`useFieldProps`](#usefieldprops) hook. |
|
||||
| `path` | If you do not provide a `path`, `name` will be used instead. This is the path to the field in the form data. |
|
||||
| `validate` | A validation function executed client-side _before_ submitting the form to the server. Different than [Field-level Validation](../fields/overview#validation) which runs strictly on the server. |
|
||||
| `disableFormData` | If `true`, the field will not be included in the form data when the form is submitted. |
|
||||
| `hasRows` | If `true`, the field will be treated as a field with rows. This is useful for fields like `array` and `blocks`. |
|
||||
@@ -72,32 +72,6 @@ type FieldType<T> = {
|
||||
}
|
||||
```
|
||||
|
||||
## useFieldProps
|
||||
|
||||
[Custom Field Components](./fields#the-field-component) can be rendered on the server. When using a server component as a custom field component, you can access dynamic props from within any client component rendered by your custom server component. This is done using the `useFieldProps` hook. This is important because some fields can be dynamic, such as when nested in an [`array`](../fields/array) or [`blocks`](../fields/block) field. For example, items can be added, re-ordered, or deleted on-the-fly.
|
||||
|
||||
You can use the `useFieldProps` hooks to access dynamic props like `path`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useFieldProps } from '@payloadcms/ui'
|
||||
|
||||
const CustomTextField: React.FC = () => {
|
||||
const { path } = useFieldProps() // highlight-line
|
||||
|
||||
return (
|
||||
<div>
|
||||
{path}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
The [`useField`](#usefield) hook calls the `useFieldProps` hook internally, so you don't need to use both in the same component unless explicitly needed.
|
||||
</Banner>
|
||||
|
||||
## useFormFields
|
||||
|
||||
There are times when a custom field component needs to have access to data from other fields, and you have a few options to do so. The `useFormFields` hook is a powerful and highly performant way to retrieve a form's field state, as well as to retrieve the `dispatchFields` method, which can be helpful for setting other fields' form states from anywhere within a form.
|
||||
@@ -900,27 +874,6 @@ const MyComponent: React.FC = () => {
|
||||
}
|
||||
```
|
||||
|
||||
## useTableCell
|
||||
|
||||
Similar to [`useFieldProps`](#usefieldprops), all [Custom Cell Components](./fields#the-cell-component) are rendered on the server, and as such, only have access to static props at render time. But, some props need to be dynamic, such as the field value itself.
|
||||
|
||||
For this reason, dynamic props like `cellData` are managed in their own React context, which can be accessed using the `useTableCell` hook.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useTableCell } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
const { cellData } = useTableCell() // highlight-line
|
||||
|
||||
return (
|
||||
<div>
|
||||
{cellData}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useDocumentEvents
|
||||
|
||||
The `useDocumentEvents` hook provides a way of subscribing to cross-document events, such as updates made to nested documents within a drawer. This hook will report document events that are outside the scope of the document currently being edited. This hook provides the following:
|
||||
|
||||
@@ -195,7 +195,7 @@ app/
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
If you set Root-level Routes _before_ auto-generating the Admin Panel, your [Project Structure](#project-structure) will already be set up correctly.
|
||||
If you set Root-level Routes _before_ auto-generating the Admin Panel via `create-payload-app`, your [Project Structure](#project-structure) will already be set up correctly.
|
||||
</Banner>
|
||||
|
||||
### Admin-level Routes
|
||||
|
||||
@@ -15,7 +15,7 @@ There are four types of views within the Admin Panel:
|
||||
- [Global Views](#global-views)
|
||||
- [Document Views](#document-views)
|
||||
|
||||
To swap in your own Custom Views, consult the list of available components. Determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-views) accordingly.
|
||||
To swap in your own Custom View, first consult the list of available components, determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-views) accordingly.
|
||||
|
||||
## Root Views
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export default buildConfig({
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en', // required
|
||||
fallback: true,
|
||||
fallback: true, // defaults to true
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -81,7 +81,7 @@ The following options are available:
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
|
||||
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
|
||||
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated. |
|
||||
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
|
||||
|
||||
### Locales
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ The following options are available:
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
|
||||
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
|
||||
| **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](../graphql/overview#graphql-options). |
|
||||
| **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/overview#csrf-protection). |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Array Fields are intended for sets of repeating fields, that you define. L
|
||||
keywords: array, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Array Field is used when you need to have a set of "repeating" [Fields](./overview). It stores an array of objects containing fields that you define. These fields can be of any type, including other arrays to achieve infinitely nested structures.
|
||||
The Array Field is used when you need to have a set of "repeating" [Fields](./overview). It stores an array of objects containing fields that you define. These fields can be of any type, including other arrays, to achieve infinitely nested data structures.
|
||||
|
||||
Arrays are useful for many different types of content from simple to complex, such as:
|
||||
|
||||
|
||||
@@ -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. |
|
||||
@@ -146,7 +147,7 @@ You can control the user experience of the join field using the `admin` config p
|
||||
| Option | Description |
|
||||
|------------------------|----------------------------------------------------------------------------------------|
|
||||
| **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. |
|
||||
| **`components.Label`** | Override the default Label of the Field Component. [More details](#the-label-component). |
|
||||
| **`components.Label`** | Override the default Label of the Field Component. [More details](../admin/fields#label) |
|
||||
|
||||
## Join Field Data
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ desc: The JSON field type will store any string in the Database. Learn how to us
|
||||
keywords: json, jsonSchema, schema, validation, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The JSON Field saves actual JSON in the database, which differs from the Code field that saves the value as a string in the database.
|
||||
The JSON Field saves raw JSON to the database and provides the [Admin Panel](../admin/overview) with a code editor styled interface. This is different from the [Code Field](./code) which saves the value as a string in the database.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/json.png"
|
||||
|
||||
@@ -93,13 +93,14 @@ Presentational Fields do not store data in the database. Instead, they are used
|
||||
Here are the available Presentational Fields:
|
||||
|
||||
- [Collapsible](/docs/fields/collapsible) - nests fields within a collapsible component
|
||||
- [Join](/docs/fields/join) - achieves two-way data binding between fields
|
||||
- [Row](/docs/fields/row) - aligns fields horizontally
|
||||
- [Tabs (Unnamed)](/docs/fields/tabs) - nests fields within a tabbed layout
|
||||
- [UI](/docs/fields/ui) - blank field for custom UI components
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Tip:</strong>
|
||||
Don't see a Field Type that fits your needs? You can build your own using a [Custom Field Component](../admin/fields#the-field-component).
|
||||
Don't see a Field Type that fits your needs? You can build your own using a [Custom Field Component](../admin/fields#field).
|
||||
</Banner>
|
||||
|
||||
## Field Options
|
||||
@@ -123,7 +124,7 @@ export const MyField: Field = {
|
||||
|
||||
### Field Names
|
||||
|
||||
All [Data Fields](#data-fields) require a `name` property. This is the key that will be used to store and retrieve the field's value in the database. This property must be unique within the Collection, Global, or nested group that it is defined in.
|
||||
All [Data Fields](#data-fields) require a `name` property. This is the key that will be used to store and retrieve the field's value in the database. This property must be unique amongst this field's siblings.
|
||||
|
||||
To set a field's name, use the `name` property in your Field Config:
|
||||
|
||||
@@ -205,7 +206,7 @@ export const MyField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
Default values can be defined as a static value or a function that returns a value. When a `defaultValue` is defined statically, Payload's DB adapters will apply it to the database schema or models.
|
||||
Default values can be defined as a static value or a function that returns a value. When a `defaultValue` is defined statically, Payload's [Database Adapters](../database/overview) will apply it to the database schema or models.
|
||||
|
||||
Functions can be written to make use of the following argument properties:
|
||||
|
||||
@@ -264,7 +265,7 @@ The following arguments are provided to the `validate` function:
|
||||
|
||||
#### Validation Context
|
||||
|
||||
The `ctx` argument contains full document data, sibling field data, the current operation, and other useful information such as currently authenticated in user:
|
||||
The `ctx` argument contains full document data, sibling field data, the current operation, and other useful information such as currently authenticated user:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
@@ -356,9 +357,9 @@ 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`:
|
||||
To define a custom ID field, add a top-level field with the `name` property set to `id`:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
@@ -368,6 +369,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
|
||||
|
||||
@@ -204,4 +204,4 @@ If you are looking to create a dynamic select field, the following tutorial will
|
||||
drawerTitle="How to Create a Custom Select Field: A Step-by-Step Guide"
|
||||
/>
|
||||
|
||||
If you want to learn more about custom components check out the [Admin > Custom Component](/docs/admin/components#field-component) docs.
|
||||
If you want to learn more about custom components check out the [Admin > Custom Component](/docs/admin/components#field) docs.
|
||||
|
||||
@@ -32,8 +32,8 @@ export const MyUIField: Field = {
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | A unique identifier for this field. |
|
||||
| **`label`** | Human-readable label for this UI field. |
|
||||
| **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More](../admin/components/#field-component) |
|
||||
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](../admin/components/#field-component) |
|
||||
| **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More](../admin/components/#field) |
|
||||
| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](../admin/components/#field) |
|
||||
| **`admin.disableListColumn`** | Set `disableListColumn` to `true` to prevent the UI field from appearing in the list view column selector. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ Adding Payload to an existing Next.js app is super straightforward. You can eith
|
||||
|
||||
If you don't have a Next.js app already, but you still want to start a project from a blank Next.js app, you can create a new Next.js app using `npx create-next-app` - and then just follow the steps below to install Payload.
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Note:</strong> Next.js version 15 or higher is required for Payload.
|
||||
</Banner>
|
||||
|
||||
#### 1. Install the relevant packages
|
||||
|
||||
First, you'll want to add the required Payload packages to your project and can do so by running the command below:
|
||||
|
||||
46
docs/jobs-queue/jobs.mdx
Normal file
46
docs/jobs-queue/jobs.mdx
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Jobs
|
||||
label: Jobs
|
||||
order: 40
|
||||
desc: A Job is a set of work that is offloaded from your APIs and will be processed at a later date.
|
||||
keywords: jobs queue, application framework, typescript, node, react, nextjs
|
||||
---
|
||||
|
||||
Now that we have covered Tasks and Workflows, we can tie them together with a concept called a Job.
|
||||
|
||||
<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>
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
#### 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({
|
||||
// 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',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
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({
|
||||
task: 'createPost',
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -1,382 +1,70 @@
|
||||
---
|
||||
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 which is a very powerful feature of many application frameworks.
|
||||
|
||||
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.
|
||||
### Example use cases
|
||||
|
||||
Tasks can either be defined within the `jobs.tasks` array in your payload config, or they can be run inline within a workflow.
|
||||
**Non-blocking workloads**
|
||||
|
||||
### Defining tasks in the config
|
||||
You might need to perform some complex, slow-running 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. Instead of running this logic directly in a hook, which would block your API response from returning until the expensive work is completed, you can queue a new Job and let it run at a later date.
|
||||
|
||||
Simply add a task to the `jobs.tasks` array in your Payload config. A task consists of the following fields:
|
||||
Examples:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `slug` | Define a slug-based name for this job. This slug needs to be unique among both tasks and workflows.|
|
||||
| `handler` | The function that should be responsible for running the job. You can either pass a string-based path to the job function file, or the job function itself. If you are using large dependencies within your job, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. |
|
||||
| `inputSchema` | Define the input field schema - payload will generate a type for this schema. |
|
||||
| `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this task. By default, this is "Task" + the capitalized task slug. |
|
||||
| `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. |
|
||||
| `retries` | Specify the number of times that this step should be retried if it fails. |
|
||||
- Create vector embeddings from your documents, and keep them in sync as your documents change
|
||||
- Send data to a third-party API on document change
|
||||
- Trigger emails based on customer actions
|
||||
|
||||
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.
|
||||
**Scheduled actions**
|
||||
|
||||
Example:
|
||||
If you need to schedule an action to be run or processed at a certain date in the future, you can queue a job with the `waitUntil` property set. This will make it so the job is not "picked up" until that `waitUntil` date has passed.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
retries: 2,
|
||||
slug: 'createPost',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ input, job, req }) => {
|
||||
const newPost = await req.payload.create({
|
||||
collection: 'post',
|
||||
req,
|
||||
data: {
|
||||
title: input.title,
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
postID: newPost.id,
|
||||
},
|
||||
}
|
||||
},
|
||||
} as TaskConfig<'createPost'>,
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
Examples:
|
||||
|
||||
### Example: defining external tasks
|
||||
- Process scheduled posts, where the scheduled date is at a time set in the future
|
||||
- Unpublish posts at a given time
|
||||
- Send a reminder email to a customer after X days of signing up for a trial
|
||||
|
||||
payload.config.ts:
|
||||
**Periodic sync or similar scheduled action**
|
||||
|
||||
```ts
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
Some applications may need to perform a regularly scheduled operation of some type. Jobs are perfect for this because you can execute their logic using `cron`, scheduled nightly, every twelve hours, or some similar time period.
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
Examples:
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
retries: 2,
|
||||
slug: 'createPost',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: path.resolve(dirname, 'src/tasks/createPost.ts') + '#createPostHandler',
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
- You'd like to send emails to all customers on a regular, scheduled basis
|
||||
- Periodically trigger a rebuild of your frontend at night
|
||||
- Sync resources to or from a third-party API during non-peak times
|
||||
|
||||
src/tasks/createPost.ts:
|
||||
**Offloading complex operations**
|
||||
|
||||
```ts
|
||||
import type { TaskHandler } from 'payload'
|
||||
You may run into the need to perform computationally expensive functions which might slow down your main Payload API server(s). The Jobs Queue allows you to offload these tasks a separate compute resource rather than slowing down the server(s) that run your Payload APIs. With Payload Task definitions, you can even keep large dependencies out of your main Next.js bundle by dynamically importing them only when they are used. This keeps your Next.js + Payload compilation fast and ensures large dependencies do not get bundled into your Payload production build.
|
||||
|
||||
export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job, req }) => {
|
||||
const newPost = await req.payload.create({
|
||||
collection: 'post',
|
||||
req,
|
||||
data: {
|
||||
title: input.title,
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
postID: newPost.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
Examples:
|
||||
|
||||
- You need to create (and then keep in sync) vector embeddings of your documents as they change, but you use an open source model to generate embeddings
|
||||
- You have a PDF generator that needs to dynamically build and send PDF versions of documents to customers
|
||||
- You need to use a headless browser to perform some type of logic
|
||||
- You need to perform a series of actions, each of which depends on a prior action and should be run in as "durable" of a fashion as possible
|
||||
|
||||
## Defining workflows
|
||||
### How it works
|
||||
|
||||
There are two types of workflows - JS-based workflows and JSON-based workflows.
|
||||
There are a few concepts that you should become familiarized with before using Payload's Jobs Queue. We recommend learning what each of these does in order to fully understand how to leverage the power of Payload's Jobs Queue.
|
||||
|
||||
### Defining JS-based workflows
|
||||
1. [Tasks](/docs/beta/jobs-queue/tasks)
|
||||
1. [Workflows](/docs/beta/jobs-queue/workflows)
|
||||
1. [Jobs](/docs/beta/jobs-queue/jobs)
|
||||
1. [Queues](/docs/beta/jobs-queue/queues)
|
||||
|
||||
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.
|
||||
All of these pieces work together in order to allow you to offload long-running, expensive, or future scheduled work from your main APIs.
|
||||
|
||||
Tasks that have successfully been completed will simply re-return the cached output without running again, and failed tasks will be re-run.
|
||||
Here's a quick overview:
|
||||
|
||||
Simply add a workflow to the `jobs.wokflows` array in your Payload config. A wokflow consists of the following fields:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `slug` | Define a slug-based name for this workflow. This slug needs to be unique among both tasks and workflows.|
|
||||
| `handler` | The function that should be responsible for running the workflow. You can either pass a string-based path to the workflow function file, or workflow job function itself. If you are using large dependencies within your workflow, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. |
|
||||
| `inputSchema` | Define the input field schema - payload will generate a type for this schema. |
|
||||
| `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this workflow. By default, this is "Workflow" + the capitalized workflow slug. |
|
||||
| `label` | Define a human-friendly label for this workflow. |
|
||||
| `queue` | Optionally, define the queue name that this workflow should be tied to. Defaults to "default". |
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// ...
|
||||
]
|
||||
workflows: [
|
||||
{
|
||||
slug: 'createPostAndUpdate',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ job, runTask }) => {
|
||||
const output = await runTask({
|
||||
task: 'createPost',
|
||||
id: '1',
|
||||
input: {
|
||||
title: job.input.title,
|
||||
},
|
||||
})
|
||||
|
||||
await runTask({
|
||||
task: 'updatePost',
|
||||
id: '2',
|
||||
input: {
|
||||
post: job.taskStatus.createPost['1'].output.postID, // or output.postID
|
||||
title: job.input.title + '2',
|
||||
},
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'updatePost'>
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Running tasks inline
|
||||
|
||||
In order to run tasks inline without predefining them, you can use the `runTaskInline` function.
|
||||
|
||||
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.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// ...
|
||||
]
|
||||
workflows: [
|
||||
{
|
||||
slug: 'createPostAndUpdate',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ job, runTask }) => {
|
||||
const output = await runTask({
|
||||
task: 'createPost',
|
||||
id: '1',
|
||||
input: {
|
||||
title: job.input.title,
|
||||
},
|
||||
})
|
||||
|
||||
const { newPost } = await runTaskInline({
|
||||
task: async ({ req }) => {
|
||||
const newPost = await req.payload.update({
|
||||
collection: 'post',
|
||||
id: output.postID,
|
||||
req,
|
||||
retries: 3,
|
||||
data: {
|
||||
title: 'updated!',
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
newPost
|
||||
},
|
||||
}
|
||||
},
|
||||
id: '2',
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'updatePost'>
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Defining JSON-based workflows
|
||||
|
||||
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.
|
||||
|
||||
This functionality is not available yet, but it will be available in the future.
|
||||
|
||||
## Queueing workflows and tasks
|
||||
|
||||
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.
|
||||
|
||||
Example: queueing workflows:
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
workflows: 'createPostAndUpdate',
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Example: queueing tasks:
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
task: 'createPost',
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Running workflows and tasks
|
||||
|
||||
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:
|
||||
|
||||
### Endpoint
|
||||
|
||||
Make a fetch request to the `api/payload-jobs/run` endpoint:
|
||||
|
||||
```ts
|
||||
await fetch('/api/payload-jobs/run', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `JWT ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Local API
|
||||
|
||||
Run the payload.jobs.run function:
|
||||
|
||||
```ts
|
||||
const results = await payload.jobs.run()
|
||||
|
||||
// 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/payload-jobs/run",
|
||||
"schedule": "*/5 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
Add a new environment variable named `CRON_SECRET` to your Vercel project settings. This 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:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
access: {
|
||||
run: ({ req }: { req: PayloadRequest }): boolean => {
|
||||
const authHeader = req.headers.get('authorization');
|
||||
return authHeader === `Bearer ${process.env.CRON_SECRET}`;
|
||||
},
|
||||
},
|
||||
// Other job configurations...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
- A Task is a specific function that performs business logic
|
||||
- Workflows are groupings of specific tasks which should be run in-order, and can be retried from a specific point of failure
|
||||
- A Job is an instance of a single task or workflow which will be executed
|
||||
- A Queue is a way to segment your jobs into different "groups" - for example, some to run nightly, and others to run every 10 minutes
|
||||
|
||||
120
docs/jobs-queue/queues.mdx
Normal file
120
docs/jobs-queue/queues.mdx
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Queues
|
||||
label: Queues
|
||||
order: 50
|
||||
desc: A Queue is a specific group of jobs which can be executed in the order that they were added.
|
||||
keywords: jobs queue, application framework, typescript, node, react, nextjs
|
||||
---
|
||||
|
||||
Queues are the final aspect of Payload's Jobs Queue and deal with how to _run your jobs_. Up to this point, all we've covered is how to queue up jobs to run, but so far, we aren't actually running any jobs.
|
||||
|
||||
<Banner type="default">
|
||||
A <strong>Queue</strong> is a grouping of jobs that should be executed in order of when they were added.
|
||||
</Banner>
|
||||
|
||||
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
|
||||
// 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': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
**Vercel Cron Example**
|
||||
|
||||
If you're deploying on Vercel, you can add a `vercel.json` file in the root of your project that configures Vercel Cron to invoke the `run` endpoint on a cron schedule.
|
||||
|
||||
Here's an example of what this file will look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/payload-jobs/run",
|
||||
"schedule": "*/5 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The configuration above schedules the endpoint `/api/payload-jobs/run` to be invoked every 5 minutes.
|
||||
|
||||
The last step will be to secure your `run` endpoint so that only the proper users can invoke the runner.
|
||||
|
||||
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.
|
||||
|
||||
Then, you can modify the `access` function for running jobs by ensuring that only Vercel can invoke your runner.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
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}`;
|
||||
},
|
||||
},
|
||||
// Other job configurations...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
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 * * * *"
|
||||
```
|
||||
141
docs/jobs-queue/tasks.mdx
Normal file
141
docs/jobs-queue/tasks.mdx
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: Tasks
|
||||
label: Tasks
|
||||
order: 20
|
||||
desc: A Task is a distinct function declaration that can be run within Payload's Jobs Queue.
|
||||
keywords: jobs queue, application framework, typescript, node, react, nextjs
|
||||
---
|
||||
|
||||
<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](/docs/beta/jobs-queue/jobs) or [Workflows](/docs/beta/jobs-queue/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
|
||||
|
||||
Simply add a task to the `jobs.tasks` array in your Payload config. A task consists of the following fields:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `slug` | Define a slug-based name for this job. This slug needs to be unique among both tasks and workflows.|
|
||||
| `handler` | The function that should be responsible for running the job. You can either pass a string-based path to the job function file, or the job function itself. If you are using large dependencies within your job, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. |
|
||||
| `inputSchema` | Define the input field schema - payload will generate a type for this schema. |
|
||||
| `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this task. By default, this is "Task" + the capitalized task slug. |
|
||||
| `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 succeeds. |
|
||||
| `retries` | Specify the number of times that this step should be retried if it fails. |
|
||||
|
||||
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:
|
||||
|
||||
```ts
|
||||
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',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// These are the properties that the function should output
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'postID',
|
||||
type: 'text',
|
||||
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',
|
||||
req,
|
||||
data: {
|
||||
title: input.title,
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
postID: newPost.id,
|
||||
},
|
||||
}
|
||||
},
|
||||
} as TaskConfig<'createPost'>,
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
In general, this is an advanced use case. Here's how this would look:
|
||||
|
||||
`payload.config.ts:`
|
||||
|
||||
```ts
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
// ...
|
||||
// The #createPostHandler is a named export within the `createPost.ts` file
|
||||
handler: path.resolve(dirname, 'src/tasks/createPost.ts') + '#createPostHandler',
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Then, the `createPost` file itself:
|
||||
|
||||
`src/tasks/createPost.ts:`
|
||||
|
||||
```ts
|
||||
import type { TaskHandler } from 'payload'
|
||||
|
||||
export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job, req }) => {
|
||||
const newPost = await req.payload.create({
|
||||
collection: 'post',
|
||||
req,
|
||||
data: {
|
||||
title: input.title,
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
postID: newPost.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
150
docs/jobs-queue/workflows.mdx
Normal file
150
docs/jobs-queue/workflows.mdx
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Workflows
|
||||
label: Workflows
|
||||
order: 30
|
||||
desc: A Task is a distinct function declaration that can be run within Payload's Jobs Queue.
|
||||
keywords: jobs queue, application framework, typescript, node, react, nextjs
|
||||
---
|
||||
|
||||
<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>
|
||||
|
||||
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.
|
||||
|
||||
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**.
|
||||
|
||||
#### Defining a workflow
|
||||
|
||||
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.
|
||||
|
||||
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 |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `slug` | Define a slug-based name for this workflow. This slug needs to be unique among both tasks and workflows.|
|
||||
| `handler` | The function that should be responsible for running the workflow. You can either pass a string-based path to the workflow function file, or workflow job function itself. If you are using large dependencies within your workflow, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. |
|
||||
| `inputSchema` | Define the input field schema - payload will generate a type for this schema. |
|
||||
| `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this workflow. By default, this is "Workflow" + the capitalized workflow slug. |
|
||||
| `label` | Define a human-friendly label for this workflow. |
|
||||
| `queue` | Optionally, define the queue name that this workflow should be tied to. Defaults to "default". |
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// ...
|
||||
]
|
||||
workflows: [
|
||||
{
|
||||
slug: 'createPostAndUpdate',
|
||||
|
||||
// The arguments that the workflow will accept
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
// The handler that defines the "control flow" of the workflow
|
||||
// Notice how it uses the `tasks` argument to execute your predefined tasks.
|
||||
// These are strongly typed!
|
||||
handler: async ({ job, tasks }) => {
|
||||
|
||||
// This workflow first runs a task called `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. Here, we hard-code it to '1'
|
||||
const output = await tasks.createPost('1', {
|
||||
input: {
|
||||
title: job.input.title,
|
||||
},
|
||||
})
|
||||
|
||||
// Once the prior task completes, it will run a task
|
||||
// called `updatePost`
|
||||
await tasks.updatePost('2', {
|
||||
input: {
|
||||
post: job.taskStatus.createPost['1'].output.postID, // or output.postID
|
||||
title: job.input.title + '2',
|
||||
},
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'updatePost'>
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Running tasks inline
|
||||
|
||||
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.
|
||||
|
||||
To do this, you can use the `inlineTask` 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:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// ...
|
||||
]
|
||||
workflows: [
|
||||
{
|
||||
slug: 'createPostAndUpdate',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ job, tasks, inlineTask }) => {
|
||||
// Here, we run a predefined task.
|
||||
// The `createPost` handler arguments and return type
|
||||
// are both strongly typed
|
||||
const output = await tasks.createPost('1', {
|
||||
input: {
|
||||
title: job.input.title,
|
||||
},
|
||||
})
|
||||
|
||||
// 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 inlineTask('2', {
|
||||
task: async ({ req }) => {
|
||||
const newPost = await req.payload.update({
|
||||
collection: 'post',
|
||||
id: '2',
|
||||
req,
|
||||
retries: 3,
|
||||
data: {
|
||||
title: 'updated!',
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
newPost
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'updatePost'>
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -370,7 +370,12 @@ const yourEditorState: SerializedEditorState // <= your current editor state her
|
||||
|
||||
// Import editor state into your headless editor
|
||||
try {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
}
|
||||
@@ -382,8 +387,6 @@ headlessEditor.getEditorState().read(() => {
|
||||
})
|
||||
```
|
||||
|
||||
The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward.
|
||||
|
||||
## Lexical => Plain Text
|
||||
|
||||
Export content from the Lexical editor into plain text using these steps:
|
||||
@@ -401,8 +404,13 @@ const yourEditorState: SerializedEditorState // <= your current editor state her
|
||||
|
||||
// Import editor state into your headless editor
|
||||
try {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
|
||||
} catch (e) {
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
}
|
||||
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -173,7 +173,6 @@ import {
|
||||
useEntityVisibility,
|
||||
useField,
|
||||
useFieldComponents,
|
||||
useFieldProps,
|
||||
useForm,
|
||||
useFormFields,
|
||||
useFormInitializing,
|
||||
@@ -199,7 +198,6 @@ import {
|
||||
useSearchParams,
|
||||
useSelection,
|
||||
useStepNav,
|
||||
useTableCell,
|
||||
useTheme,
|
||||
useThrottledEffect,
|
||||
useTranslation,
|
||||
@@ -468,10 +466,9 @@ export const ServerRenderedDescription = () => <ClientRenderedDescription />
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { TextFieldDescriptionClientComponent } from 'payload'
|
||||
import { useFieldProps, useFormFields } from '@payloadcms/ui'
|
||||
import { useFormFields } from '@payloadcms/ui'
|
||||
|
||||
export const ClientRenderedDescription: TextFieldDescriptionClientComponent = () ={
|
||||
const { path } = useFieldProps()
|
||||
export const ClientRenderedDescription: TextFieldDescriptionClientComponent = ({ path }) => {
|
||||
const { value } = useFormFields(([fields]) => fields[path])
|
||||
const customDescription = `Component description: ${path} - ${value}`
|
||||
|
||||
@@ -526,10 +523,9 @@ export const ServerRenderedCollapsibleLabel = () => <ClientCollapsibleLabel />
|
||||
// file: components/ClientCollapsibleLabel.tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useFieldProps, useFormFields } from '@payloadcms/ui'
|
||||
import { useFormFields } from '@payloadcms/ui'
|
||||
|
||||
export const ClientCollapsibleLabel = () => {
|
||||
const { path } = useFieldProps()
|
||||
export const ClientCollapsibleLabel = ({ path }) => {
|
||||
const { value } = useFormFields(([fields]) => fields[path])
|
||||
const customLabel = `${value?.fieldInCollapsible || 'Untitled Collapsible'}`
|
||||
|
||||
@@ -537,63 +533,6 @@ export const ClientCollapsibleLabel = () => {
|
||||
}
|
||||
```
|
||||
|
||||
11. The `admin.components.Cell` no longer receives `rowData` or `cellData`.
|
||||
|
||||
If using a custom component, you must now get the data yourself via the `useTableCell` hook, i.e. `const { cellData, rowData } = useTableCell()`.
|
||||
|
||||
|
||||
```tsx
|
||||
// file: payload.config.ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { ServerRenderedTitleCell } from './components/ServerRenderedTitleCell.tsx'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
collections: [
|
||||
{
|
||||
slug: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: ServerRenderedTitleCell
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
// ...
|
||||
})
|
||||
|
||||
|
||||
// file: components/ServerRenderedTitleCell.tsx
|
||||
import React from 'react'
|
||||
|
||||
import { ClientTitleCell } from './ClientTitleCell.tsx'
|
||||
|
||||
export const ServerRenderedTitleCell = () => <ClientTitleCell />
|
||||
|
||||
|
||||
// file: components/ClientTitleCell.tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTableCell } from '@payloadcms/ui'
|
||||
|
||||
export const ClientTitleCell: CellComponent = () ={
|
||||
const { cellData, rowData } = useTableCell()
|
||||
const customCellText = `Component cell: ${cellData}`
|
||||
|
||||
return (
|
||||
<div>
|
||||
{customCellText}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
12. `admin.components.RowLabel` no longer accepts a function, instead use a custom component and make use of the `useRowLabel` hook:
|
||||
|
||||
```tsx
|
||||
|
||||
@@ -151,7 +151,7 @@ With the [REST API](../rest-api/overview), you can use the full power of Payload
|
||||
|
||||
To understand the syntax, you need to understand that complex URL search strings are parsed into a JSON object. This one isn't too bad, but more complex queries get unavoidably more difficult to write.
|
||||
|
||||
For this reason, we recommend to use the extremely helpful and ubiquitous [`qs`](https://www.npmjs.com/package/qs) package to parse your JSON / object-formatted queries into query strings:
|
||||
For this reason, we recommend to use the extremely helpful and ubiquitous [`qs-esm`](https://www.npmjs.com/package/qs-esm) package to parse your JSON / object-formatted queries into query strings:
|
||||
|
||||
```ts
|
||||
import { stringify } from 'qs-esm'
|
||||
|
||||
@@ -6,11 +6,13 @@ desc: Payload select determines which fields are selected to the result.
|
||||
keywords: query, documents, pagination, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
You may not need the full data from your Local API / REST queries, but only some specific fields. The select fields API can help you to optimize those cases.
|
||||
By default, Payload's APIs will return _all fields_ for a given collection or global. But, you may not need all of that data for all of your queries. Sometimes, you might want just a few fields from the response, which can speed up the Payload API and reduce the amount of JSON that is sent to you from the API.
|
||||
|
||||
This is where Payload's `select` feature comes in. Here, you can define exactly which fields you'd like to retrieve from the API.
|
||||
|
||||
## Local API
|
||||
|
||||
To specify select in the [Local API](../local-api/overview), you can use the `select` option in your query:
|
||||
To specify `select` in the [Local API](../local-api/overview), you can use the `select` option in your query:
|
||||
|
||||
```ts
|
||||
// Include mode
|
||||
@@ -51,7 +53,7 @@ const getPosts = async () => {
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
To perform querying with `select` efficiently, it works on the database level. Because of that, your `beforeRead` and `afterRead` hooks may not receive the full `doc`.
|
||||
To perform querying with `select` efficiently, Payload implements your `select` query on the database level. Because of that, your `beforeRead` and `afterRead` hooks may not receive the full `doc`.
|
||||
</Banner>
|
||||
|
||||
|
||||
@@ -67,7 +69,7 @@ fetch('https://localhost:3000/api/posts?select[color]=true&select[group][number]
|
||||
|
||||
To understand the syntax, you need to understand that complex URL search strings are parsed into a JSON object. This one isn't too bad, but more complex queries get unavoidably more difficult to write.
|
||||
|
||||
For this reason, we recommend to use the extremely helpful and ubiquitous [`qs`](https://www.npmjs.com/package/qs) package to parse your JSON / object-formatted queries into query strings:
|
||||
For this reason, we recommend to use the extremely helpful and ubiquitous [`qs-esm`](https://www.npmjs.com/package/qs-esm) package to parse your JSON / object-formatted queries into query strings:
|
||||
|
||||
```ts
|
||||
import { stringify } from 'qs-esm'
|
||||
@@ -100,11 +102,17 @@ const getPosts = async () => {
|
||||
</Banner>
|
||||
|
||||
|
||||
## `defaultPopulate` collection config property
|
||||
## defaultPopulate collection config property
|
||||
|
||||
The `defaultPopulate` property allows you specify which fields to select when populating the collection from another document.
|
||||
This is especially useful for links where only the `slug` is needed instead of the entire document.
|
||||
|
||||
With this feature, you can dramatically reduce the amount of JSON that is populated from [Relationship](/docs/beta/fields/relationship) or [Upload](/docs/beta/fields/upload) fields.
|
||||
|
||||
For example, in your content model, you might have a `Link` field which links out to a different page. When you go to retrieve these links, you really only need the `slug` of the page.
|
||||
|
||||
Loading all of the page content, its related links, and everything else is going to be overkill and will bog down your Payload APIs. Instead, you can define the `defaultPopulate` property on your `Pages` collection, so that when Payload "populates" a related Page, it only selects the `slug` field and therefore returns significantly less JSON:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
@@ -128,3 +136,35 @@ export const Pages: CollectionConfig<'pages'> = {
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## populate
|
||||
|
||||
Setting `defaultPopulate` will enforce that each time Payload performs a "population" of a related document, only the fields specified will be queried and returned. However, 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
|
||||
// Now, no matter what the `defaultPopulate` is set to on the "pages" collection,
|
||||
// it will be overridden, and the `text` field will be returned instead.
|
||||
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
|
||||
|
||||
|
||||
@@ -140,6 +140,23 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
### Custom filename via hooks
|
||||
|
||||
You can customize the filename before it's uploaded to the server by using a `beforeOperation` hook.
|
||||
|
||||
```ts
|
||||
beforeOperation: [
|
||||
({ req, operation }) => {
|
||||
if ((operation === 'create' || operation === 'update') && req.file) {
|
||||
req.file.name = 'test.jpg'
|
||||
}
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
The `req.file` object will have additional information about the file, such as mimeType and extension, and you also have full access to the file data itself.
|
||||
The filename from here will also be threaded to image sizes if they're enabled.
|
||||
|
||||
## Image Sizes
|
||||
|
||||
If you specify an array of `imageSizes` to your `upload` config, Payload will automatically crop and resize your uploads to fit each of the sizes specified by your config.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,7 @@ type Args = {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
import './custom.scss'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,7 @@ type Args = {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -20,7 +20,9 @@ export const Login = ({ tenantSlug }: Props) => {
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!usernameRef?.current?.value || !passwordRef?.current?.value) {return}
|
||||
if (!usernameRef?.current?.value || !passwordRef?.current?.value) {
|
||||
return
|
||||
}
|
||||
const actionRes = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users/external-users/login`,
|
||||
{
|
||||
|
||||
@@ -6,7 +6,9 @@ import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const ensureUniqueSlug: FieldHook = async ({ data, originalDoc, req, value }) => {
|
||||
// if value is unchanged, skip validation
|
||||
if (originalDoc.slug === value) {return value}
|
||||
if (originalDoc.slug === value) {
|
||||
return value
|
||||
}
|
||||
|
||||
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
|
||||
const currentTenantID =
|
||||
|
||||
@@ -14,7 +14,9 @@ export const Pages: CollectionConfig = {
|
||||
read: (args) => {
|
||||
// when viewing pages inside the admin panel
|
||||
// restrict access to the ones your user has access to
|
||||
if (isPayloadAdminPanel(args.req)) {return filterByTenantRead(args)}
|
||||
if (isPayloadAdminPanel(args.req)) {
|
||||
return filterByTenantRead(args)
|
||||
}
|
||||
|
||||
// when viewing pages from outside the admin panel
|
||||
// you should be able to see your tenants and public tenants
|
||||
|
||||
@@ -7,7 +7,9 @@ export const tenantRead: Access = (args) => {
|
||||
const req = args.req
|
||||
|
||||
// Super admin can read all
|
||||
if (isSuperAdmin(args)) {return true}
|
||||
if (isSuperAdmin(args)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const tenantIDs = getTenantAccessIDs(req.user)
|
||||
|
||||
|
||||
@@ -7,13 +7,19 @@ import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAcces
|
||||
|
||||
export const createAccess: Access<User> = (args) => {
|
||||
const { req } = args
|
||||
if (!req.user) {return false}
|
||||
if (!req.user) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isSuperAdmin(args)) {return true}
|
||||
if (isSuperAdmin(args)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const adminTenantAccessIDs = getTenantAdminTenantAccessIDs(req.user)
|
||||
|
||||
if (adminTenantAccessIDs.length > 0) {return true}
|
||||
if (adminTenantAccessIDs.length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const isAccessingSelf: Access = ({ id, req }) => {
|
||||
if (!req?.user) {return false}
|
||||
if (!req?.user) {
|
||||
return false
|
||||
}
|
||||
return req.user.id === id
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAcces
|
||||
|
||||
export const updateAndDeleteAccess: Access = (args) => {
|
||||
const { req } = args
|
||||
if (!req.user) {return false}
|
||||
if (!req.user) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isSuperAdmin(args)) {return true}
|
||||
if (isSuperAdmin(args)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const adminTenantAccessIDs = getTenantAdminTenantAccessIDs(req.user)
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import { getTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'
|
||||
|
||||
export const ensureUniqueUsername: FieldHook = async ({ data, originalDoc, req, value }) => {
|
||||
// if value is unchanged, skip validation
|
||||
if (originalDoc.username === value) {return value}
|
||||
if (originalDoc.username === value) {
|
||||
return value
|
||||
}
|
||||
|
||||
const incomingTenantID = typeof data?.tenant === 'object' ? data.tenant.id : data?.tenant
|
||||
const currentTenantID =
|
||||
|
||||
@@ -8,7 +8,9 @@ export const autofillTenant: FieldHook = ({ req, value }) => {
|
||||
// return that tenant ID as the value
|
||||
if (!value) {
|
||||
const tenantIDs = getTenantAccessIDs(req.user)
|
||||
if (tenantIDs.length === 1) {return tenantIDs[0]}
|
||||
if (tenantIDs.length === 1) {
|
||||
return tenantIDs[0]
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
|
||||
@@ -10,7 +10,9 @@ export const tenantField: Field = {
|
||||
access: {
|
||||
read: () => true,
|
||||
update: (args) => {
|
||||
if (isSuperAdmin(args)) {return true}
|
||||
if (isSuperAdmin(args)) {
|
||||
return true
|
||||
}
|
||||
return tenantFieldUpdate(args)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { User } from '../payload-types'
|
||||
|
||||
export const getTenantAccessIDs = (user: null | User): string[] => {
|
||||
if (!user) {return []}
|
||||
if (!user) {
|
||||
return []
|
||||
}
|
||||
return (
|
||||
user?.tenants?.reduce((acc: string[], { tenant }) => {
|
||||
if (tenant) {
|
||||
@@ -13,7 +15,9 @@ export const getTenantAccessIDs = (user: null | User): string[] => {
|
||||
}
|
||||
|
||||
export const getTenantAdminTenantAccessIDs = (user: null | User): string[] => {
|
||||
if (!user) {return []}
|
||||
if (!user) {
|
||||
return []
|
||||
}
|
||||
|
||||
return (
|
||||
user?.tenants?.reduce((acc: string[], { roles, tenant }) => {
|
||||
|
||||
@@ -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,6 @@
|
||||
(The MIT License)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
@@ -19,6 +19,11 @@ const config = withBundleAnalyzer(
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.124",
|
||||
"version": "3.0.0-beta.130",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -58,6 +58,8 @@
|
||||
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
||||
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
||||
"dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts",
|
||||
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod",
|
||||
"dev:prod:memorydb": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod --start-memory-db",
|
||||
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
@@ -69,8 +71,8 @@
|
||||
"lint:fix": "turbo run lint:fix --concurrency 1 --continue",
|
||||
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
"prepare": "husky",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
||||
@@ -132,8 +134,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 +164,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"
|
||||
},
|
||||
|
||||
22
packages/create-payload-app/license.md
Normal file
22
packages/create-payload-app/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.124",
|
||||
"version": "3.0.0-beta.130",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,6 +8,14 @@
|
||||
"directory": "packages/create-payload-app"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./types": {
|
||||
|
||||
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
|
||||
@@ -78,15 +78,14 @@ export async function createProject(args: {
|
||||
)
|
||||
await fse.copy(localTemplate, projectDir)
|
||||
} else if ('url' in template) {
|
||||
let templateUrl = template.url
|
||||
if (cliArgs['--template-branch']) {
|
||||
templateUrl = `${template.url}#${cliArgs['--template-branch']}`
|
||||
debug(`Using template url: ${templateUrl}`)
|
||||
template.url = `${template.url.split('#')?.[0]}#${cliArgs['--template-branch']}`
|
||||
}
|
||||
|
||||
await downloadTemplate({
|
||||
name: template.name,
|
||||
branch: 'beta',
|
||||
debug: cliArgs['--debug'],
|
||||
projectDir,
|
||||
template,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,7 +100,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)
|
||||
}
|
||||
|
||||
@@ -2,27 +2,35 @@ import { Readable } from 'node:stream'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { x } from 'tar'
|
||||
|
||||
import type { ProjectTemplate } from '../types.js'
|
||||
|
||||
import { debug as debugLog } from '../utils/log.js'
|
||||
|
||||
export async function downloadTemplate({
|
||||
name,
|
||||
branch,
|
||||
debug,
|
||||
projectDir,
|
||||
template,
|
||||
}: {
|
||||
branch: string
|
||||
/**
|
||||
* The name of the template to download
|
||||
* Must be dir /templates/<name>
|
||||
*/
|
||||
name: string
|
||||
debug?: boolean
|
||||
projectDir: string
|
||||
template: ProjectTemplate
|
||||
}) {
|
||||
const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${branch}`
|
||||
const filter = `payload-${branch}/templates/${name}/`
|
||||
const branchOrTag = template.url.split('#')?.[1] || 'beta'
|
||||
const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${branchOrTag}`
|
||||
const filter = `payload-${branchOrTag.replace(/^v/, '')}/templates/${template.name}/`
|
||||
|
||||
if (debug) {
|
||||
debugLog(`Using template url: ${template.url}`)
|
||||
debugLog(`Codeload url: ${url}`)
|
||||
debugLog(`Filter: ${filter}`)
|
||||
}
|
||||
|
||||
await pipeline(
|
||||
await downloadTarStream(url),
|
||||
x({
|
||||
cwd: projectDir,
|
||||
filter: (p) => p.includes(filter),
|
||||
strip: 2 + name.split('/').length,
|
||||
strip: 2 + template.name.split('/').length,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,13 +32,13 @@ type InitNextArgs = {
|
||||
} & Pick<CliArgs, '--debug'>
|
||||
|
||||
type InitNextResult =
|
||||
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
|
||||
| {
|
||||
isSrcDir: boolean
|
||||
nextAppDir: string
|
||||
payloadConfigPath: string
|
||||
success: true
|
||||
}
|
||||
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
|
||||
|
||||
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
|
||||
const { dbType: dbType, packageManager, projectDir } = args
|
||||
|
||||
@@ -15,15 +15,9 @@ export async function installPackages(args: {
|
||||
let stderr = ''
|
||||
|
||||
switch (packageManager) {
|
||||
case 'npm': {
|
||||
;({ exitCode, stderr } = await execa('npm', ['install', '--save', ...packagesToInstall], {
|
||||
cwd: projectDir,
|
||||
}))
|
||||
break
|
||||
}
|
||||
case 'yarn':
|
||||
case 'bun':
|
||||
case 'pnpm':
|
||||
case 'bun': {
|
||||
case 'yarn': {
|
||||
if (packageManager === 'bun') {
|
||||
warning('Bun support is untested.')
|
||||
}
|
||||
@@ -32,6 +26,12 @@ export async function installPackages(args: {
|
||||
}))
|
||||
break
|
||||
}
|
||||
case 'npm': {
|
||||
;({ exitCode, stderr } = await execa('npm', ['install', '--save', ...packagesToInstall], {
|
||||
cwd: projectDir,
|
||||
}))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (exitCode !== 0) {
|
||||
|
||||
@@ -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()
|
||||
@@ -13,51 +14,26 @@ export function validateTemplate(templateName: string): boolean {
|
||||
}
|
||||
|
||||
export function getValidTemplates(): ProjectTemplate[] {
|
||||
// Starters _must_ be a valid template name from the templates/ directory
|
||||
return [
|
||||
{
|
||||
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
|
||||
|
||||
// {
|
||||
// name: 'blank',
|
||||
// type: 'starter',
|
||||
// description: 'Blank Template',
|
||||
// url: 'https://github.com/payloadcms/payload/templates/blank',
|
||||
// },
|
||||
// {
|
||||
// name: 'ecommerce',
|
||||
// type: 'starter',
|
||||
// description: 'E-commerce Template',
|
||||
// url: 'https://github.com/payloadcms/payload/templates/ecommerce',
|
||||
// },
|
||||
// {
|
||||
// name: 'plugin',
|
||||
// type: 'plugin',
|
||||
// description: 'Template for creating a Payload plugin',
|
||||
// url: 'https://github.com/payloadcms/payload-plugin-template#beta',
|
||||
// },
|
||||
// {
|
||||
// name: 'payload-demo',
|
||||
// type: 'starter',
|
||||
// description: 'Payload demo site at https://demo.payloadcms.com',
|
||||
// url: 'https://github.com/payloadcms/public-demo',
|
||||
// },
|
||||
// {
|
||||
// name: 'payload-website',
|
||||
// type: 'starter',
|
||||
// description: 'Payload website CMS at https://payloadcms.com',
|
||||
// url: 'https://github.com/payloadcms/website-cms',
|
||||
// url: 'https://github.com/payloadcms/plugin-template#beta',
|
||||
// },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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: v${PACKAGE_VERSION}`)
|
||||
}
|
||||
|
||||
const validTemplates = getValidTemplates()
|
||||
const template = await parseTemplate(this.args, validTemplates)
|
||||
if (!template) {
|
||||
@@ -210,6 +217,16 @@ export class Main {
|
||||
}
|
||||
|
||||
switch (template.type) {
|
||||
case 'plugin': {
|
||||
await createProject({
|
||||
cliArgs: this.args,
|
||||
packageManager,
|
||||
projectDir,
|
||||
projectName,
|
||||
template,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'starter': {
|
||||
const dbDetails = await selectDb(this.args, projectName)
|
||||
const payloadSecret = generateSecret()
|
||||
@@ -231,16 +248,6 @@ export class Main {
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'plugin': {
|
||||
await createProject({
|
||||
cliArgs: this.args,
|
||||
packageManager,
|
||||
projectDir,
|
||||
projectName,
|
||||
template,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
info('Payload project successfully created!')
|
||||
|
||||
@@ -6,13 +6,16 @@ import path from 'path'
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function copyRecursiveSync(src: string, dest: string) {
|
||||
export function copyRecursiveSync(src: string, dest: string, ignoreRegex?: string[]): void {
|
||||
const exists = fs.existsSync(src)
|
||||
const stats = exists && fs.statSync(src)
|
||||
const isDirectory = exists && stats !== false && stats.isDirectory()
|
||||
if (isDirectory) {
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
fs.readdirSync(src).forEach((childItemName) => {
|
||||
if (ignoreRegex && ignoreRegex.some((regex) => new RegExp(regex).test(childItemName))) {
|
||||
return
|
||||
}
|
||||
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
|
||||
})
|
||||
} else {
|
||||
|
||||
22
packages/db-mongodb/license.md
Normal file
22
packages/db-mongodb/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.124",
|
||||
"version": "3.0.0-beta.130",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -10,6 +10,13 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -57,6 +57,21 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
}
|
||||
|
||||
if (this.ensureIndexes) {
|
||||
await Promise.all(
|
||||
this.payload.config.collections.map(async (coll) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.collections[coll.slug]?.ensureIndexes(function (err) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
|
||||
await this.migrate({ migrations: this.prodMigrations })
|
||||
}
|
||||
|
||||
48
packages/db-mongodb/src/countGlobalVersions.ts
Normal file
48
packages/db-mongodb/src/countGlobalVersions.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { CountGlobalVersions, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: MongooseAdapter,
|
||||
{ global, locale, req = {} as PayloadRequest, where },
|
||||
) {
|
||||
const Model = this.versions[global]
|
||||
const options: QueryOptions = await withSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
||||
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
|
||||
// the correct indexed field
|
||||
options.hint = {
|
||||
_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.countDocuments(query, options)
|
||||
|
||||
return {
|
||||
totalDocs: result,
|
||||
}
|
||||
}
|
||||
48
packages/db-mongodb/src/countVersions.ts
Normal file
48
packages/db-mongodb/src/countVersions.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { CountVersions, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, where },
|
||||
) {
|
||||
const Model = this.versions[collection]
|
||||
const options: QueryOptions = await withSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
||||
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
|
||||
// the correct indexed field
|
||||
options.hint = {
|
||||
_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.countDocuments(query, options)
|
||||
|
||||
return {
|
||||
totalDocs: result,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { CollectionModel, GlobalModel, MigrateDownArgs, MigrateUpArgs } fro
|
||||
|
||||
import { connect } from './connect.js'
|
||||
import { count } from './count.js'
|
||||
import { countGlobalVersions } from './countGlobalVersions.js'
|
||||
import { countVersions } from './countVersions.js'
|
||||
import { create } from './create.js'
|
||||
import { createGlobal } from './createGlobal.js'
|
||||
import { createGlobalVersion } from './createGlobalVersion.js'
|
||||
@@ -72,8 +74,14 @@ export interface Args {
|
||||
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
|
||||
useFacet?: boolean
|
||||
} & ConnectOptions
|
||||
|
||||
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
|
||||
disableIndexHints?: boolean
|
||||
/**
|
||||
* Set to `true` to ensure that indexes are ready before completing connection.
|
||||
* NOTE: not recommended for production. This can slow down the initialization of Payload.
|
||||
*/
|
||||
ensureIndexes?: boolean
|
||||
migrationDir?: string
|
||||
/**
|
||||
* typed as any to avoid dependency
|
||||
@@ -94,6 +102,7 @@ export type MongooseAdapter = {
|
||||
[slug: string]: CollectionModel
|
||||
}
|
||||
connection: Connection
|
||||
ensureIndexes: boolean
|
||||
globals: GlobalModel
|
||||
mongoMemoryServer: MongoMemoryReplSet
|
||||
prodMigrations?: {
|
||||
@@ -116,6 +125,7 @@ declare module 'payload' {
|
||||
[slug: string]: CollectionModel
|
||||
}
|
||||
connection: Connection
|
||||
ensureIndexes: boolean
|
||||
globals: GlobalModel
|
||||
mongoMemoryServer: MongoMemoryReplSet
|
||||
prodMigrations?: {
|
||||
@@ -136,6 +146,7 @@ export function mongooseAdapter({
|
||||
autoPluralization = true,
|
||||
connectOptions,
|
||||
disableIndexHints = false,
|
||||
ensureIndexes,
|
||||
migrationDir: migrationDirArg,
|
||||
mongoMemoryServer,
|
||||
prodMigrations,
|
||||
@@ -154,8 +165,8 @@ export function mongooseAdapter({
|
||||
collections: {},
|
||||
connection: undefined,
|
||||
connectOptions: connectOptions || {},
|
||||
count,
|
||||
disableIndexHints,
|
||||
ensureIndexes,
|
||||
globals: undefined,
|
||||
mongoMemoryServer,
|
||||
sessions: {},
|
||||
@@ -166,6 +177,9 @@ export function mongooseAdapter({
|
||||
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
||||
commitTransaction,
|
||||
connect,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
@@ -45,25 +45,28 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
versionSchema.plugin(mongooseAggregatePaginate)
|
||||
}
|
||||
|
||||
const model = mongoose.model(
|
||||
const versionCollectionName =
|
||||
this.autoPluralization === true && !collection.dbName ? undefined : versionModelName
|
||||
|
||||
this.versions[collection.slug] = mongoose.model(
|
||||
versionModelName,
|
||||
versionSchema,
|
||||
this.autoPluralization === true ? undefined : versionModelName,
|
||||
versionCollectionName,
|
||||
) as CollectionModel
|
||||
|
||||
this.versions[collection.slug] = model
|
||||
}
|
||||
|
||||
const model = mongoose.model(
|
||||
getDBName({ config: collection }),
|
||||
const modelName = getDBName({ config: collection })
|
||||
const collectionName =
|
||||
this.autoPluralization === true && !collection.dbName ? undefined : modelName
|
||||
|
||||
this.collections[collection.slug] = mongoose.model(
|
||||
modelName,
|
||||
schema,
|
||||
this.autoPluralization === true ? undefined : collection.slug,
|
||||
collectionName,
|
||||
) as CollectionModel
|
||||
this.collections[collection.slug] = model
|
||||
})
|
||||
|
||||
const model = buildGlobalModel(this.payload.config)
|
||||
this.globals = model
|
||||
this.globals = buildGlobalModel(this.payload.config)
|
||||
|
||||
this.payload.config.globals.forEach((global) => {
|
||||
if (global.versions) {
|
||||
@@ -85,12 +88,11 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
|
||||
.plugin(getBuildQueryPlugin({ versionsFields: versionGlobalFields }))
|
||||
|
||||
const versionsModel = mongoose.model(
|
||||
this.versions[global.slug] = mongoose.model(
|
||||
versionModelName,
|
||||
versionSchema,
|
||||
versionModelName,
|
||||
) as CollectionModel
|
||||
this.versions[global.slug] = versionsModel
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -353,6 +353,9 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['Point'],
|
||||
...(typeof field.defaultValue !== 'undefined' && {
|
||||
default: 'Point',
|
||||
}),
|
||||
},
|
||||
coordinates: {
|
||||
type: [Number],
|
||||
|
||||
@@ -89,11 +89,7 @@ export async function buildSearchParam({
|
||||
const [{ field, path }] = paths
|
||||
|
||||
if (path) {
|
||||
const {
|
||||
operator: formattedOperator,
|
||||
rawQuery,
|
||||
val: formattedValue,
|
||||
} = sanitizeQueryValue({
|
||||
const sanitizedQueryValue = sanitizeQueryValue({
|
||||
field,
|
||||
hasCustomID,
|
||||
operator,
|
||||
@@ -101,6 +97,12 @@ export async function buildSearchParam({
|
||||
val,
|
||||
})
|
||||
|
||||
if (!sanitizedQueryValue) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { operator: formattedOperator, rawQuery, val: formattedValue } = sanitizedQueryValue
|
||||
|
||||
if (rawQuery) {
|
||||
return { value: rawQuery }
|
||||
}
|
||||
|
||||
@@ -115,7 +115,6 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
projection,
|
||||
query: versionQuery,
|
||||
|
||||
@@ -58,7 +58,11 @@ export const buildJoinAggregation = async ({
|
||||
for (const join of joinConfig[slug]) {
|
||||
const joinModel = adapter.collections[join.field.collection]
|
||||
|
||||
if (projection && !projection[join.schemaPath]) {
|
||||
if (projection && !projection[join.joinPath]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (joins?.[join.joinPath] === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -66,7 +70,7 @@ export const buildJoinAggregation = async ({
|
||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||
where: whereJoin,
|
||||
} = joins?.[join.schemaPath] || {}
|
||||
} = joins?.[join.joinPath] || {}
|
||||
|
||||
const sort = buildSortParam({
|
||||
config: adapter.payload.config,
|
||||
@@ -99,7 +103,7 @@ export const buildJoinAggregation = async ({
|
||||
|
||||
if (adapter.payload.config.localization && locale === 'all') {
|
||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
||||
const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${code}`
|
||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
||||
|
||||
aggregate.push(
|
||||
{
|
||||
@@ -140,7 +144,7 @@ export const buildJoinAggregation = async ({
|
||||
} else {
|
||||
const localeSuffix =
|
||||
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
|
||||
const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${localeSuffix}`
|
||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${localeSuffix}`
|
||||
|
||||
aggregate.push(
|
||||
{
|
||||
|
||||
@@ -104,34 +104,10 @@ const traverseFields = ({
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'collapsible':
|
||||
case 'row':
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
fields: field.fields,
|
||||
projection,
|
||||
select,
|
||||
selectMode,
|
||||
withinLocalizedField,
|
||||
})
|
||||
break
|
||||
|
||||
case 'tabs':
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
projection,
|
||||
select,
|
||||
selectMode,
|
||||
withinLocalizedField,
|
||||
})
|
||||
break
|
||||
|
||||
case 'array':
|
||||
case 'group':
|
||||
case 'tab':
|
||||
case 'array': {
|
||||
|
||||
case 'tab': {
|
||||
let fieldSelect: SelectType
|
||||
|
||||
if (field.type === 'tab' && !tabHasName(field)) {
|
||||
@@ -206,6 +182,30 @@ const traverseFields = ({
|
||||
|
||||
break
|
||||
}
|
||||
case 'collapsible':
|
||||
case 'row':
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
fields: field.fields,
|
||||
projection,
|
||||
select,
|
||||
selectMode,
|
||||
withinLocalizedField,
|
||||
})
|
||||
break
|
||||
|
||||
case 'tabs':
|
||||
traverseFields({
|
||||
adapter,
|
||||
databaseSchemaPath,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
projection,
|
||||
select,
|
||||
selectMode,
|
||||
withinLocalizedField,
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -19,8 +19,8 @@ export const handleError = ({
|
||||
collection,
|
||||
errors: [
|
||||
{
|
||||
field: Object.keys(error.keyValue)[0],
|
||||
message: req.t('error:valueMustBeUnique'),
|
||||
path: Object.keys(error.keyValue)[0],
|
||||
},
|
||||
],
|
||||
global,
|
||||
|
||||
22
packages/db-postgres/license.md
Normal file
22
packages/db-postgres/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.124",
|
||||
"version": "3.0.0-beta.130",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -10,6 +10,13 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -49,9 +56,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' &&
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -33,9 +35,9 @@ import {
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
convertPathToJSONTraversal,
|
||||
countDistinct,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
defaultDrizzleSnapshot,
|
||||
@@ -48,12 +50,17 @@ import {
|
||||
requireDrizzleKit,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import path from 'path'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Args, PostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
|
||||
@@ -75,15 +82,25 @@ 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,
|
||||
createMigration(args) {
|
||||
return createMigration.bind(this)({ ...args, dirname })
|
||||
},
|
||||
defaultDrizzleSnapshot,
|
||||
disableCreateDatabase: args.disableCreateDatabase ?? false,
|
||||
drizzle: undefined,
|
||||
enums: {},
|
||||
extensions,
|
||||
features: {
|
||||
json: true,
|
||||
},
|
||||
@@ -106,6 +123,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',
|
||||
|
||||
@@ -114,14 +132,14 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
args.transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
||||
commitTransaction,
|
||||
connect,
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
createVersion,
|
||||
defaultIDType: payloadIDType,
|
||||
deleteMany,
|
||||
|
||||
@@ -13,44 +13,6 @@ type Args = {
|
||||
export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
const newPath = `${path ? `${path}.` : ''}${field.name}`
|
||||
const newDoc = doc?.[field.name]
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (field.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: field.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
return traverseFields({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const rowData = doc?.[field.name]
|
||||
|
||||
@@ -124,45 +86,47 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
return field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
const newDoc = doc?.[tab.name]
|
||||
const newPath = `${path ? `${path}.` : ''}${tab.name}`
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (tab.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: tab.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: tab.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traverseFields({
|
||||
doc,
|
||||
fields: tab.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
case 'collapsible':
|
||||
// falls through
|
||||
case 'row': {
|
||||
return traverseFields({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
const newPath = `${path ? `${path}.` : ''}${field.name}`
|
||||
const newDoc = doc?.[field.name]
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (field.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: field.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
// falls through
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
@@ -211,6 +175,43 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'tabs': {
|
||||
return field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
const newDoc = doc?.[tab.name]
|
||||
const newPath = `${path ? `${path}.` : ''}${tab.name}`
|
||||
|
||||
if (typeof newDoc === 'object' && newDoc !== null) {
|
||||
if (tab.localized) {
|
||||
Object.entries(newDoc).forEach(([locale, localeDoc]) => {
|
||||
return traverseFields({
|
||||
doc: localeDoc,
|
||||
fields: tab.fields,
|
||||
locale,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return traverseFields({
|
||||
doc: newDoc as Record<string, unknown>,
|
||||
fields: tab.fields,
|
||||
path: newPath,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traverseFields({
|
||||
doc,
|
||||
fields: tab.fields,
|
||||
path,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,30 +27,6 @@ type Args = {
|
||||
export const traverseFields = (args: Args) => {
|
||||
args.fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
|
||||
|
||||
if (field.localized && args.payload.config.localization) {
|
||||
newTableName += args.adapter.localesSuffix
|
||||
}
|
||||
|
||||
return traverseFields({
|
||||
...args,
|
||||
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
|
||||
fields: field.fields,
|
||||
newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
|
||||
})
|
||||
}
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
return traverseFields({
|
||||
...args,
|
||||
fields: field.fields,
|
||||
})
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const newTableName = args.adapter.tableNameMap.get(
|
||||
`${args.newTableName}_${toSnakeCase(field.name)}`,
|
||||
@@ -82,7 +58,42 @@ export const traverseFields = (args: Args) => {
|
||||
})
|
||||
})
|
||||
}
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
return traverseFields({
|
||||
...args,
|
||||
fields: field.fields,
|
||||
})
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
let newTableName = `${args.newTableName}_${toSnakeCase(field.name)}`
|
||||
|
||||
if (field.localized && args.payload.config.localization) {
|
||||
newTableName += args.adapter.localesSuffix
|
||||
}
|
||||
|
||||
return traverseFields({
|
||||
...args,
|
||||
columnPrefix: `${args.columnPrefix}${toSnakeCase(field.name)}_`,
|
||||
fields: field.fields,
|
||||
newTableName,
|
||||
path: `${args.path ? `${args.path}.` : ''}${field.name}`,
|
||||
})
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
case 'tabs': {
|
||||
return field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
@@ -101,17 +112,6 @@ export const traverseFields = (args: Args) => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (typeof field.relationTo === 'string') {
|
||||
if (field.type === 'upload' || !field.hasMany) {
|
||||
args.pathsToQuery.add(`${args.path ? `${args.path}.` : ''}${field.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
22
packages/db-sqlite/license.md
Normal file
22
packages/db-sqlite/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.124",
|
||||
"version": "3.0.0-beta.130",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -10,6 +10,13 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -47,9 +54,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"
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -114,6 +116,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
@@ -179,183 +179,6 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'text': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyTextField = 'index'
|
||||
} else if (!hasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in SQLite for hasMany text fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(text(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'email':
|
||||
case 'code':
|
||||
case 'textarea': {
|
||||
targetTable[fieldName] = withDefault(text(columnName), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyNumberField = 'index'
|
||||
} else if (!hasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in Postgres for hasMany number fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(numeric(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText':
|
||||
case 'json': {
|
||||
targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'date': {
|
||||
targetTable[fieldName] = withDefault(text(columnName), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
break
|
||||
}
|
||||
|
||||
case 'radio':
|
||||
case 'select': {
|
||||
const options = field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return option.value
|
||||
}
|
||||
|
||||
return option
|
||||
}) as [string, ...string[]]
|
||||
|
||||
if (field.type === 'select' && field.hasMany) {
|
||||
const selectTableName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `${newTableName}_`,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
const baseColumns: Record<string, SQLiteColumnBuilder> = {
|
||||
order: integer('order').notNull(),
|
||||
parent: getIDColumn({
|
||||
name: 'parent_id',
|
||||
type: parentIDColType,
|
||||
notNull: true,
|
||||
primaryKey: false,
|
||||
}),
|
||||
value: text('value', { enum: options }),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
|
||||
parentFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${selectTableName}_parent_fk`,
|
||||
columns: [cols.parent],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns.locale = text('locale', { enum: locales }).notNull()
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fields: [],
|
||||
rootTableName,
|
||||
tableName: selectTableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// selects have their own localized table, independent of the base table.
|
||||
localized: false,
|
||||
target: selectTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${selectTableName}`] = relations(
|
||||
adapter.tables[selectTableName],
|
||||
({ one }) => ({
|
||||
parent: one(adapter.tables[parentTableName], {
|
||||
fields: [adapter.tables[selectTableName].parent],
|
||||
references: [adapter.tables[parentTableName].id],
|
||||
relationName: fieldName,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(
|
||||
text(columnName, {
|
||||
enum: options,
|
||||
}),
|
||||
field,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'checkbox': {
|
||||
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
@@ -493,7 +316,6 @@ export const traverseFields = ({
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
@@ -646,9 +468,82 @@ export const traverseFields = ({
|
||||
|
||||
break
|
||||
}
|
||||
case 'checkbox': {
|
||||
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
|
||||
break
|
||||
}
|
||||
case 'code':
|
||||
|
||||
case 'tab':
|
||||
case 'group': {
|
||||
case 'email':
|
||||
|
||||
case 'textarea': {
|
||||
targetTable[fieldName] = withDefault(text(columnName), field)
|
||||
break
|
||||
}
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
hasLocalizedField: rowHasLocalizedField,
|
||||
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
|
||||
hasManyNumberField: rowHasManyNumberField,
|
||||
hasManyTextField: rowHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
locales,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (rowHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (rowHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (rowHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (rowHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (rowHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (rowHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'date': {
|
||||
targetTable[fieldName] = withDefault(text(columnName), field)
|
||||
break
|
||||
}
|
||||
|
||||
case 'group':
|
||||
case 'tab': {
|
||||
if (!('name' in field)) {
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
@@ -758,114 +653,136 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
case 'json':
|
||||
|
||||
const {
|
||||
hasLocalizedField: tabHasLocalizedField,
|
||||
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
|
||||
hasManyNumberField: tabHasManyNumberField,
|
||||
hasManyTextField: tabHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
forceLocalized,
|
||||
indexes,
|
||||
locales,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
case 'richText': {
|
||||
targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
|
||||
break
|
||||
}
|
||||
|
||||
if (tabHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (tabHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (tabHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (tabHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (tabHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (tabHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
case 'number': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyNumberField = 'index'
|
||||
} else if (!hasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in Postgres for hasMany number fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(numeric(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
hasLocalizedField: rowHasLocalizedField,
|
||||
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
|
||||
hasManyNumberField: rowHasManyNumberField,
|
||||
hasManyTextField: rowHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
forceLocalized,
|
||||
indexes,
|
||||
locales,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
case 'point': {
|
||||
break
|
||||
}
|
||||
case 'radio':
|
||||
|
||||
if (rowHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (rowHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (rowHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (rowHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (rowHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (rowHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
case 'select': {
|
||||
const options = field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
return option.value
|
||||
}
|
||||
|
||||
return option
|
||||
}) as [string, ...string[]]
|
||||
|
||||
if (field.type === 'select' && field.hasMany) {
|
||||
const selectTableName = createTableName({
|
||||
adapter,
|
||||
config: field,
|
||||
parentTableName: newTableName,
|
||||
prefix: `${newTableName}_`,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
const baseColumns: Record<string, SQLiteColumnBuilder> = {
|
||||
order: integer('order').notNull(),
|
||||
parent: getIDColumn({
|
||||
name: 'parent_id',
|
||||
type: parentIDColType,
|
||||
notNull: true,
|
||||
primaryKey: false,
|
||||
}),
|
||||
value: text('value', { enum: options }),
|
||||
}
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {
|
||||
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
|
||||
parentFk: (cols) =>
|
||||
foreignKey({
|
||||
name: `${selectTableName}_parent_fk`,
|
||||
columns: [cols.parent],
|
||||
foreignColumns: [adapter.tables[parentTableName].id],
|
||||
}).onDelete('cascade'),
|
||||
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
|
||||
}
|
||||
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
baseColumns.locale = text('locale', { enum: locales }).notNull()
|
||||
baseExtraConfig.localeIdx = (cols) =>
|
||||
index(`${selectTableName}_locale_idx`).on(cols.locale)
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
baseExtraConfig.value = (cols) => index(`${selectTableName}_value_idx`).on(cols.value)
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fields: [],
|
||||
rootTableName,
|
||||
tableName: selectTableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
relationsToBuild.set(fieldName, {
|
||||
type: 'many',
|
||||
// selects have their own localized table, independent of the base table.
|
||||
localized: false,
|
||||
target: selectTableName,
|
||||
})
|
||||
|
||||
adapter.relations[`relations_${selectTableName}`] = relations(
|
||||
adapter.tables[selectTableName],
|
||||
({ one }) => ({
|
||||
parent: one(adapter.tables[parentTableName], {
|
||||
fields: [adapter.tables[selectTableName].parent],
|
||||
references: [adapter.tables[parentTableName].id],
|
||||
relationName: fieldName,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(
|
||||
text(columnName, {
|
||||
enum: options,
|
||||
}),
|
||||
field,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -931,6 +848,89 @@ export const traverseFields = ({
|
||||
|
||||
break
|
||||
|
||||
case 'tabs': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: tabHasLocalizedField,
|
||||
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
|
||||
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
|
||||
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
|
||||
hasManyNumberField: tabHasManyNumberField,
|
||||
hasManyTextField: tabHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
forceLocalized,
|
||||
indexes,
|
||||
locales,
|
||||
localesColumns,
|
||||
localesIndexes,
|
||||
newTableName,
|
||||
parentTableName,
|
||||
relationships,
|
||||
relationsToBuild,
|
||||
rootRelationsToBuild,
|
||||
rootTableIDColType,
|
||||
rootTableName,
|
||||
uniqueRelationships,
|
||||
versions,
|
||||
withinLocalizedArrayOrBlock,
|
||||
})
|
||||
|
||||
if (tabHasLocalizedField) {
|
||||
hasLocalizedField = true
|
||||
}
|
||||
if (tabHasLocalizedRelationshipField) {
|
||||
hasLocalizedRelationshipField = true
|
||||
}
|
||||
if (tabHasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
if (tabHasLocalizedManyTextField) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
if (tabHasManyNumberField) {
|
||||
hasManyNumberField = true
|
||||
}
|
||||
if (tabHasLocalizedManyNumberField) {
|
||||
hasLocalizedManyNumberField = true
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'text': {
|
||||
if (field.hasMany) {
|
||||
const isLocalized =
|
||||
Boolean(field.localized && adapter.payload.config.localization) ||
|
||||
withinLocalizedArrayOrBlock ||
|
||||
forceLocalized
|
||||
|
||||
if (isLocalized) {
|
||||
hasLocalizedManyTextField = true
|
||||
}
|
||||
|
||||
if (field.index) {
|
||||
hasManyTextField = 'index'
|
||||
} else if (!hasManyTextField) {
|
||||
hasManyTextField = true
|
||||
}
|
||||
|
||||
if (field.unique) {
|
||||
throw new InvalidConfiguration(
|
||||
'Unique is not supported in SQLite for hasMany text fields.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
targetTable[fieldName] = withDefault(text(columnName), field)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
22
packages/db-vercel-postgres/license.md
Normal file
22
packages/db-vercel-postgres/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.0.0-beta.124",
|
||||
"version": "3.0.0-beta.130",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -10,6 +10,13 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -49,9 +56,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' &&
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -33,9 +35,9 @@ import {
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
import {
|
||||
convertPathToJSONTraversal,
|
||||
countDistinct,
|
||||
createDatabase,
|
||||
createExtensions,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
defaultDrizzleSnapshot,
|
||||
@@ -48,12 +50,17 @@ import {
|
||||
requireDrizzleKit,
|
||||
} from '@payloadcms/drizzle/postgres'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import path from 'path'
|
||||
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { Args, VercelPostgresAdapter } from './types.js'
|
||||
|
||||
import { connect } from './connect.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<VercelPostgresAdapter> {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
|
||||
@@ -75,15 +82,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 +121,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',
|
||||
|
||||
@@ -115,14 +130,17 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
args.transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
||||
commitTransaction,
|
||||
connect,
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
createJSONQuery,
|
||||
createMigration,
|
||||
createMigration(args) {
|
||||
return createMigration.bind(this)({ ...args, dirname })
|
||||
},
|
||||
createVersion,
|
||||
defaultIDType: payloadIDType,
|
||||
deleteMany,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user