Compare commits
119 Commits
templates/
...
feat/sched
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af94901a3 | ||
|
|
d6f44f9c94 | ||
|
|
a7109ed048 | ||
|
|
dec87e971a | ||
|
|
a501e604d6 | ||
|
|
6bba7bec4e | ||
|
|
2a6bac97f5 | ||
|
|
52b1a9a720 | ||
|
|
7bedd6d822 | ||
|
|
59fc9d094e | ||
|
|
7c4ea5b86e | ||
|
|
7292220109 | ||
|
|
dd3c2eb42b | ||
|
|
b3308736c4 | ||
|
|
46e50c4572 | ||
|
|
d03658de01 | ||
|
|
07be617963 | ||
|
|
d8c106cb2b | ||
|
|
e468292039 | ||
|
|
034b442699 | ||
|
|
6a8aecadf8 | ||
|
|
23f1ed4a48 | ||
|
|
ba0e7aeee5 | ||
|
|
5753efb0a4 | ||
|
|
12dad35cf9 | ||
|
|
997aed346f | ||
|
|
0c57eef621 | ||
|
|
1d46b6d738 | ||
|
|
03ff77544e | ||
|
|
0e5bda9a74 | ||
|
|
eee6432715 | ||
|
|
044c22de54 | ||
|
|
ce74f1b238 | ||
|
|
605cf42cbe | ||
|
|
97c120ab28 | ||
|
|
97a1f4afa9 | ||
|
|
439dd04ce9 | ||
|
|
a3457af36d | ||
|
|
d0d7b51ed5 | ||
|
|
ef90ebb395 | ||
|
|
194a8c189a | ||
|
|
1446fe4694 | ||
|
|
f29e6335f6 | ||
|
|
93dde52fa9 | ||
|
|
198763a24e | ||
|
|
b8de401195 | ||
|
|
2ee3e30b50 | ||
|
|
61c5e0d3e0 | ||
|
|
4bfa329fa4 | ||
|
|
f5c13deb24 | ||
|
|
70666a0f7b | ||
|
|
b0b2fc6c47 | ||
|
|
eb037a0cc6 | ||
|
|
99ca1babc6 | ||
|
|
13e050582b | ||
|
|
77871050b8 | ||
|
|
e04be4bf62 | ||
|
|
7037983de0 | ||
|
|
29ad1fcb77 | ||
|
|
2d2a52b00f | ||
|
|
3f35d36934 | ||
|
|
41167bfbeb | ||
|
|
ed44ec0a9c | ||
|
|
fa49e04cf8 | ||
|
|
f54e180370 | ||
|
|
c50f4237a4 | ||
|
|
b0d648bf30 | ||
|
|
8258d5c943 | ||
|
|
0f63db055b | ||
|
|
12fa4fd2f9 | ||
|
|
6dea111d28 | ||
|
|
26a10ed071 | ||
|
|
727fba7b1c | ||
|
|
c187bff581 | ||
|
|
00909ec5c4 | ||
|
|
2ec4d0c2ef | ||
|
|
f5516b96da | ||
|
|
050ff8409c | ||
|
|
4dc50030b6 | ||
|
|
e073183ea8 | ||
|
|
c2adf38593 | ||
|
|
36e21f182a | ||
|
|
c1673652a8 | ||
|
|
1d6a9358d9 | ||
|
|
f48f9810a0 | ||
|
|
1502e09581 | ||
|
|
33d5482e9d | ||
|
|
4c3e41beb1 | ||
|
|
50e7c24b17 | ||
|
|
796df37461 | ||
|
|
9c8cdea4b3 | ||
|
|
4334940755 | ||
|
|
b101feca7a | ||
|
|
0d07ce22e8 | ||
|
|
a582431a36 | ||
|
|
7a8b46484b | ||
|
|
d57cad632d | ||
|
|
7e3fd5d76c | ||
|
|
abee24e1d0 | ||
|
|
0d8643a9a3 | ||
|
|
d78550c561 | ||
|
|
c7272bb2bf | ||
|
|
9eb1b508f6 | ||
|
|
6fffbdb27a | ||
|
|
c298cbc90d | ||
|
|
5af71fb8d0 | ||
|
|
d4d79c1141 | ||
|
|
9d324ff207 | ||
|
|
fffab668c9 | ||
|
|
bae2fe535e | ||
|
|
c8046cade7 | ||
|
|
5e3963482e | ||
|
|
d9efd192e7 | ||
|
|
23e2f7bc9e | ||
|
|
4c57df69ca | ||
|
|
6a09fe1bf9 | ||
|
|
821bd35578 | ||
|
|
afa08d0ebf | ||
|
|
d97d7eda37 |
28
.github/workflows/main.yml
vendored
28
.github/workflows/main.yml
vendored
@@ -62,8 +62,12 @@ jobs:
|
||||
echo "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
lint:
|
||||
# Follows same github's ci skip: [skip lint], [lint skip], [no lint]
|
||||
if: >
|
||||
github.event_name == 'pull_request' && !contains(github.event.pull_request.title, 'no-lint') && !contains(github.event.pull_request.title, 'skip-lint')
|
||||
github.event_name == 'pull_request' &&
|
||||
!contains(github.event.pull_request.title, '[skip lint]') &&
|
||||
!contains(github.event.pull_request.title, '[lint skip]') &&
|
||||
!contains(github.event.pull_request.title, '[no lint]')
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -176,6 +180,7 @@ jobs:
|
||||
- postgres-uuid
|
||||
- supabase
|
||||
- sqlite
|
||||
- sqlite-uuid
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
@@ -279,20 +284,31 @@ jobs:
|
||||
- admin-root
|
||||
- auth
|
||||
- auth-basic
|
||||
- joins
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
- fields
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Array
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Collapsible
|
||||
- fields__collections__ConditionalLogic
|
||||
- fields__collections__CustomID
|
||||
- fields__collections__Date
|
||||
- fields__collections__Email
|
||||
- fields__collections__Indexed
|
||||
- fields__collections__JSON
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Date
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Radio
|
||||
- fields__collections__Relationship
|
||||
- fields__collections__RichText
|
||||
- fields__collections__Row
|
||||
- fields__collections__Select
|
||||
- fields__collections__Tabs
|
||||
- fields__collections__Tabs2
|
||||
- fields__collections__Text
|
||||
- fields__collections__UI
|
||||
- fields__collections__Upload
|
||||
- live-preview
|
||||
- localization
|
||||
|
||||
56
.github/workflows/post-release-templates.yml
vendored
56
.github/workflows/post-release-templates.yml
vendored
@@ -13,7 +13,39 @@ env:
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
wait_for_release:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
sparse-checkout: .github/workflows
|
||||
|
||||
- name: Determine Release Tag
|
||||
id: determine_tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
echo "Using tag from release event: ${{ github.event.release.tag_name }}"
|
||||
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# pull latest tag from github, must match any version except v2. Should match v3, v4, v99, etc.
|
||||
echo "Fetching latest tag from github..."
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude 'v2*')
|
||||
echo "Latest tag: $LATEST_TAG"
|
||||
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Wait until latest versions resolve on npm registry
|
||||
run: |
|
||||
./.github/workflows/wait-until-package-version.sh payload ${{ steps.determine_tag.outputs.release_tag }}
|
||||
./.github/workflows/wait-until-package-version.sh @payloadcms/translations ${{ steps.determine_tag.outputs.release_tag }}
|
||||
./.github/workflows/wait-until-package-version.sh @payloadcms/next ${{ steps.determine_tag.outputs.release_tag }}
|
||||
|
||||
update_templates:
|
||||
needs: wait_for_release
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -25,8 +57,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Needed for tags
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
@@ -60,20 +90,6 @@ jobs:
|
||||
- name: Update template lockfiles and migrations
|
||||
run: pnpm script:gen-templates
|
||||
|
||||
- name: Determine Release Tag
|
||||
id: determine_tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
echo "Using tag from release event: ${{ github.event.release.tag_name }}"
|
||||
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# pull latest tag from github, must match any version except v2. Should match v3, v4, v99, etc.
|
||||
echo "Fetching latest tag from github..."
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude 'v2*')
|
||||
echo "Latest tag: $LATEST_TAG"
|
||||
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
id: commit
|
||||
env:
|
||||
@@ -85,7 +101,7 @@ jobs:
|
||||
|
||||
git diff --name-only
|
||||
|
||||
export BRANCH_NAME=templates/bump-${{ steps.determine_tag.outputs.release_tag }}-$(date +%s)
|
||||
export BRANCH_NAME=templates/bump-${{ needs.wait_for_release.outputs.release_tag }}-$(date +%s)
|
||||
echo "branch=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create pull request
|
||||
@@ -94,12 +110,12 @@ jobs:
|
||||
token: ${{ secrets.GH_TOKEN_POST_RELEASE_TEMPLATES }}
|
||||
labels: 'area: templates'
|
||||
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
commit-message: 'templates: bump templates for ${{ steps.determine_tag.outputs.release_tag }}'
|
||||
commit-message: 'templates: bump templates for ${{ needs.wait_for_release.outputs.release_tag }}'
|
||||
branch: ${{ steps.commit.outputs.branch }}
|
||||
base: main
|
||||
assignees: ${{ github.actor }}
|
||||
title: 'templates: bump for ${{ steps.determine_tag.outputs.release_tag }}'
|
||||
title: 'templates: bump for ${{ needs.wait_for_release.outputs.release_tag }}'
|
||||
body: |
|
||||
🤖 Automated bump of templates for ${{ steps.determine_tag.outputs.release_tag }}
|
||||
🤖 Automated bump of templates for ${{ needs.wait_for_release.outputs.release_tag }}
|
||||
|
||||
Triggered by user: @${{ github.actor }}
|
||||
|
||||
2
.github/workflows/pr-title.yml
vendored
2
.github/workflows/pr-title.yml
vendored
@@ -41,7 +41,9 @@ jobs:
|
||||
db-vercel-postgres
|
||||
db-sqlite
|
||||
drizzle
|
||||
email-\*
|
||||
email-nodemailer
|
||||
email-resend
|
||||
eslint
|
||||
graphql
|
||||
live-preview
|
||||
|
||||
31
.github/workflows/wait-until-package-version.sh
vendored
Executable file
31
.github/workflows/wait-until-package-version.sh
vendored
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$#" -ne 2 ]]; then
|
||||
echo "Usage: $0 <package-name> <version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGE_NAME="$1"
|
||||
TARGET_VERSION=${2#v} # Git tag has leading 'v', npm version does not
|
||||
TIMEOUT=300 # 5 minutes in seconds
|
||||
INTERVAL=10 # 10 seconds
|
||||
ELAPSED=0
|
||||
|
||||
echo "Waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve... (timeout: ${TIMEOUT} seconds)"
|
||||
|
||||
while [[ ${ELAPSED} -lt ${TIMEOUT} ]]; do
|
||||
latest_version=$(npm show "${PACKAGE_NAME}" version 2>/dev/null)
|
||||
|
||||
if [[ ${latest_version} == "${TARGET_VERSION}" ]]; then
|
||||
echo "SUCCCESS: Version ${TARGET_VERSION} of ${PACKAGE_NAME} is available."
|
||||
exit 0
|
||||
else
|
||||
echo "Version ${TARGET_VERSION} of ${PACKAGE_NAME} is not available yet. Retrying in ${INTERVAL} seconds... (elapsed: ${ELAPSED}s)"
|
||||
fi
|
||||
|
||||
sleep "${INTERVAL}"
|
||||
ELAPSED=$((ELAPSED + INTERVAL))
|
||||
done
|
||||
|
||||
echo "Timed out after ${TIMEOUT} seconds waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve."
|
||||
exit 1
|
||||
@@ -98,7 +98,7 @@ The following options are available:
|
||||
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root <html> tag. Defaults to `false`. |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ import type { CollectionConfig } from 'payload'
|
||||
export const UsersWithoutJWTs: CollectionConfig = {
|
||||
slug: 'users-without-jwts',
|
||||
auth: {
|
||||
removeTokenFromRepsonse: true, // highlight-line
|
||||
removeTokenFromResponse: true, // highlight-line
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,14 +30,15 @@ export default buildConfig({
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ----------- |
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
| Option | Description |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
|
||||
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
|
||||
## Access to Mongoose models
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Note:</strong>
|
||||
If when using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to adapter's 'args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
|
||||
</Banner>
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
@@ -60,21 +65,33 @@ export default buildConfig({
|
||||
| `schemaName` (experimental) | A string for the postgres schema to use, defaults to 'public'. |
|
||||
| `idType` | A string of 'serial', or 'uuid' that is used for the data type given to id columns. |
|
||||
| `transactionOptions` | A PgTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
|
||||
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
|
||||
| `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. |
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
|
||||
|
||||
You can access Drizzle as follows:
|
||||
To ensure type-safety, you need to generate Drizzle schema first with:
|
||||
```sh
|
||||
npx payload generate:db-schema
|
||||
```
|
||||
|
||||
```text
|
||||
payload.db.drizzle
|
||||
Then, you can access Drizzle as follows:
|
||||
```ts
|
||||
import { posts } from './payload-generated-schema'
|
||||
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
|
||||
import { eq, sql, and } from '@payloadcms/db-postgres/drizzle'
|
||||
|
||||
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
|
||||
const posts = await payload.db.drizzle.query.posts.findMany()
|
||||
// Drizzle's Select API https://orm.drizzle.team/docs/select
|
||||
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
|
||||
```
|
||||
|
||||
## Tables, relations, and enums
|
||||
@@ -109,7 +126,7 @@ Runs before the schema is built. You can use this hook to extend your database s
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'
|
||||
import { integer, pgTable, serial } from '@payloadcms/db-postgres/drizzle/pg-core'
|
||||
|
||||
postgresAdapter({
|
||||
beforeSchemaInit: [
|
||||
@@ -178,7 +195,7 @@ postgresAdapter({
|
||||
})
|
||||
```
|
||||
|
||||
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
|
||||
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
|
||||
|
||||
|
||||
### afterSchemaInit
|
||||
@@ -189,7 +206,7 @@ The following example adds the `extra_integer_column` column and a composite ind
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { index, integer } from 'drizzle-orm/pg-core'
|
||||
import { index, integer } from '@payloadcms/db-postgres/drizzle/pg-core'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
@@ -231,3 +248,43 @@ export default buildConfig({
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
### Note for generated schema:
|
||||
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
|
||||
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
|
||||
|
||||
```ts
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
postgresAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
// Add a new table
|
||||
schema.rawTables.myTable = {
|
||||
name: 'my_table',
|
||||
columns: [{
|
||||
name: 'my_id',
|
||||
type: 'serial',
|
||||
primaryKey: true
|
||||
}],
|
||||
}
|
||||
|
||||
// Add a new column to generated by Payload table:
|
||||
schema.rawTables.posts.columns.customColumn = {
|
||||
name: 'custom_column',
|
||||
// Note that Payload SQL doesn't support everything that Drizzle does.
|
||||
type: 'integer',
|
||||
notNull: true
|
||||
}
|
||||
// Add a new index to generated by Payload table:
|
||||
schema.rawTables.posts.indexes.customColumnIdx = {
|
||||
name: 'custom_column_idx',
|
||||
unique: true,
|
||||
on: ['custom_column']
|
||||
}
|
||||
|
||||
return schema
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -34,27 +34,41 @@ export default buildConfig({
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
|
||||
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
| Option | Description |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
|
||||
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
|
||||
| `idType` | A string of 'number', or 'uuid' that is used for the data type given to id columns. |
|
||||
| `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) |
|
||||
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
|
||||
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
|
||||
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
|
||||
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
|
||||
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
|
||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||
|
||||
## Access to Drizzle
|
||||
|
||||
After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it.
|
||||
|
||||
You can access Drizzle as follows:
|
||||
To ensure type-safety, you need to generate Drizzle schema first with:
|
||||
```sh
|
||||
npx payload generate:db-schema
|
||||
```
|
||||
|
||||
```text
|
||||
payload.db.drizzle
|
||||
Then, you can access Drizzle as follows:
|
||||
```ts
|
||||
// Import table from the generated file
|
||||
import { posts } from './payload-generated-schema'
|
||||
// To avoid installing Drizzle, you can import everything that drizzle has from our re-export path.
|
||||
import { eq, sql, and } from '@payloadcms/db-sqlite/drizzle'
|
||||
|
||||
// Drizzle's Querying API: https://orm.drizzle.team/docs/rqb
|
||||
const posts = await payload.db.drizzle.query.posts.findMany()
|
||||
// Drizzle's Select API https://orm.drizzle.team/docs/select
|
||||
const result = await payload.db.drizzle.select().from(posts).where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`))
|
||||
```
|
||||
|
||||
## Tables and relations
|
||||
@@ -88,7 +102,7 @@ Runs before the schema is built. You can use this hook to extend your database s
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
|
||||
import { integer, sqliteTable } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
|
||||
|
||||
sqliteAdapter({
|
||||
beforeSchemaInit: [
|
||||
@@ -168,7 +182,7 @@ The following example adds the `extra_integer_column` column and a composite ind
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { index, integer } from 'drizzle-orm/sqlite-core'
|
||||
import { index, integer } from '@payloadcms/db-sqlite/drizzle/sqlite-core'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
@@ -210,3 +224,43 @@ export default buildConfig({
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
### Note for generated schema:
|
||||
Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema.
|
||||
If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`:
|
||||
|
||||
```ts
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
|
||||
sqliteAdapter({
|
||||
beforeSchemaInit: [
|
||||
({ schema, adapter }) => {
|
||||
// Add a new table
|
||||
schema.rawTables.myTable = {
|
||||
name: 'my_table',
|
||||
columns: [{
|
||||
name: 'my_id',
|
||||
type: 'integer',
|
||||
primaryKey: true
|
||||
}],
|
||||
}
|
||||
|
||||
// Add a new column to generated by Payload table:
|
||||
schema.rawTables.posts.columns.customColumn = {
|
||||
name: 'custom_column',
|
||||
// Note that Payload SQL doesn't support everything that Drizzle does.
|
||||
type: 'integer',
|
||||
notNull: true
|
||||
}
|
||||
// Add a new index to generated by Payload table:
|
||||
schema.rawTables.posts.indexes.customColumnIdx = {
|
||||
name: 'custom_column_idx',
|
||||
unique: true,
|
||||
on: ['custom_column']
|
||||
}
|
||||
|
||||
return schema
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -125,8 +125,8 @@ 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. |
|
||||
| **`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). |
|
||||
@@ -136,6 +136,7 @@ powerful Admin UI.
|
||||
| **`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. |
|
||||
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -144,10 +145,11 @@ _\* An asterisk denotes that a property is required._
|
||||
|
||||
You can control the user experience of the join field using the `admin` config properties. The following options are supported:
|
||||
|
||||
| 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](../admin/fields#label) |
|
||||
| Option | Description |
|
||||
|------------------------|---------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`defaultColumns`** | Array of field names that correspond to which columns to show in the relationship table. Default is the collection config. |
|
||||
| **`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](../admin/fields#label) |
|
||||
|
||||
## Join Field Data
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ Functions can be written to make use of the following argument properties:
|
||||
|
||||
- `user` - the authenticated user object
|
||||
- `locale` - the currently selected locale string
|
||||
- `req` - the `PayloadRequest` object
|
||||
|
||||
Here is an example of a `defaultValue` function:
|
||||
|
||||
@@ -227,7 +228,7 @@ export const myField: Field = {
|
||||
name: 'attribution',
|
||||
type: 'text',
|
||||
// highlight-start
|
||||
defaultValue: ({ user, locale }) =>
|
||||
defaultValue: ({ user, locale, req }) =>
|
||||
`${translation[locale]} ${user.name}`,
|
||||
// highlight-end
|
||||
}
|
||||
@@ -235,7 +236,7 @@ export const myField: Field = {
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
You can use async `defaultValue` functions to fill fields with data from API requests.
|
||||
You can use async `defaultValue` functions to fill fields with data from API requests or Local API using `req.payload`.
|
||||
</Banner>
|
||||
|
||||
### Validation
|
||||
|
||||
@@ -61,6 +61,7 @@ export const MyRelationshipField: Field = {
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -6,7 +6,13 @@ desc: The Rich Text field allows dynamic content to be written through the Admin
|
||||
keywords: rich text, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Rich Text Field is a powerful way to allow editors to write dynamic content. The content is saved as JSON in the database and can be converted into any format, including HTML, that you need.
|
||||
The Rich Text Field lets editors write and format dynamic content in a familiar interface.
|
||||
The content is saved as JSON in the database and can be converted to HTML or any other format needed.
|
||||
|
||||
Consistent with Payload's goal of making you learn as little of Payload as possible, customizing
|
||||
and using the Rich Text Editor does not involve learning how to develop for a Payload rich text editor.
|
||||
Instead, you can invest your time and effort into learning the underlying open-source tools that will allow
|
||||
you to apply your learnings elsewhere as well.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/richtext.png"
|
||||
@@ -15,23 +21,6 @@ The Rich Text Field is a powerful way to allow editors to write dynamic content.
|
||||
caption="Admin Panel screenshot of a Rich Text field"
|
||||
/>
|
||||
|
||||
Payload's rich text field is built on an "adapter pattern" which lets you specify which rich text editor you'd like to use.
|
||||
|
||||
Right now, Payload is officially supporting two rich text editors:
|
||||
|
||||
1. [SlateJS](/docs/rich-text/slate) - legacy, backwards-compatible with 1.0
|
||||
2. [Lexical](/docs/lexical/overview) - recommended
|
||||
|
||||
<Banner type="success">
|
||||
<strong>
|
||||
Consistent with Payload's goal of making you learn as little of Payload as possible, customizing
|
||||
and using the Rich Text Editor does not involve learning how to develop for a{' '}<em>Payload</em>{' '}rich text editor.
|
||||
</strong>
|
||||
|
||||
Instead, you can invest your time and effort into learning the underlying open-source tools that
|
||||
will allow you to apply your learnings elsewhere as well.
|
||||
</Banner>
|
||||
|
||||
## Config Options
|
||||
|
||||
| Option | Description |
|
||||
@@ -47,7 +36,7 @@ Right now, Payload is officially supporting two rich text editors:
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`editor`** | Override the rich text editor specified in your base configuration for this field. |
|
||||
| **`editor`** | Customize or override the rich text editor. [More details](/docs/rich-text/overview). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
@@ -79,4 +68,5 @@ The Rich Text Field inherits all of the default options from the base [Field Adm
|
||||
|
||||
## Editor-specific Options
|
||||
|
||||
For a ton more editor-specific options, including how to build custom rich text elements directly into your editor, take a look at either the [Slate docs](/docs/rich-text/slate) or the [Lexical docs](/docs/lexical/overview) depending on which editor you're using.
|
||||
For a ton more editor-specific options, including how to build custom rich text elements directly into your editor,
|
||||
take a look at the [rich text editor documentation](/docs/rich-text/overview).
|
||||
|
||||
@@ -68,11 +68,12 @@ export const MyTextareaField: Field = {
|
||||
|
||||
The Textarea Field inherits all of the default options from the base [Field Admin Config](../admin/fields#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
|
||||
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
|
||||
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
|
||||
| Option | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`placeholder`** | Set this property to define a placeholder string in the textarea. |
|
||||
| **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. |
|
||||
| **`rows`** | Set the number of visible text rows in the textarea. Defaults to `2` if not specified. |
|
||||
| **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. |
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export const MyUploadField: Field = {
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
| **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/graphql/overview#field-complexity) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ keywords: documentation, getting started, guide, Content Management System, cms,
|
||||
---
|
||||
|
||||
<YouTube
|
||||
id="In_lFhzmbME"
|
||||
title="Payload Introduction - Closing the Gap Between Headless CMS and Application Frameworks"
|
||||
id="ftohATkHBi0"
|
||||
title="Introduction to Payload — The open-source Next.js backend"
|
||||
/>
|
||||
|
||||
**Payload is the Next.js fullstack framework.** Write a Payload Config and instantly get:
|
||||
|
||||
@@ -23,6 +23,7 @@ At the top of your Payload Config you can define all the options to manage Graph
|
||||
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
|
||||
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
|
||||
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
|
||||
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
|
||||
|
||||
## Collections
|
||||
|
||||
@@ -124,6 +125,55 @@ You can even log in using the `login[collection-singular-label-here]` mutation t
|
||||
see a ton of detail about how GraphQL operates within Payload.
|
||||
</Banner>
|
||||
|
||||
## Custom Validation Rules
|
||||
|
||||
You can add custom validation rules to your GraphQL API by defining a `validationRules` function in your Payload Config. This function should return an array of [Validation Rules](https://graphql.org/graphql-js/validation/#validation-rules) that will be applied to all incoming queries and mutations.
|
||||
|
||||
```ts
|
||||
import { GraphQL } from '@payloadcms/graphql/types'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
graphQL: {
|
||||
validationRules: (args) => [
|
||||
NoProductionIntrospection
|
||||
]
|
||||
},
|
||||
// ...
|
||||
})
|
||||
|
||||
const NoProductionIntrospection: GraphQL.ValidationRule = (context) => ({
|
||||
Field(node) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (node.name.value === '__schema' || node.name.value === '__type') {
|
||||
context.reportError(
|
||||
new GraphQL.GraphQLError(
|
||||
'GraphQL introspection is not allowed, but the query contained __schema or __type',
|
||||
{ nodes: [node] }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Query complexity limits
|
||||
|
||||
Payload comes with a built-in query complexity limiter to prevent bad people from trying to slow down your server by running massive queries. To learn more, [click here](/docs/production/preventing-abuse#limiting-graphql-complexity).
|
||||
|
||||
## Field complexity
|
||||
|
||||
You can define custom complexity for `relationship`, `upload` and `join` type fields. This is useful if you want to assign a higher complexity to a field that is more expensive to resolve. This can help prevent users from running queries that are too complex.
|
||||
|
||||
```ts
|
||||
const fieldWithComplexity = {
|
||||
name: 'authors',
|
||||
type: 'relationship',
|
||||
relationship: 'authors',
|
||||
graphQL: {
|
||||
complexity: 100, // highlight-line
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -141,3 +141,65 @@ export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuring task restoration
|
||||
|
||||
By default, if a task has passed previously and a workflow is re-run, the task will not be re-run. Instead, the output from the previous task run will be returned. This is to prevent unnecessary re-runs of tasks that have already passed.
|
||||
|
||||
You can configure this behavior through the `retries.shouldRestore` property. This property accepts a boolean or a function.
|
||||
|
||||
If `shouldRestore` is set to true, the task will only be re-run if it previously failed. This is the default behavior.
|
||||
|
||||
If `shouldRestore` this is set to false, the task will be re-run even if it previously succeeded, ignoring the maximum number of retries.
|
||||
|
||||
If `shouldRestore` is a function, the return value of the function will determine whether the task should be re-run. This can be used for more complex restore logic, e.g you may want to re-run a task up to X amount of times and then restore it for consecutive runs, or only re-run a task if the input has changed.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
slug: 'myTask',
|
||||
retries: {
|
||||
shouldRestore: false,
|
||||
}
|
||||
// ...
|
||||
} as TaskConfig<'myTask'>,
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Example - determine whether a task should be restored based on the input data:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// ...
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
slug: 'myTask',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'someDate',
|
||||
type: 'date',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
retries: {
|
||||
shouldRestore: ({ input }) => {
|
||||
if(new Date(input.someDate) > new Date()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
// ...
|
||||
} as TaskConfig<'myTask'>,
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
---
|
||||
title: Lexical Overview
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
|
||||
keywords: lexical, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
One of Payload's goals is to build the best rich text editor experience that we possibly can. We want to combine the beauty and polish of the Medium editing experience with the strength and features of the Notion editor - all in one place.
|
||||
|
||||
Classically, we've used SlateJS to work toward this goal, but building custom elements into Slate has proven to be more difficult than we'd like, and we've been keeping our options open.
|
||||
|
||||
Lexical is extremely impressive and trivializes a lot of the hard parts of building new elements into a rich text editor. It has a few distinct advantages over Slate, including the following:
|
||||
|
||||
1. A "/" menu, which allows editors to easily add new elements while never leaving their keyboard
|
||||
1. A "hover" toolbar that pops up if you select text
|
||||
1. It supports Payload blocks natively, directly within your rich text editor
|
||||
1. Custom elements, called "features", are much easier to build in Lexical vs. Slate
|
||||
|
||||
To use the Lexical editor, first you need to install it:
|
||||
|
||||
```
|
||||
npm install @payloadcms/richtext-lexical
|
||||
```
|
||||
|
||||
Once you have it installed, you can pass it to your top-level Payload Config as follows:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
// your collections here
|
||||
],
|
||||
// Pass the Lexical editor to the root config
|
||||
editor: lexicalEditor({}),
|
||||
})
|
||||
```
|
||||
|
||||
You can also override Lexical settings on a field-by-field basis as follows:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
// Pass the Lexical editor here and override base settings as necessary
|
||||
editor: lexicalEditor({}),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the lexical editor with Features
|
||||
|
||||
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
|
||||
|
||||
### Features: The Building Blocks
|
||||
|
||||
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
|
||||
|
||||
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
|
||||
|
||||
### Integrating New Features
|
||||
|
||||
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
|
||||
|
||||
```ts
|
||||
import {
|
||||
BlocksFeature,
|
||||
LinkFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { Banner } from '../blocks/Banner'
|
||||
import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
{
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures, rootFeatures }) => [
|
||||
...defaultFeatures,
|
||||
LinkFeature({
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Link feature
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Upload feature
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// This is incredibly powerful. You can re-use your Payload blocks
|
||||
// directly in the Lexical editor as follows:
|
||||
BlocksFeature({
|
||||
blocks: [Banner, CallToAction],
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`features` can be both an array of features, or a function returning an array of features. The function provides the following props:
|
||||
|
||||
|
||||
| Prop | Description |
|
||||
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. |
|
||||
| **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. |
|
||||
|
||||
|
||||
## Features overview
|
||||
|
||||
Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
|---------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
|
||||
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](/docs/lexical/building-custom-features).
|
||||
|
||||
## TypeScript
|
||||
|
||||
Every single piece of saved data is 100% fully-typed within lexical. It provides a type for every single node, which can be imported from `@payloadcms/richtext-lexical` - each type is prefixed with `Serialized`, e.g. `SerializedUploadNode`.
|
||||
|
||||
In order to fully type the entire editor JSON, you can use our `TypedEditorState` helper type, which accepts a union of all possible node types as a generic. The reason we do not provide a type which already contains all possible node types is because the possible node types depend on which features you have enabled in your editor. Here is an example:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SerializedAutoLinkNode,
|
||||
SerializedBlockNode,
|
||||
SerializedHorizontalRuleNode,
|
||||
SerializedLinkNode,
|
||||
SerializedListItemNode,
|
||||
SerializedListNode,
|
||||
SerializedParagraphNode,
|
||||
SerializedQuoteNode,
|
||||
SerializedRelationshipNode,
|
||||
SerializedTextNode,
|
||||
SerializedUploadNode,
|
||||
TypedEditorState,
|
||||
SerializedHeadingNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: TypedEditorState<
|
||||
| SerializedAutoLinkNode
|
||||
| SerializedBlockNode
|
||||
| SerializedHorizontalRuleNode
|
||||
| SerializedLinkNode
|
||||
| SerializedListItemNode
|
||||
| SerializedListNode
|
||||
| SerializedParagraphNode
|
||||
| SerializedQuoteNode
|
||||
| SerializedRelationshipNode
|
||||
| SerializedTextNode
|
||||
| SerializedUploadNode
|
||||
| SerializedHeadingNode
|
||||
> = {
|
||||
root: {
|
||||
type: 'root',
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Some text. Every property here is fully-typed',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DefaultTypedEditorState
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: DefaultTypedEditorState = {
|
||||
root: {
|
||||
type: 'root',
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Some text. Every property here is fully-typed',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: `DefaultTypedEditorState<SerializedBlockNode | YourCustomSerializedNode>`.
|
||||
|
||||
This is a type-safe representation of the editor state. Looking at the auto-suggestions of `type` it will show you all the possible node types you can use.
|
||||
|
||||
Make sure to only use types exported from `@payloadcms/richtext-lexical`, not from the lexical core packages. We only have control over types we export and can guarantee that those are correct, even though lexical core may export types with identical names.
|
||||
|
||||
### Automatic type generation
|
||||
|
||||
Lexical does not generate the accurate type definitions for your richText fields for you yet - this will be improved in the future. Currently, it only outputs the rough shape of the editor JSON which you can enhance using type assertions.
|
||||
@@ -79,7 +79,7 @@ This allows you to add i18n translations scoped to your feature. This specific e
|
||||
|
||||
### Markdown Transformers#server-feature-markdown-transformers
|
||||
|
||||
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/lexical/converters#markdown-lexical).
|
||||
The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/rich-text/converters#markdown-lexical).
|
||||
|
||||
```ts
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical';
|
||||
@@ -10,20 +10,24 @@ Lexical saves data in JSON - this is great for storage and flexibility and allow
|
||||
|
||||
## Lexical => JSX
|
||||
|
||||
If you have a React-based frontend, converting lexical to JSX is the recommended way to render rich text content in your frontend. To do that, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the lexical content to it:
|
||||
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { RichText } from '@payloadcms/richtext-lexical/react'
|
||||
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
export const MyComponent = ({ lexicalData }) => {
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
return (
|
||||
<RichText data={lexicalData} />
|
||||
<RichText data={data} />
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `RichText` component will come with the most common serializers built-in, though you can also pass in your own serializers if you need to.
|
||||
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
|
||||
|
||||
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
|
||||
|
||||
|
||||
<Banner type="default">
|
||||
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
title: Lexical Rich Text
|
||||
label: Lexical
|
||||
order: 30
|
||||
desc: Built by Meta, Lexical is an incredibly powerful rich text editor, and it works beautifully within Payload.
|
||||
keywords: lexical, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
The new lexical docs can be found at [Lexical](/docs/lexical/overview).
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Lexical Migration
|
||||
label: Migration
|
||||
order: 30
|
||||
order: 90
|
||||
desc: Migration from slate and payload-plugin-lexical to lexical
|
||||
keywords: lexical, rich text, editor, headless cms, migrate, migration
|
||||
---
|
||||
@@ -1,18 +1,300 @@
|
||||
---
|
||||
title: Overview
|
||||
title: Rich Text Editor
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Rich Text within Payload is extremely powerful. We've combined the beauty of the Medium editor with the power of the Notion editor all in one place.
|
||||
keywords: slatejs, lexical, rich text, json, custom editor, javascript, typescript
|
||||
desc: The Payload editor, based on Lexical, allows for great customization with unparalleled ease.
|
||||
keywords: lexical, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
Payload currently supports two official rich text editors and you can choose either one depending on your needs.
|
||||
<Banner type="warning">
|
||||
|
||||
1. [SlateJS](/docs/rich-text/slate) - stable, backwards-compatible with 1.0
|
||||
2. [Lexical](/docs/lexical/overview) - recommended
|
||||
The Payload editor is based on Lexical, Meta's rich text editor. The previous default editor was
|
||||
based on Slate and is still supported. You can read [its documentation](/docs/rich-text/slate),
|
||||
or the optional [migration guide](/docs/rich-text/migration) to migrate from Slate to Lexical (recommended).
|
||||
|
||||
These editors are built on an "adapter pattern" which means that you will need to install the editor you'd like to use. Take a look at the docs for the editor you'd like to use for instructions on how to install it.
|
||||
</Banner>
|
||||
|
||||
The big TL;DR here is that Slate is what we have used in the past, and we still support it for existing projects, but if you're building something new and you're feeling adventurous, you should give Lexical a shot. Slate has a lot of good stuff, but Lexical has lots more.
|
||||
One of Payload's goals is to build the best rich text editor experience that we possibly can. We want to combine the beauty and polish of the Medium editing experience with the strength and features of the Notion editor - all in one place.
|
||||
|
||||
No matter which editor you use, you have to install it at the top-level on the `config.editor` property, which will then cascade throughout all of your rich text fields and be used accordingly. Additionally, you also have the option to override the editor on a field-by-field basis if you'd like.
|
||||
Classically, we've used SlateJS to work toward this goal, but building custom elements into Slate has proven to be more difficult than we'd like, and we've been keeping our options open.
|
||||
|
||||
Lexical is extremely impressive and trivializes a lot of the hard parts of building new elements into a rich text editor. It has a few distinct advantages over Slate, including the following:
|
||||
|
||||
1. A "/" menu, which allows editors to easily add new elements while never leaving their keyboard
|
||||
1. A "hover" toolbar that pops up if you select text
|
||||
1. It supports Payload blocks natively, directly within your rich text editor
|
||||
1. Custom elements, called "features", are much easier to build in Lexical vs. Slate
|
||||
|
||||
To use the Lexical editor, first you need to install it:
|
||||
|
||||
```
|
||||
npm install @payloadcms/richtext-lexical
|
||||
```
|
||||
|
||||
Once you have it installed, you can pass it to your top-level Payload Config as follows:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
// your collections here
|
||||
],
|
||||
// Pass the Lexical editor to the root config
|
||||
editor: lexicalEditor({}),
|
||||
})
|
||||
```
|
||||
|
||||
You can also override Lexical settings on a field-by-field basis as follows:
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
// Pass the Lexical editor here and override base settings as necessary
|
||||
editor: lexicalEditor({}),
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the lexical editor with Features
|
||||
|
||||
Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life.
|
||||
|
||||
### Features: The Building Blocks
|
||||
|
||||
At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed.
|
||||
|
||||
If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch.
|
||||
|
||||
### Integrating New Features
|
||||
|
||||
To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done:
|
||||
|
||||
```ts
|
||||
import {
|
||||
BlocksFeature,
|
||||
LinkFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import { Banner } from '../blocks/Banner'
|
||||
import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
{
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures, rootFeatures }) => [
|
||||
...defaultFeatures,
|
||||
LinkFeature({
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Link feature
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'rel',
|
||||
label: 'Rel Attribute',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: ['noopener', 'noreferrer', 'nofollow'],
|
||||
admin: {
|
||||
description:
|
||||
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
// Example showing how to customize the built-in fields
|
||||
// of the Upload feature
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
// This is incredibly powerful. You can re-use your Payload blocks
|
||||
// directly in the Lexical editor as follows:
|
||||
BlocksFeature({
|
||||
blocks: [Banner, CallToAction],
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`features` can be both an array of features, or a function returning an array of features. The function provides the following props:
|
||||
|
||||
|
||||
| Prop | Description |
|
||||
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. |
|
||||
| **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. |
|
||||
|
||||
|
||||
## Features overview
|
||||
|
||||
Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
|---------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`BoldTextFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughTextFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptTextFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptTextFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeTextFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
|
||||
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](/docs/rich-text/building-custom-features).
|
||||
|
||||
## TypeScript
|
||||
|
||||
Every single piece of saved data is 100% fully-typed within lexical. It provides a type for every single node, which can be imported from `@payloadcms/richtext-lexical` - each type is prefixed with `Serialized`, e.g. `SerializedUploadNode`.
|
||||
|
||||
In order to fully type the entire editor JSON, you can use our `TypedEditorState` helper type, which accepts a union of all possible node types as a generic. The reason we do not provide a type which already contains all possible node types is because the possible node types depend on which features you have enabled in your editor. Here is an example:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SerializedAutoLinkNode,
|
||||
SerializedBlockNode,
|
||||
SerializedHorizontalRuleNode,
|
||||
SerializedLinkNode,
|
||||
SerializedListItemNode,
|
||||
SerializedListNode,
|
||||
SerializedParagraphNode,
|
||||
SerializedQuoteNode,
|
||||
SerializedRelationshipNode,
|
||||
SerializedTextNode,
|
||||
SerializedUploadNode,
|
||||
TypedEditorState,
|
||||
SerializedHeadingNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: TypedEditorState<
|
||||
| SerializedAutoLinkNode
|
||||
| SerializedBlockNode
|
||||
| SerializedHorizontalRuleNode
|
||||
| SerializedLinkNode
|
||||
| SerializedListItemNode
|
||||
| SerializedListNode
|
||||
| SerializedParagraphNode
|
||||
| SerializedQuoteNode
|
||||
| SerializedRelationshipNode
|
||||
| SerializedTextNode
|
||||
| SerializedUploadNode
|
||||
| SerializedHeadingNode
|
||||
> = {
|
||||
root: {
|
||||
type: 'root',
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Some text. Every property here is fully-typed',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DefaultTypedEditorState
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const editorState: DefaultTypedEditorState = {
|
||||
root: {
|
||||
type: 'root',
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Some text. Every property here is fully-typed',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
textFormat: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: `DefaultTypedEditorState<SerializedBlockNode | YourCustomSerializedNode>`.
|
||||
|
||||
This is a type-safe representation of the editor state. Looking at the auto-suggestions of `type` it will show you all the possible node types you can use.
|
||||
|
||||
Make sure to only use types exported from `@payloadcms/richtext-lexical`, not from the lexical core packages. We only have control over types we export and can guarantee that those are correct, even though lexical core may export types with identical names.
|
||||
|
||||
### Automatic type generation
|
||||
|
||||
Lexical does not generate the accurate type definitions for your richText fields for you yet - this will be improved in the future. Currently, it only outputs the rough shape of the editor JSON which you can enhance using type assertions.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Slate Rich Text
|
||||
label: Slate
|
||||
order: 20
|
||||
title: Slate Editor
|
||||
label: Slate (legacy)
|
||||
order: 100
|
||||
desc: The Slate editor has been supported by Payload since beta. It's very powerful and stores content as JSON, which unlocks a ton of power.
|
||||
keywords: slatejs, slate, rich text, editor, headless cms
|
||||
---
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# NOTE: Change port of `PAYLOAD_PUBLIC_SITE_URL` if front-end is running on another server
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
# Database connection string
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-draft-preview-example
|
||||
|
||||
# Used to encrypt JWT tokens
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
# Used to configure CORS, format links and more. No trailing slash
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-example-auth
|
||||
PAYLOAD_SECRET=PAYLOAD_AUTH_EXAMPLE_SECRET_KEY
|
||||
|
||||
# Used to share cookies across subdomains
|
||||
COOKIE_DOMAIN=localhost
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
playwright.config.ts
|
||||
jest.config.js
|
||||
@@ -1,15 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
|
||||
ignorePatterns: ['**/payload-types.ts'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
extends: ['@payloadcms'],
|
||||
}
|
||||
|
||||
3
examples/auth/.gitignore
vendored
3
examples/auth/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
build
|
||||
dist
|
||||
node_modules
|
||||
package - lock.json.env
|
||||
package-lock.json
|
||||
.env
|
||||
|
||||
24
examples/auth/.swcrc
Normal file
24
examples/auth/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,27 @@
|
||||
|
||||
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
|
||||
|
||||
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, the principals are generally the same. To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Quick Start
|
||||
|
||||
To spin up this example locally, follow these steps:
|
||||
To spin up this example locally, follow the steps below:
|
||||
|
||||
1. Clone this repo
|
||||
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
1. Navigate into the project directory and install dependencies using your preferred package manager:
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
- `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
1. `cp .env.example .env` to copy the example environment variables
|
||||
> \*NOTE: The --ignore-workspace flag is needed if you are running this example within the Payload monorepo to avoid workspace conflicts.
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
1. Start the server:
|
||||
- Depending on your package manager, run `pnpm dev`, `yarn dev` or `npm run dev`
|
||||
- When prompted, type `y` then `enter` to seed the database with sample data
|
||||
1. Access the application:
|
||||
- Open your browser and navigate to `http://localhost:3000` to access the homepage.
|
||||
- Open `http://localhost:3000/admin` to access the admin panel.
|
||||
1. Login:
|
||||
|
||||
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
1. `open http://localhost:3000` to access the home page
|
||||
1. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
- Use the following credentials to log into the admin panel:
|
||||
> `Email: demo@payloadcms.com` > `Password: demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -45,7 +44,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
import config from '../../payload.config'
|
||||
|
||||
export default async function AccountPage({ searchParams }) {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
2
examples/auth/next-env.d.ts
vendored
2
examples/auth/next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -3,41 +3,39 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Payload authentication example.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation && pnpm seed && next dev",
|
||||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
"generate:schema": "payload-graphql generate:schema",
|
||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"seed": "npm run payload migrate:fresh",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.24",
|
||||
"@payloadcms/next": "3.0.0-beta.24",
|
||||
"@payloadcms/richtext-slate": "3.0.0-beta.24",
|
||||
"@payloadcms/ui": "3.0.0-beta.24",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@payloadcms/ui": "latest",
|
||||
"cross-env": "^7.0.3",
|
||||
"next": "14.3.0-canary.68",
|
||||
"payload": "3.0.0-beta.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"next": "^15.0.0",
|
||||
"payload": "latest",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.51.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@payloadcms/eslint-config": "^1.1.1",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@swc/types": "^0.1.6",
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"dotenv": "^16.4.5",
|
||||
"@payloadcms/graphql": "latest",
|
||||
"@swc/core": "^1.6.13",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"eslint": "^8.57.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "5.4.4"
|
||||
"eslint-config-next": "^15.0.0",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
10841
examples/auth/pnpm-lock.yaml
generated
10841
examples/auth/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
31
examples/auth/src/app/(app)/_components/Header/index.tsx
Normal file
31
examples/auth/src/app/(app)/_components/Header/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
import { HeaderNav } from './Nav'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link className={classes.logo} href="/">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<HeaderNav />
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { AccountForm } from './AccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Account() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CreateAccountForm } from './CreateAccountForm'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function CreateAccount() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import classes from './index.module.scss'
|
||||
import { LoginForm } from './LoginForm'
|
||||
|
||||
export default async function Login() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import classes from './index.module.scss'
|
||||
import { LogoutPage } from './LogoutPage'
|
||||
|
||||
export default async function Logout() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Gutter } from './_components/Gutter'
|
||||
import { HydrateClientUser } from './_components/HydrateClientUser'
|
||||
|
||||
export default async function HomePage() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { permissions, user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import classes from './index.module.scss'
|
||||
import { RecoverPasswordForm } from './RecoverPasswordForm'
|
||||
|
||||
export default async function RecoverPassword() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import classes from './index.module.scss'
|
||||
import { ResetPasswordForm } from './ResetPasswordForm'
|
||||
|
||||
export default async function ResetPassword() {
|
||||
const headers = getHeaders()
|
||||
const headers = await getHeaders()
|
||||
const payload = await getPayload({ config })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
|
||||
@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
|
||||
@@ -5,18 +5,21 @@ import type { Metadata } from 'next'
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default Page
|
||||
|
||||
5
examples/auth/src/app/(payload)/admin/importMap.js
Normal file
5
examples/auth/src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BeforeLogin as BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e } from '@/components/BeforeLogin'
|
||||
|
||||
export const importMap = {
|
||||
'@/components/BeforeLogin#BeforeLogin': BeforeLogin_8a7ab0eb7ab5c511aba12e68480bfe5e,
|
||||
}
|
||||
@@ -1,16 +1,32 @@
|
||||
/* 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 type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
|
||||
@@ -1,13 +1,62 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
import { admins } from './access/admins'
|
||||
import adminsAndUser from './access/adminsAndUser'
|
||||
import { anyone } from './access/anyone'
|
||||
import { checkRole } from './access/checkRole'
|
||||
import { loginAfterCreate } from './hooks/loginAfterCreate'
|
||||
import { protectRoles } from './hooks/protectRoles'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: {
|
||||
tokenExpiration: 28800, // 8 hours
|
||||
cookies: {
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
domain: process.env.COOKIE_DOMAIN,
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
access: {
|
||||
read: adminsAndUser,
|
||||
create: anyone,
|
||||
update: adminsAndUser,
|
||||
delete: admins,
|
||||
admin: ({ req: { user } }) => checkRole(['admin'], user),
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [loginAfterCreate],
|
||||
},
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
{
|
||||
name: 'firstName',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
saveToJWT: true,
|
||||
hooks: {
|
||||
beforeChange: [protectRoles],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
const BeforeLogin: React.FC = () => {
|
||||
export const BeforeLogin: React.FC = () => {
|
||||
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
|
||||
return (
|
||||
<p>
|
||||
@@ -13,5 +13,3 @@ const BeforeLogin: React.FC = () => {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default BeforeLogin
|
||||
|
||||
@@ -7,16 +7,53 @@
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -38,6 +75,24 @@ export interface User {
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
@@ -72,6 +127,63 @@ export interface PayloadMigration {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
firstName?: T;
|
||||
lastName?: T;
|
||||
roles?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
|
||||
@@ -2,28 +2,21 @@ import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
import { Users } from './collections/Users'
|
||||
import BeforeLogin from './components/BeforeLogin'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
components: {
|
||||
beforeLogin: [BeforeLogin],
|
||||
beforeLogin: ['@/components/BeforeLogin#BeforeLogin'],
|
||||
},
|
||||
},
|
||||
collections: [Users],
|
||||
cors: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
csrf: [
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
process.env.PAYLOAD_PUBLIC_SITE_URL || '',
|
||||
].filter(Boolean),
|
||||
cors: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
|
||||
csrf: [process.env.NEXT_PUBLIC_SERVER_URL || ''].filter(Boolean),
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI || '',
|
||||
}),
|
||||
|
||||
@@ -23,10 +23,25 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@payload-config": ["src/payload.config.ts"]
|
||||
}
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@payload-config": [
|
||||
"src/payload.config.ts"
|
||||
],
|
||||
"@payload-types": [
|
||||
"src/payload-types.ts"
|
||||
]
|
||||
},
|
||||
"target": "ES2022",
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import '@payloadcms/next/css'
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"dotenv": "^8.2.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"graphql": "^16.9.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"next": "^15.0.0",
|
||||
"payload": "latest",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CollectionSlug } from 'payload'
|
||||
import type { CollectionSlug, PayloadRequest } from 'payload'
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
@@ -42,23 +41,20 @@ export async function GET(
|
||||
return new Response('No path provided', { status: 404 })
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
return new Response('This endpoint can only be used for internal previews', { status: 500 })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
try {
|
||||
user = jwt.verify(token, payload.secret)
|
||||
} catch (error) {
|
||||
payload.logger.error({
|
||||
err: error,
|
||||
msg: 'Error verifying token for live preview',
|
||||
user = await payload.auth({
|
||||
req: req as unknown as PayloadRequest,
|
||||
headers: req.headers,
|
||||
})
|
||||
} catch (error) {
|
||||
payload.logger.error({ err: error }, 'Error verifying token for live preview')
|
||||
return new Response('You are not allowed to preview this page', { status: 403 })
|
||||
}
|
||||
|
||||
const draft = await draftMode()
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.41.0",
|
||||
"react-select": "^5.8.0"
|
||||
"react-select": "^5.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/graphql": "latest",
|
||||
|
||||
@@ -32,7 +32,7 @@ const config = withBundleAnalyzer(
|
||||
return [
|
||||
{
|
||||
destination: '/admin',
|
||||
permanent: true,
|
||||
permanent: false,
|
||||
source: '/',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.6.0",
|
||||
"version": "3.9.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -54,6 +54,7 @@
|
||||
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts",
|
||||
"dev:generate-db-schema": "pnpm runts ./test/generateDatabaseSchema.ts",
|
||||
"dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
|
||||
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
||||
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
||||
@@ -168,7 +169,7 @@
|
||||
"tempy": "1.0.1",
|
||||
"tstyche": "^3.1.1",
|
||||
"tsx": "4.19.2",
|
||||
"turbo": "^2.1.3",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "5.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.6.0",
|
||||
"version": "3.9.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -4,42 +4,66 @@ import path from 'path'
|
||||
import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
|
||||
|
||||
import { debug, error } from '../utils/log.js'
|
||||
import { dbChoiceRecord } from './select-db.js'
|
||||
|
||||
const updateEnvVariables = (
|
||||
contents: string,
|
||||
const updateEnvExampleVariables = (contents: string, databaseType: DbType | undefined): string => {
|
||||
return contents
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (line.startsWith('#') || !line.includes('=')) {
|
||||
return line // Preserve comments and unrelated lines
|
||||
}
|
||||
|
||||
const [key] = line.split('=')
|
||||
|
||||
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
|
||||
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
|
||||
|
||||
if (dbChoice) {
|
||||
const placeholderUri = `${dbChoice.dbConnectionPrefix}your-database-name${
|
||||
dbChoice.dbConnectionSuffix || ''
|
||||
}`
|
||||
return databaseType === 'vercel-postgres'
|
||||
? `POSTGRES_URL=${placeholderUri}`
|
||||
: `DATABASE_URI=${placeholderUri}`
|
||||
}
|
||||
|
||||
return `DATABASE_URI=your-database-connection-here` // Fallback
|
||||
}
|
||||
|
||||
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
|
||||
return `PAYLOAD_SECRET=YOUR_SECRET_HERE`
|
||||
}
|
||||
|
||||
return line
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
const generateEnvContent = (
|
||||
existingEnv: string,
|
||||
databaseType: DbType | undefined,
|
||||
databaseUri: string,
|
||||
payloadSecret: string,
|
||||
): string => {
|
||||
return contents
|
||||
const dbKey = databaseType === 'vercel-postgres' ? 'POSTGRES_URL' : 'DATABASE_URI'
|
||||
|
||||
const envVars: Record<string, string> = {}
|
||||
existingEnv
|
||||
.split('\n')
|
||||
.filter((e) => e)
|
||||
.map((line) => {
|
||||
if (line.startsWith('#') || !line.includes('=')) {
|
||||
return line
|
||||
}
|
||||
|
||||
const [key, ...valueParts] = line.split('=')
|
||||
let value = valueParts.join('=')
|
||||
|
||||
if (
|
||||
key === 'MONGODB_URI' ||
|
||||
key === 'MONGO_URL' ||
|
||||
key === 'DATABASE_URI' ||
|
||||
key === 'POSTGRES_URL'
|
||||
) {
|
||||
value = databaseUri
|
||||
if (databaseType === 'vercel-postgres') {
|
||||
value = databaseUri
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
|
||||
value = payloadSecret
|
||||
}
|
||||
|
||||
return `${key}=${value}`
|
||||
.filter((line) => line.includes('=') && !line.startsWith('#'))
|
||||
.forEach((line) => {
|
||||
const [key, value] = line.split('=')
|
||||
envVars[key] = value
|
||||
})
|
||||
|
||||
// Override specific keys
|
||||
envVars[dbKey] = databaseUri
|
||||
envVars['PAYLOAD_SECRET'] = payloadSecret
|
||||
|
||||
// Rebuild content
|
||||
return Object.entries(envVars)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
@@ -65,6 +89,7 @@ export async function manageEnvFiles(args: {
|
||||
try {
|
||||
let updatedExampleContents: string
|
||||
|
||||
// Update .env.example
|
||||
if (template?.type === 'starter') {
|
||||
if (!fs.existsSync(envExamplePath)) {
|
||||
error(`.env.example file not found at ${envExamplePath}`)
|
||||
@@ -72,25 +97,25 @@ export async function manageEnvFiles(args: {
|
||||
}
|
||||
|
||||
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
|
||||
updatedExampleContents = updateEnvVariables(
|
||||
envExampleContents,
|
||||
databaseType,
|
||||
databaseUri,
|
||||
payloadSecret,
|
||||
)
|
||||
updatedExampleContents = updateEnvExampleVariables(envExampleContents, databaseType)
|
||||
|
||||
await fs.writeFile(envExamplePath, updatedExampleContents)
|
||||
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
|
||||
debug(`.env.example file successfully updated`)
|
||||
} else {
|
||||
updatedExampleContents = `# Added by Payload\nDATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}\n`
|
||||
updatedExampleContents = `# Added by Payload\nDATABASE_URI=your-connection-string-here\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
|
||||
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
|
||||
}
|
||||
|
||||
const existingEnvContents = fs.existsSync(envPath) ? await fs.readFile(envPath, 'utf8') : ''
|
||||
const updatedEnvContents = existingEnvContents
|
||||
? `${existingEnvContents}\n# Added by Payload\n${updatedExampleContents}`
|
||||
: `# Added by Payload\n${updatedExampleContents}`
|
||||
// Merge existing variables and create or update .env
|
||||
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
|
||||
const envContent = generateEnvContent(
|
||||
envExampleContents,
|
||||
databaseType,
|
||||
databaseUri,
|
||||
payloadSecret,
|
||||
)
|
||||
await fs.writeFile(envPath, `# Added by Payload\n${envContent.trimEnd()}\n`)
|
||||
|
||||
await fs.writeFile(envPath, updatedEnvContents)
|
||||
debug(`.env file successfully created or updated`)
|
||||
} catch (err: unknown) {
|
||||
error('Unable to manage environment files')
|
||||
|
||||
@@ -10,7 +10,7 @@ type DbChoice = {
|
||||
value: DbType
|
||||
}
|
||||
|
||||
const dbChoiceRecord: Record<DbType, DbChoice> = {
|
||||
export const dbChoiceRecord: Record<DbType, DbChoice> = {
|
||||
mongodb: {
|
||||
dbConnectionPrefix: 'mongodb://127.0.0.1/',
|
||||
title: 'MongoDB',
|
||||
|
||||
@@ -13,10 +13,16 @@ export function copyRecursiveSync(src: string, dest: string, ignoreRegex?: strin
|
||||
if (isDirectory) {
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
fs.readdirSync(src).forEach((childItemName) => {
|
||||
if (ignoreRegex && ignoreRegex.some((regex) => new RegExp(regex).test(childItemName))) {
|
||||
if (
|
||||
ignoreRegex &&
|
||||
ignoreRegex.some((regex) => {
|
||||
return new RegExp(regex).test(childItemName)
|
||||
})
|
||||
) {
|
||||
console.log(`Ignoring ${childItemName} due to regex: ${ignoreRegex}`)
|
||||
return
|
||||
}
|
||||
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
|
||||
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName), ignoreRegex)
|
||||
})
|
||||
} else {
|
||||
fs.copyFileSync(src, dest)
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||
"noEmit": false /* Do not emit outputs. */,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["dist", "build", "tests", "test", "node_modules", "eslint.config.js"],
|
||||
"include": ["src/**/*.ts", "src/**/*.spec.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.6.0",
|
||||
"version": "3.9.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import type { CountOptions } from 'mongodb'
|
||||
import type { Count, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import type { Count } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const count: Count = async function count(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, where },
|
||||
{ collection, locale, req, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options: CountOptions = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
const hasNearConstraint = getHasNearConstraint(where)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
let result: number
|
||||
if (useEstimatedCount) {
|
||||
result = await Model.estimatedDocumentCount({ session: options.session })
|
||||
result = await Model.collection.estimatedDocumentCount()
|
||||
} else {
|
||||
result = await Model.countDocuments(query, options)
|
||||
const options: CountOptions = { session }
|
||||
|
||||
if (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,
|
||||
}
|
||||
}
|
||||
|
||||
result = await Model.collection.countDocuments(query, options)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import type { CountOptions } from 'mongodb'
|
||||
import type { CountGlobalVersions, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import type { CountGlobalVersions } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: MongooseAdapter,
|
||||
{ global, locale, req = {} as PayloadRequest, where },
|
||||
{ global, locale, req, where },
|
||||
) {
|
||||
const Model = this.versions[global]
|
||||
const options: CountOptions = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
const hasNearConstraint = getHasNearConstraint(where)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
let result: number
|
||||
if (useEstimatedCount) {
|
||||
result = await Model.estimatedDocumentCount({ session: options.session })
|
||||
result = await Model.collection.estimatedDocumentCount()
|
||||
} else {
|
||||
result = await Model.countDocuments(query, options)
|
||||
const options: CountOptions = { session }
|
||||
|
||||
if (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,
|
||||
}
|
||||
}
|
||||
|
||||
result = await Model.collection.countDocuments(query, options)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import type { CountOptions } from 'mongodb'
|
||||
import type { CountVersions, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import type { CountVersions } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, where },
|
||||
{ collection, locale, req, where },
|
||||
) {
|
||||
const Model = this.versions[collection]
|
||||
const options: CountOptions = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
const hasNearConstraint = getHasNearConstraint(where)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
let result: number
|
||||
if (useEstimatedCount) {
|
||||
result = await Model.estimatedDocumentCount({ session: options.session })
|
||||
result = await Model.collection.estimatedDocumentCount()
|
||||
} else {
|
||||
result = await Model.countDocuments(query, options)
|
||||
const options: CountOptions = { session }
|
||||
|
||||
if (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,
|
||||
}
|
||||
}
|
||||
|
||||
result = await Model.collection.countDocuments(query, options)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import type { Create, Document, PayloadRequest } from 'payload'
|
||||
import type { Create } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, req = {} as PayloadRequest },
|
||||
{ collection, data, req },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options = await withSession(this, req)
|
||||
let doc
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data,
|
||||
fields: this.payload.collections[collection].config.fields,
|
||||
})
|
||||
const fields = this.payload.collections[collection].config.flattenedFields
|
||||
|
||||
if (this.payload.collections[collection].customIDType) {
|
||||
sanitizedData._id = sanitizedData.id
|
||||
data._id = data.id
|
||||
}
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'create',
|
||||
})
|
||||
|
||||
try {
|
||||
;[doc] = await Model.create([sanitizedData], options)
|
||||
const { insertedId: insertedID } = await Model.collection.insertOne(data, { session })
|
||||
data._id = insertedID
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import type { CreateGlobal, PayloadRequest } from 'payload'
|
||||
import type { CreateGlobal } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, data, req = {} as PayloadRequest },
|
||||
{ slug, data, req },
|
||||
) {
|
||||
const Model = this.globals
|
||||
|
||||
const global = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
globalType: slug,
|
||||
...data,
|
||||
},
|
||||
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
|
||||
const fields = this.payload.config.globals.find(
|
||||
(globalConfig) => globalConfig.slug === slug,
|
||||
).flattenedFields
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
globalSlug: slug,
|
||||
operation: 'create',
|
||||
})
|
||||
|
||||
const options = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
let [result] = (await Model.create([global], options)) as any
|
||||
const { insertedId: insertedID } = await Model.collection.insertOne(data, { session })
|
||||
;(data as any)._id = insertedID
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
buildVersionGlobalFields,
|
||||
type CreateGlobalVersion,
|
||||
type Document,
|
||||
type PayloadRequest,
|
||||
} from 'payload'
|
||||
import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
|
||||
this: MongooseAdapter,
|
||||
@@ -18,41 +13,48 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
globalSlug,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
req,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
},
|
||||
) {
|
||||
const VersionModel = this.versions[globalSlug]
|
||||
const options = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const data = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
fields: buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
),
|
||||
const data = {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
}
|
||||
|
||||
const fields = buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
true,
|
||||
)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'create',
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
const { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session })
|
||||
;(data as any)._id = insertedID
|
||||
|
||||
await VersionModel.updateMany(
|
||||
await VersionModel.collection.updateMany(
|
||||
{
|
||||
$and: [
|
||||
{
|
||||
_id: {
|
||||
$ne: doc._id,
|
||||
$ne: insertedID,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -68,16 +70,15 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
],
|
||||
},
|
||||
{ $unset: { latest: 1 } },
|
||||
options,
|
||||
{ session },
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return data as any
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Types } from 'mongoose'
|
||||
import {
|
||||
buildVersionCollectionFields,
|
||||
type CreateVersion,
|
||||
type Document,
|
||||
type PayloadRequest,
|
||||
} from 'payload'
|
||||
import { buildVersionCollectionFields, type CreateVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const createVersion: CreateVersion = async function createVersion(
|
||||
this: MongooseAdapter,
|
||||
@@ -19,34 +14,41 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
createdAt,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req = {} as PayloadRequest,
|
||||
req,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
},
|
||||
) {
|
||||
const VersionModel = this.versions[collectionSlug]
|
||||
const options = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const data = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
data: {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
},
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collectionSlug].config,
|
||||
),
|
||||
const data: any = {
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
version: versionData,
|
||||
}
|
||||
|
||||
const fields = buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collectionSlug].config,
|
||||
true,
|
||||
)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'create',
|
||||
})
|
||||
|
||||
const [doc] = await VersionModel.create([data], options, req)
|
||||
const { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session })
|
||||
data._id = insertedID
|
||||
|
||||
const parentQuery = {
|
||||
$or: [
|
||||
@@ -57,7 +59,7 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
},
|
||||
],
|
||||
}
|
||||
if (data.parent instanceof Types.ObjectId) {
|
||||
if ((data.parent as unknown) instanceof Types.ObjectId) {
|
||||
parentQuery.$or.push({
|
||||
parent: {
|
||||
$eq: data.parent.toString(),
|
||||
@@ -65,12 +67,12 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
})
|
||||
}
|
||||
|
||||
await VersionModel.updateMany(
|
||||
await VersionModel.collection.updateMany(
|
||||
{
|
||||
$and: [
|
||||
{
|
||||
_id: {
|
||||
$ne: doc._id,
|
||||
$ne: insertedID,
|
||||
},
|
||||
},
|
||||
parentQuery,
|
||||
@@ -81,22 +83,21 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
$lt: new Date(doc.updatedAt),
|
||||
$lt: new Date(data.updatedAt),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ $unset: { latest: 1 } },
|
||||
options,
|
||||
{ session },
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const verificationToken = doc._verificationToken
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import type { DeleteMany, PayloadRequest } from 'payload'
|
||||
import type { DeleteMany } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteMany: DeleteMany = async function deleteMany(
|
||||
this: MongooseAdapter,
|
||||
{ collection, req = {} as PayloadRequest, where },
|
||||
{ collection, req, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
}
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
payload: this.payload,
|
||||
session,
|
||||
where,
|
||||
})
|
||||
|
||||
await Model.deleteMany(query, options)
|
||||
await Model.collection.deleteMany(query, {
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
import type { DeleteOne, Document, PayloadRequest } from 'payload'
|
||||
import type { DeleteOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, req = {} as PayloadRequest, select, where },
|
||||
{ collection, req, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const options = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
payload: this.payload,
|
||||
session,
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, {
|
||||
...options,
|
||||
const fields = this.payload.collections[collection].config.flattenedFields
|
||||
|
||||
const doc = await Model.collection.findOneAndDelete(query, {
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
fields,
|
||||
select,
|
||||
}),
|
||||
}).lean()
|
||||
session,
|
||||
})
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import type { DeleteVersions, PayloadRequest } from 'payload'
|
||||
import type { DeleteVersions } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
|
||||
export const deleteVersions: DeleteVersions = async function deleteVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, where },
|
||||
{ collection, locale, req, where },
|
||||
) {
|
||||
const VersionsModel = this.versions[collection]
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
}
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await VersionsModel.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where,
|
||||
})
|
||||
|
||||
await VersionsModel.deleteMany(query, options)
|
||||
await VersionsModel.collection.deleteMany(query, {
|
||||
session,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { PaginateOptions } from 'mongoose'
|
||||
import type { Find, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
import type { CollationOptions } from 'mongodb'
|
||||
import type { Find } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { findMany } from './utilities/findMany.js'
|
||||
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const find: Find = async function find(
|
||||
this: MongooseAdapter,
|
||||
@@ -20,8 +20,7 @@ export const find: Find = async function find(
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
req = {} as PayloadRequest,
|
||||
req,
|
||||
select,
|
||||
sort: sortArg,
|
||||
where,
|
||||
@@ -29,20 +28,17 @@ export const find: Find = async function find(
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const options = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
const hasNearConstraint = getHasNearConstraint(where)
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
const fields = collectionConfig.flattenedFields
|
||||
|
||||
let sort
|
||||
if (!hasNearConstraint) {
|
||||
sort = buildSortParam({
|
||||
config: this.payload.config,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
fields,
|
||||
locale,
|
||||
sort: sortArg || collectionConfig.defaultSort,
|
||||
timestamps: true,
|
||||
@@ -52,88 +48,51 @@ export const find: Find = async function find(
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
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
|
||||
const paginationOptions: PaginateOptions = {
|
||||
lean: true,
|
||||
leanWithId: true,
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (select) {
|
||||
paginationOptions.projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
select,
|
||||
})
|
||||
}
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields,
|
||||
select,
|
||||
})
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
const collation: CollationOptions | undefined = this.collation
|
||||
? {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
|
||||
...this.collation,
|
||||
}
|
||||
: undefined
|
||||
|
||||
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
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
Model.countDocuments(query, {
|
||||
...options,
|
||||
hint: { _id: 1 },
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (limit >= 0) {
|
||||
paginationOptions.limit = limit
|
||||
// limit must also be set here, it's ignored when pagination is false
|
||||
paginationOptions.options.limit = limit
|
||||
|
||||
// Disable pagination if limit is 0
|
||||
if (limit === 0) {
|
||||
paginationOptions.pagination = false
|
||||
}
|
||||
}
|
||||
|
||||
let result
|
||||
|
||||
const aggregate = await buildJoinAggregation({
|
||||
const joinAgreggation = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
locale,
|
||||
query,
|
||||
session,
|
||||
})
|
||||
// build join aggregation
|
||||
if (aggregate) {
|
||||
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
|
||||
} else {
|
||||
result = await Model.paginate(query, paginationOptions)
|
||||
}
|
||||
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
const result = await findMany({
|
||||
adapter: this,
|
||||
collation,
|
||||
collection: Model.collection,
|
||||
joinAgreggation,
|
||||
limit,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
query,
|
||||
session,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
})
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
transform({ adapter: this, data: result.docs, fields, operation: 'read' })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
import type { FindGlobal, PayloadRequest } from 'payload'
|
||||
import type { FindGlobal } from 'payload'
|
||||
|
||||
import { combineQueries } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findGlobal: FindGlobal = async function findGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, locale, req = {} as PayloadRequest, select, where },
|
||||
{ slug, locale, req, select, where },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
select: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields,
|
||||
select,
|
||||
}),
|
||||
}
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
globalSlug: slug,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where: combineQueries({ globalType: { equals: slug } }, where),
|
||||
})
|
||||
|
||||
let doc = (await Model.findOne(query, {}, options)) as any
|
||||
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields
|
||||
|
||||
const doc = await Model.collection.findOne(query, {
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
})
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
if (doc._id) {
|
||||
doc.id = doc._id
|
||||
delete doc._id
|
||||
}
|
||||
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
doc = sanitizeInternalFields(doc)
|
||||
transform({ adapter: this, data: doc, fields, operation: 'read' })
|
||||
|
||||
return doc
|
||||
return doc as any
|
||||
}
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import type { PaginateOptions } from 'mongoose'
|
||||
import type { FindGlobalVersions, PayloadRequest } from 'payload'
|
||||
import type { CollationOptions } from 'mongodb'
|
||||
import type { FindGlobalVersions } from 'payload'
|
||||
|
||||
import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionGlobalFields } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { findMany } from './utilities/findMany.js'
|
||||
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
|
||||
this: MongooseAdapter,
|
||||
{
|
||||
global,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where,
|
||||
},
|
||||
{ global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where },
|
||||
) {
|
||||
const Model = this.versions[global]
|
||||
const versionFields = buildVersionGlobalFields(
|
||||
@@ -31,18 +22,8 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
this.payload.globals.config.find(({ slug }) => slug === global),
|
||||
true,
|
||||
)
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
limit,
|
||||
skip,
|
||||
}
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
const hasNearConstraint = getHasNearConstraint(where)
|
||||
|
||||
let sort
|
||||
if (!hasNearConstraint) {
|
||||
@@ -55,69 +36,49 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
})
|
||||
}
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
globalSlug: global,
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
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
|
||||
const paginationOptions: PaginateOptions = {
|
||||
lean: true,
|
||||
leanWithId: true,
|
||||
|
||||
const projection = buildProjectionFromSelect({ adapter: this, fields: versionFields, select })
|
||||
|
||||
const collation: CollationOptions | undefined = this.collation
|
||||
? {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
|
||||
...this.collation,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const result = await findMany({
|
||||
adapter: this,
|
||||
collation,
|
||||
collection: Model.collection,
|
||||
limit,
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }),
|
||||
projection,
|
||||
query,
|
||||
session,
|
||||
skip,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
})
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: versionFields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
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
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
Model.countDocuments(query, {
|
||||
...options,
|
||||
hint: { _id: 1 },
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (limit >= 0) {
|
||||
paginationOptions.limit = limit
|
||||
// limit must also be set here, it's ignored when pagination is false
|
||||
paginationOptions.options.limit = limit
|
||||
|
||||
// Disable pagination if limit is 0
|
||||
if (limit === 0) {
|
||||
paginationOptions.pagination = false
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,64 +1,76 @@
|
||||
import type { MongooseQueryOptions, QueryOptions } from 'mongoose'
|
||||
import type { Document, FindOne, PayloadRequest } from 'payload'
|
||||
import type { FindOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findOne: FindOne = async function findOne(
|
||||
this: MongooseAdapter,
|
||||
{ collection, joins, locale, req = {} as PayloadRequest, select, where },
|
||||
{ collection, joins, locale, req, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const options: MongooseQueryOptions = {
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
}
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where,
|
||||
})
|
||||
|
||||
const fields = collectionConfig.flattenedFields
|
||||
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
fields,
|
||||
select,
|
||||
})
|
||||
|
||||
const aggregate = await buildJoinAggregation({
|
||||
const joinAggregation = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit: 1,
|
||||
locale,
|
||||
projection,
|
||||
query,
|
||||
session,
|
||||
})
|
||||
|
||||
let doc
|
||||
if (aggregate) {
|
||||
;[doc] = await Model.aggregate(aggregate, options)
|
||||
if (joinAggregation) {
|
||||
const aggregation = Model.collection.aggregate(
|
||||
[
|
||||
{
|
||||
$match: query,
|
||||
},
|
||||
],
|
||||
{ session },
|
||||
)
|
||||
aggregation.limit(1)
|
||||
for (const stage of joinAggregation) {
|
||||
aggregation.addStage(stage)
|
||||
}
|
||||
|
||||
;[doc] = await aggregation.toArray()
|
||||
} else {
|
||||
;(options as Record<string, unknown>).projection = projection
|
||||
doc = await Model.findOne(query, {}, options)
|
||||
doc = await Model.collection.findOne(query, { projection, session })
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,44 +1,27 @@
|
||||
import type { PaginateOptions } from 'mongoose'
|
||||
import type { FindVersions, PayloadRequest } from 'payload'
|
||||
import type { CollationOptions } from 'mongodb'
|
||||
import type { FindVersions } from 'payload'
|
||||
|
||||
import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { findMany } from './utilities/findMany.js'
|
||||
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const findVersions: FindVersions = async function findVersions(
|
||||
this: MongooseAdapter,
|
||||
{
|
||||
collection,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
where,
|
||||
},
|
||||
{ collection, limit, locale, page, pagination, req = {}, select, skip, sort: sortArg, where },
|
||||
) {
|
||||
const Model = this.versions[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const options = {
|
||||
...(await withSession(this, req)),
|
||||
limit,
|
||||
skip,
|
||||
}
|
||||
|
||||
let hasNearConstraint = false
|
||||
const session = await getSession(this, req)
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
const hasNearConstraint = getHasNearConstraint(where)
|
||||
|
||||
let sort
|
||||
if (!hasNearConstraint) {
|
||||
@@ -54,69 +37,48 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where,
|
||||
})
|
||||
|
||||
const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
// 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
|
||||
const paginationOptions: PaginateOptions = {
|
||||
lean: true,
|
||||
leanWithId: true,
|
||||
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: versionFields,
|
||||
select,
|
||||
})
|
||||
|
||||
const collation: CollationOptions | undefined = this.collation
|
||||
? {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
|
||||
...this.collation,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const result = await findMany({
|
||||
adapter: this,
|
||||
collation,
|
||||
collection: Model.collection,
|
||||
limit,
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
select,
|
||||
}),
|
||||
projection,
|
||||
query,
|
||||
session,
|
||||
skip,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
})
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: versionFields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
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
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
Model.countDocuments(query, {
|
||||
...options,
|
||||
hint: { _id: 1 },
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (limit >= 0) {
|
||||
paginationOptions.limit = limit
|
||||
// limit must also be set here, it's ignored when pagination is false
|
||||
paginationOptions.options.limit = limit
|
||||
|
||||
// Disable pagination if limit is 0
|
||||
if (limit === 0) {
|
||||
paginationOptions.pagination = false
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import type { CollationOptions, TransactionOptions } from 'mongodb'
|
||||
import type { MongoMemoryReplSet } from 'mongodb-memory-server'
|
||||
import type { ClientSession, Connection, ConnectOptions, QueryOptions } from 'mongoose'
|
||||
import type {
|
||||
ClientSession,
|
||||
Connection,
|
||||
ConnectOptions,
|
||||
QueryOptions,
|
||||
SchemaOptions,
|
||||
} from 'mongoose'
|
||||
import type {
|
||||
BaseDatabaseAdapter,
|
||||
CollectionSlug,
|
||||
DatabaseAdapterObj,
|
||||
Payload,
|
||||
TypeWithID,
|
||||
@@ -52,6 +59,8 @@ import { upsert } from './upsert.js'
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
|
||||
|
||||
export { transform } from './utilities/transform.js'
|
||||
|
||||
export interface Args {
|
||||
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
|
||||
autoPluralization?: boolean
|
||||
@@ -79,12 +88,13 @@ export interface Args {
|
||||
* Defaults to disabled.
|
||||
*/
|
||||
collation?: Omit<CollationOptions, 'locale'>
|
||||
collectionsSchemaOptions?: Partial<Record<CollectionSlug, SchemaOptions>>
|
||||
|
||||
/** Extra configuration options */
|
||||
connectOptions?: {
|
||||
/** 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
|
||||
/**
|
||||
@@ -103,6 +113,7 @@ export interface Args {
|
||||
up: (args: MigrateUpArgs) => Promise<void>
|
||||
}[]
|
||||
transactionOptions?: false | TransactionOptions
|
||||
|
||||
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
|
||||
url: false | string
|
||||
}
|
||||
@@ -163,6 +174,7 @@ declare module 'payload' {
|
||||
|
||||
export function mongooseAdapter({
|
||||
autoPluralization = true,
|
||||
collectionsSchemaOptions = {},
|
||||
connectOptions,
|
||||
disableIndexHints = false,
|
||||
ensureIndexes,
|
||||
@@ -194,6 +206,7 @@ export function mongooseAdapter({
|
||||
versions: {},
|
||||
// DatabaseAdapter
|
||||
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
||||
collectionsSchemaOptions,
|
||||
commitTransaction,
|
||||
connect,
|
||||
count,
|
||||
|
||||
@@ -17,7 +17,9 @@ import { getDBName } from './utilities/getDBName.js'
|
||||
|
||||
export const init: Init = function init(this: MongooseAdapter) {
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const schema = buildCollectionSchema(collection, this.payload)
|
||||
const schemaOptions = this.collectionsSchemaOptions[collection.slug]
|
||||
|
||||
const schema = buildCollectionSchema(collection, this.payload, schemaOptions)
|
||||
|
||||
if (collection.versions) {
|
||||
const versionModelName = getDBName({ config: collection, versions: true })
|
||||
@@ -32,6 +34,7 @@ export const init: Init = function init(this: MongooseAdapter) {
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
},
|
||||
...schemaOptions,
|
||||
})
|
||||
|
||||
versionSchema.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true }).plugin(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
|
||||
import prompts from 'prompts'
|
||||
|
||||
@@ -45,7 +43,7 @@ export async function migrateFresh(
|
||||
msg: `Found ${migrationFiles.length} migration files.`,
|
||||
})
|
||||
|
||||
const req = { payload } as PayloadRequest
|
||||
const req = { payload }
|
||||
|
||||
// Run all migrate up
|
||||
for (const migration of migrationFiles) {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type { ClientSession, Model } from 'mongoose'
|
||||
import type { Field, PayloadRequest, SanitizedConfig } from 'payload'
|
||||
import type { Field, FlattenedField, PayloadRequest } from 'payload'
|
||||
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from '../withSession.js'
|
||||
import { getSession } from '../utilities/getSession.js'
|
||||
import { transform } from '../utilities/transform.js'
|
||||
|
||||
const migrateModelWithBatching = async ({
|
||||
adapter,
|
||||
batchSize,
|
||||
config,
|
||||
fields,
|
||||
Model,
|
||||
session,
|
||||
}: {
|
||||
adapter: MongooseAdapter
|
||||
batchSize: number
|
||||
config: SanitizedConfig
|
||||
fields: Field[]
|
||||
fields: FlattenedField[]
|
||||
Model: Model<any>
|
||||
session: ClientSession
|
||||
}): Promise<void> => {
|
||||
@@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({
|
||||
}
|
||||
|
||||
for (const doc of docs) {
|
||||
sanitizeRelationshipIDs({ config, data: doc, fields })
|
||||
transform({ adapter, data: doc, fields, operation: 'update', validateRelationships: false })
|
||||
}
|
||||
|
||||
await Model.collection.bulkWrite(
|
||||
@@ -109,15 +109,15 @@ export async function migrateRelationshipsV2_V3({
|
||||
const db = payload.db as MongooseAdapter
|
||||
const config = payload.config
|
||||
|
||||
const { session } = await withSession(db, req)
|
||||
const session = await getSession(db, req)
|
||||
|
||||
for (const collection of payload.config.collections.filter(hasRelationshipOrUploadField)) {
|
||||
payload.logger.info(`Migrating collection "${collection.slug}"`)
|
||||
|
||||
await migrateModelWithBatching({
|
||||
adapter: db,
|
||||
batchSize,
|
||||
config,
|
||||
fields: collection.fields,
|
||||
fields: collection.flattenedFields,
|
||||
Model: db.collections[collection.slug],
|
||||
session,
|
||||
})
|
||||
@@ -128,9 +128,9 @@ export async function migrateRelationshipsV2_V3({
|
||||
payload.logger.info(`Migrating collection versions "${collection.slug}"`)
|
||||
|
||||
await migrateModelWithBatching({
|
||||
adapter: db,
|
||||
batchSize,
|
||||
config,
|
||||
fields: buildVersionCollectionFields(config, collection),
|
||||
fields: buildVersionCollectionFields(config, collection, true),
|
||||
Model: db.versions[collection.slug],
|
||||
session,
|
||||
})
|
||||
@@ -156,7 +156,13 @@ export async function migrateRelationshipsV2_V3({
|
||||
|
||||
// in case if the global doesn't exist in the database yet (not saved)
|
||||
if (doc) {
|
||||
sanitizeRelationshipIDs({ config, data: doc, fields: global.fields })
|
||||
transform({
|
||||
adapter: db,
|
||||
data: doc,
|
||||
fields: global.flattenedFields,
|
||||
operation: 'update',
|
||||
validateRelationships: false,
|
||||
})
|
||||
|
||||
await GlobalsModel.collection.updateOne(
|
||||
{
|
||||
@@ -173,9 +179,9 @@ export async function migrateRelationshipsV2_V3({
|
||||
payload.logger.info(`Migrating global versions "${global.slug}"`)
|
||||
|
||||
await migrateModelWithBatching({
|
||||
adapter: db,
|
||||
batchSize,
|
||||
config,
|
||||
fields: buildVersionGlobalFields(config, global),
|
||||
fields: buildVersionGlobalFields(config, global, true),
|
||||
Model: db.versions[global.slug],
|
||||
session,
|
||||
})
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { Payload, PayloadRequest } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { withSession } from '../withSession.js'
|
||||
import { getSession } from '../utilities/getSession.js'
|
||||
|
||||
export async function migrateVersionsV1_V2({ req }: { req: PayloadRequest }) {
|
||||
const { payload } = req
|
||||
|
||||
const { session } = await withSession(payload.db as MongooseAdapter, req)
|
||||
const session = await getSession(payload.db as MongooseAdapter, req)
|
||||
|
||||
// For each collection
|
||||
|
||||
@@ -106,12 +106,12 @@ async function migrateCollectionDocs({
|
||||
return
|
||||
}
|
||||
|
||||
const remainingDocIds = remainingDocs.map((doc) => doc._versionID)
|
||||
const remainingDocIDs = remainingDocs.map((doc) => doc._versionID)
|
||||
|
||||
await VersionsModel.updateMany(
|
||||
{
|
||||
_id: {
|
||||
$in: remainingDocIds,
|
||||
$in: remainingDocIDs,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ClientSession } from 'mongodb'
|
||||
import type { FlattenedField, Payload, Where } from 'payload'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
@@ -8,6 +9,7 @@ export async function buildAndOrConditions({
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
session,
|
||||
where,
|
||||
}: {
|
||||
collectionSlug?: string
|
||||
@@ -15,6 +17,7 @@ export async function buildAndOrConditions({
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
session?: ClientSession
|
||||
where: Where[]
|
||||
}): Promise<Record<string, unknown>[]> {
|
||||
const completedConditions = []
|
||||
@@ -30,6 +33,7 @@ export async function buildAndOrConditions({
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
session,
|
||||
where: condition,
|
||||
})
|
||||
if (Object.keys(result).length > 0) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ClientSession } from 'mongodb'
|
||||
import type { FlattenedField, Payload, Where } from 'payload'
|
||||
|
||||
import { QueryError } from 'payload'
|
||||
|
||||
import { parseParams } from './parseParams.js'
|
||||
|
||||
type GetBuildQueryPluginArgs = {
|
||||
@@ -13,6 +12,7 @@ export type BuildQueryArgs = {
|
||||
globalSlug?: string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
session?: ClientSession
|
||||
where: Where
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export const getBuildQueryPlugin = ({
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
session,
|
||||
where,
|
||||
}: BuildQueryArgs): Promise<Record<string, unknown>> {
|
||||
let fields = versionsFields
|
||||
@@ -41,20 +42,17 @@ export const getBuildQueryPlugin = ({
|
||||
fields = collectionConfig.flattenedFields
|
||||
}
|
||||
}
|
||||
const errors = []
|
||||
|
||||
const result = await parseParams({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
session,
|
||||
where,
|
||||
})
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new QueryError(errors)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
modifiedSchema.statics.buildQuery = buildQuery
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ClientSession, FindOptions } from 'mongodb'
|
||||
import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
@@ -15,9 +16,11 @@ type SearchParam = {
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
const subQueryOptions = {
|
||||
lean: true,
|
||||
const subQueryOptions: FindOptions = {
|
||||
limit: 50,
|
||||
projection: {
|
||||
_id: true,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +34,7 @@ export async function buildSearchParam({
|
||||
locale,
|
||||
operator,
|
||||
payload,
|
||||
session,
|
||||
val,
|
||||
}: {
|
||||
collectionSlug?: string
|
||||
@@ -40,6 +44,7 @@ export async function buildSearchParam({
|
||||
locale?: string
|
||||
operator: string
|
||||
payload: Payload
|
||||
session?: ClientSession
|
||||
val: unknown
|
||||
}): Promise<SearchParam> {
|
||||
// Replace GraphQL nested field double underscore formatting
|
||||
@@ -87,6 +92,7 @@ export async function buildSearchParam({
|
||||
const sanitizedQueryValue = sanitizeQueryValue({
|
||||
field,
|
||||
hasCustomID,
|
||||
locale,
|
||||
operator,
|
||||
path,
|
||||
payload,
|
||||
@@ -133,17 +139,14 @@ export async function buildSearchParam({
|
||||
},
|
||||
})
|
||||
|
||||
const result = await SubModel.find(subQuery, subQueryOptions)
|
||||
const result = await SubModel.collection
|
||||
.find(subQuery, { session, ...subQueryOptions })
|
||||
.toArray()
|
||||
|
||||
const $in: unknown[] = []
|
||||
|
||||
result.forEach((doc) => {
|
||||
const stringID = doc._id.toString()
|
||||
$in.push(stringID)
|
||||
|
||||
if (Types.ObjectId.isValid(stringID)) {
|
||||
$in.push(doc._id)
|
||||
}
|
||||
$in.push(doc._id)
|
||||
})
|
||||
|
||||
if (pathsToQuery.length === 1) {
|
||||
@@ -161,7 +164,9 @@ export async function buildSearchParam({
|
||||
}
|
||||
|
||||
const subQuery = priorQueryResult.value
|
||||
const result = await SubModel.find(subQuery, subQueryOptions)
|
||||
const result = await SubModel.collection
|
||||
.find(subQuery, { session, ...subQueryOptions })
|
||||
.toArray()
|
||||
|
||||
const $in = result.map((doc) => doc._id)
|
||||
|
||||
|
||||
@@ -11,20 +11,13 @@ type Args = {
|
||||
timestamps: boolean
|
||||
}
|
||||
|
||||
export type SortArgs = {
|
||||
direction: SortDirection
|
||||
property: string
|
||||
}[]
|
||||
|
||||
export type SortDirection = 'asc' | 'desc'
|
||||
|
||||
export const buildSortParam = ({
|
||||
config,
|
||||
fields,
|
||||
locale,
|
||||
sort,
|
||||
timestamps,
|
||||
}: Args): PaginateOptions['sort'] => {
|
||||
}: Args): Record<string, -1 | 1> => {
|
||||
if (!sort) {
|
||||
if (timestamps) {
|
||||
sort = '-createdAt'
|
||||
@@ -37,15 +30,15 @@ export const buildSortParam = ({
|
||||
sort = [sort]
|
||||
}
|
||||
|
||||
const sorting = sort.reduce<PaginateOptions['sort']>((acc, item) => {
|
||||
const sorting = sort.reduce<Record<string, -1 | 1>>((acc, item) => {
|
||||
let sortProperty: string
|
||||
let sortDirection: SortDirection
|
||||
let sortDirection: -1 | 1
|
||||
if (item.indexOf('-') === 0) {
|
||||
sortProperty = item.substring(1)
|
||||
sortDirection = 'desc'
|
||||
sortDirection = -1
|
||||
} else {
|
||||
sortProperty = item
|
||||
sortDirection = 'asc'
|
||||
sortDirection = 1
|
||||
}
|
||||
if (sortProperty === 'id') {
|
||||
acc['_id'] = sortDirection
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FlattenedField, SanitizedConfig } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
config: SanitizedConfig
|
||||
@@ -33,7 +33,7 @@ export const getLocalizedSortProperty = ({
|
||||
(field) => fieldAffectsData(field) && field.name === firstSegment,
|
||||
)
|
||||
|
||||
if (matchedField && !fieldIsPresentationalOnly(matchedField)) {
|
||||
if (matchedField) {
|
||||
let nextFields: FlattenedField[]
|
||||
const remainingSegments = [...segments]
|
||||
let localizedSegment = matchedField.name
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ClientSession } from 'mongodb'
|
||||
import type { FilterQuery } from 'mongoose'
|
||||
import type { FlattenedField, Operator, Payload, Where } from 'payload'
|
||||
|
||||
@@ -13,6 +14,7 @@ export async function parseParams({
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
session,
|
||||
where,
|
||||
}: {
|
||||
collectionSlug?: string
|
||||
@@ -20,6 +22,7 @@ export async function parseParams({
|
||||
globalSlug?: string
|
||||
locale: string
|
||||
payload: Payload
|
||||
session?: ClientSession
|
||||
where: Where
|
||||
}): Promise<Record<string, unknown>> {
|
||||
let result = {} as FilterQuery<any>
|
||||
@@ -62,6 +65,7 @@ export async function parseParams({
|
||||
locale,
|
||||
operator,
|
||||
payload,
|
||||
session,
|
||||
val: pathOperators[operator],
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createArrayFromCommaDelineated } from 'payload'
|
||||
type SanitizeQueryValueArgs = {
|
||||
field: FlattenedField
|
||||
hasCustomID: boolean
|
||||
locale?: string
|
||||
operator: string
|
||||
path: string
|
||||
payload: Payload
|
||||
@@ -36,6 +37,22 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeCoordinates = (coordinates: unknown[]): unknown[] => {
|
||||
const result: unknown[] = []
|
||||
|
||||
for (const value of coordinates) {
|
||||
if (typeof value === 'string') {
|
||||
result.push(Number(value))
|
||||
} else if (Array.isArray(value)) {
|
||||
result.push(sanitizeCoordinates(value))
|
||||
} else {
|
||||
result.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships
|
||||
const getFieldFromSegments = ({
|
||||
field,
|
||||
@@ -74,6 +91,7 @@ const getFieldFromSegments = ({
|
||||
export const sanitizeQueryValue = ({
|
||||
field,
|
||||
hasCustomID,
|
||||
locale,
|
||||
operator,
|
||||
path,
|
||||
payload,
|
||||
@@ -205,11 +223,34 @@ export const sanitizeQueryValue = ({
|
||||
formattedValue.value = new Types.ObjectId(value)
|
||||
}
|
||||
|
||||
let localizedPath = path
|
||||
|
||||
if (field.localized && payload.config.localization && locale) {
|
||||
localizedPath = `${path}.${locale}`
|
||||
}
|
||||
|
||||
return {
|
||||
rawQuery: {
|
||||
$and: [
|
||||
{ [`${path}.value`]: { $eq: formattedValue.value } },
|
||||
{ [`${path}.relationTo`]: { $eq: formattedValue.relationTo } },
|
||||
$or: [
|
||||
{
|
||||
[localizedPath]: {
|
||||
$eq: {
|
||||
// disable auto sort
|
||||
/* eslint-disable */
|
||||
value: formattedValue.value,
|
||||
relationTo: formattedValue.relationTo,
|
||||
/* eslint-enable */
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
[localizedPath]: {
|
||||
$eq: {
|
||||
relationTo: formattedValue.relationTo,
|
||||
value: formattedValue.value,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -334,6 +375,14 @@ export const sanitizeQueryValue = ({
|
||||
}
|
||||
|
||||
if (operator === 'within' || operator === 'intersects') {
|
||||
if (
|
||||
formattedValue &&
|
||||
typeof formattedValue === 'object' &&
|
||||
Array.isArray(formattedValue.coordinates)
|
||||
) {
|
||||
formattedValue.coordinates = sanitizeCoordinates(formattedValue.coordinates)
|
||||
}
|
||||
|
||||
formattedValue = {
|
||||
$geometry: formattedValue,
|
||||
}
|
||||
|
||||
@@ -1,43 +1,29 @@
|
||||
import type { PaginateOptions } from 'mongoose'
|
||||
import type { PayloadRequest, QueryDrafts } from 'payload'
|
||||
import type { CollationOptions } from 'mongodb'
|
||||
import type { QueryDrafts } from 'payload'
|
||||
|
||||
import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload'
|
||||
import { buildVersionCollectionFields, combineQueries } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { findMany } from './utilities/findMany.js'
|
||||
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
this: MongooseAdapter,
|
||||
{
|
||||
collection,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
sort: sortArg,
|
||||
where,
|
||||
},
|
||||
{ collection, joins, limit, locale, page, pagination, req, select, sort: sortArg, where },
|
||||
) {
|
||||
const VersionModel = this.versions[collection]
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
const options = await withSession(this, req)
|
||||
const session = await getSession(this, req)
|
||||
|
||||
let hasNearConstraint
|
||||
const hasNearConstraint = getHasNearConstraint(where)
|
||||
let sort
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
if (!hasNearConstraint) {
|
||||
sort = buildSortParam({
|
||||
config: this.payload.config,
|
||||
@@ -53,95 +39,65 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
const versionQuery = await VersionModel.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where: combinedWhere,
|
||||
})
|
||||
|
||||
const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
|
||||
const projection = buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
fields: versionFields,
|
||||
select,
|
||||
})
|
||||
// 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 || !versionQuery || Object.keys(versionQuery).length === 0
|
||||
const paginationOptions: PaginateOptions = {
|
||||
lean: true,
|
||||
leanWithId: true,
|
||||
options,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
const collation: CollationOptions | undefined = this.collation
|
||||
? {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en',
|
||||
...this.collation,
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (
|
||||
!useEstimatedCount &&
|
||||
Object.keys(versionQuery).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
|
||||
paginationOptions.useCustomCountFn = () => {
|
||||
return Promise.resolve(
|
||||
VersionModel.countDocuments(versionQuery, {
|
||||
hint: { _id: 1 },
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (limit > 0) {
|
||||
paginationOptions.limit = limit
|
||||
// limit must also be set here, it's ignored when pagination is false
|
||||
paginationOptions.options.limit = limit
|
||||
}
|
||||
|
||||
let result
|
||||
|
||||
const aggregate = await buildJoinAggregation({
|
||||
const joinAgreggation = await buildJoinAggregation({
|
||||
adapter: this,
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
locale,
|
||||
projection,
|
||||
query: versionQuery,
|
||||
session,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
// build join aggregation
|
||||
if (aggregate) {
|
||||
result = await VersionModel.aggregatePaginate(
|
||||
VersionModel.aggregate(aggregate),
|
||||
paginationOptions,
|
||||
)
|
||||
} else {
|
||||
result = await VersionModel.paginate(versionQuery, paginationOptions)
|
||||
const result = await findMany({
|
||||
adapter: this,
|
||||
collation,
|
||||
collection: VersionModel.collection,
|
||||
joinAgreggation,
|
||||
limit,
|
||||
page,
|
||||
pagination,
|
||||
projection,
|
||||
query: versionQuery,
|
||||
session,
|
||||
sort,
|
||||
useEstimatedCount,
|
||||
})
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result.docs,
|
||||
fields: versionFields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
for (let i = 0; i < result.docs.length; i++) {
|
||||
const id = result.docs[i].parent
|
||||
result.docs[i] = result.docs[i].version
|
||||
result.docs[i].id = id
|
||||
}
|
||||
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
doc = {
|
||||
_id: doc.parent,
|
||||
id: doc.parent,
|
||||
...doc.version,
|
||||
}
|
||||
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { PayloadRequest, UpdateGlobal } from 'payload'
|
||||
import type { UpdateGlobal } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
this: MongooseAdapter,
|
||||
{ slug, data, options: optionsArgs = {}, req = {} as PayloadRequest, select },
|
||||
{ slug, data, options: optionsArgs = {}, req, select },
|
||||
) {
|
||||
const Model = this.globals
|
||||
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
|
||||
const fields = this.payload.config.globals.find((global) => global.slug === slug).flattenedFields
|
||||
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.config.globals.find((global) => global.slug === slug).flattenedFields,
|
||||
select,
|
||||
}),
|
||||
}
|
||||
const session = await getSession(this, req)
|
||||
|
||||
let result
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'update',
|
||||
timestamps: optionsArgs.timestamps !== false,
|
||||
})
|
||||
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
|
||||
const result: any = await Model.collection.findOneAndUpdate(
|
||||
{ globalType: slug },
|
||||
{ $set: data },
|
||||
{
|
||||
...optionsArgs,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
|
||||
returnDocument: 'after',
|
||||
session,
|
||||
},
|
||||
)
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
|
||||
import {
|
||||
buildVersionGlobalFields,
|
||||
type PayloadRequest,
|
||||
type TypeWithID,
|
||||
type UpdateGlobalVersionArgs,
|
||||
} from 'payload'
|
||||
import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
this: MongooseAdapter,
|
||||
@@ -20,7 +13,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
global: globalSlug,
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req = {} as PayloadRequest,
|
||||
req,
|
||||
select,
|
||||
versionData,
|
||||
where,
|
||||
@@ -28,44 +21,50 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
) {
|
||||
const VersionModel = this.versions[globalSlug]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
const fields = buildVersionGlobalFields(
|
||||
this.payload.config,
|
||||
this.payload.config.globals.find((global) => global.slug === globalSlug),
|
||||
true,
|
||||
)
|
||||
|
||||
const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
|
||||
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
|
||||
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true),
|
||||
select,
|
||||
}),
|
||||
}
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
transform({
|
||||
adapter: this,
|
||||
data: versionData,
|
||||
fields,
|
||||
operation: 'update',
|
||||
timestamps: optionsArgs.timestamps !== false,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
const doc: any = await VersionModel.collection.findOneAndUpdate(
|
||||
query,
|
||||
{ $set: versionData },
|
||||
{
|
||||
...optionsArgs,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields,
|
||||
select,
|
||||
}),
|
||||
returnDocument: 'after',
|
||||
session,
|
||||
},
|
||||
)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -1,65 +1,57 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { PayloadRequest, UpdateOne } from 'payload'
|
||||
import type { UpdateOne } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateOne: UpdateOne = async function updateOne(
|
||||
this: MongooseAdapter,
|
||||
{
|
||||
id,
|
||||
collection,
|
||||
data,
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
where: whereArg,
|
||||
},
|
||||
{ id, collection, data, locale, options: optionsArgs = {}, req, select, where: whereArg },
|
||||
) {
|
||||
const where = id ? { id: { equals: id } } : whereArg
|
||||
const Model = this.collections[collection]
|
||||
const fields = this.payload.collections[collection].config.fields
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
select,
|
||||
}),
|
||||
}
|
||||
const fields = this.payload.collections[collection].config.flattenedFields
|
||||
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where,
|
||||
})
|
||||
|
||||
let result
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
transform({
|
||||
adapter: this,
|
||||
data,
|
||||
fields,
|
||||
operation: 'update',
|
||||
timestamps: optionsArgs.timestamps !== false,
|
||||
})
|
||||
|
||||
try {
|
||||
result = await Model.findOneAndUpdate(query, sanitizedData, options)
|
||||
const result = await Model.collection.findOneAndUpdate(
|
||||
query,
|
||||
{ $set: data },
|
||||
{
|
||||
...optionsArgs,
|
||||
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
|
||||
returnDocument: 'after',
|
||||
session,
|
||||
},
|
||||
)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
return result
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,71 +1,65 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
|
||||
import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload'
|
||||
import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
|
||||
import { withSession } from './withSession.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
this: MongooseAdapter,
|
||||
{
|
||||
id,
|
||||
collection,
|
||||
locale,
|
||||
options: optionsArgs = {},
|
||||
req = {} as PayloadRequest,
|
||||
select,
|
||||
versionData,
|
||||
where,
|
||||
},
|
||||
{ id, collection, locale, options: optionsArgs = {}, req, select, versionData, where },
|
||||
) {
|
||||
const VersionModel = this.versions[collection]
|
||||
const whereToUse = where || { id: { equals: id } }
|
||||
const fields = buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
)
|
||||
|
||||
const options: QueryOptions = {
|
||||
...optionsArgs,
|
||||
...(await withSession(this, req)),
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
select,
|
||||
}),
|
||||
}
|
||||
const session = await getSession(this, req)
|
||||
|
||||
const query = await VersionModel.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
session,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const sanitizedData = sanitizeRelationshipIDs({
|
||||
config: this.payload.config,
|
||||
transform({
|
||||
adapter: this,
|
||||
data: versionData,
|
||||
fields,
|
||||
operation: 'update',
|
||||
timestamps: optionsArgs.timestamps !== false,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
|
||||
const doc = await VersionModel.collection.findOneAndUpdate(
|
||||
query,
|
||||
{ $set: versionData },
|
||||
{
|
||||
...optionsArgs,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: buildVersionCollectionFields(
|
||||
this.payload.config,
|
||||
this.payload.collections[collection].config,
|
||||
true,
|
||||
),
|
||||
select,
|
||||
}),
|
||||
returnDocument: 'after',
|
||||
session,
|
||||
},
|
||||
)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
transform({
|
||||
adapter: this,
|
||||
data: doc,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return doc as any
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PayloadRequest, Upsert } from 'payload'
|
||||
import type { Upsert } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
export const upsert: Upsert = async function upsert(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, locale, req = {} as PayloadRequest, select, where },
|
||||
{ collection, data, locale, req, select, where },
|
||||
) {
|
||||
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, select, where })
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ClientSession } from 'mongodb'
|
||||
import type { PipelineStage } from 'mongoose'
|
||||
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
||||
|
||||
@@ -10,12 +11,9 @@ type BuildJoinAggregationArgs = {
|
||||
collection: CollectionSlug
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
joins: JoinQuery
|
||||
// the number of docs to get at the top collection level
|
||||
limit?: number
|
||||
locale: string
|
||||
projection?: Record<string, true>
|
||||
// the where clause for the top collection
|
||||
query?: Where
|
||||
session?: ClientSession
|
||||
/** whether the query is from drafts */
|
||||
versions?: boolean
|
||||
}
|
||||
@@ -25,10 +23,9 @@ export const buildJoinAggregation = async ({
|
||||
collection,
|
||||
collectionConfig,
|
||||
joins,
|
||||
limit,
|
||||
locale,
|
||||
projection,
|
||||
query,
|
||||
session,
|
||||
versions,
|
||||
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
|
||||
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
|
||||
@@ -36,23 +33,7 @@ export const buildJoinAggregation = async ({
|
||||
}
|
||||
|
||||
const joinConfig = adapter.payload.collections[collection].config.joins
|
||||
const aggregate: PipelineStage[] = [
|
||||
{
|
||||
$sort: { createdAt: -1 },
|
||||
},
|
||||
]
|
||||
|
||||
if (query) {
|
||||
aggregate.push({
|
||||
$match: query,
|
||||
})
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
aggregate.push({
|
||||
$limit: limit,
|
||||
})
|
||||
}
|
||||
const aggregate: PipelineStage[] = []
|
||||
|
||||
for (const slug of Object.keys(joinConfig)) {
|
||||
for (const join of joinConfig[slug]) {
|
||||
@@ -72,26 +53,25 @@ export const buildJoinAggregation = async ({
|
||||
where: whereJoin,
|
||||
} = joins?.[join.joinPath] || {}
|
||||
|
||||
const sort = buildSortParam({
|
||||
const $sort = buildSortParam({
|
||||
config: adapter.payload.config,
|
||||
fields: adapter.payload.collections[slug].config.flattenedFields,
|
||||
locale,
|
||||
sort: sortJoin,
|
||||
timestamps: true,
|
||||
})
|
||||
const sortProperty = Object.keys(sort)[0]
|
||||
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
|
||||
|
||||
const $match = await joinModel.buildQuery({
|
||||
locale,
|
||||
payload: adapter.payload,
|
||||
session,
|
||||
where: whereJoin,
|
||||
})
|
||||
|
||||
const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [
|
||||
{ $match },
|
||||
{
|
||||
$sort: { [sortProperty]: sortDirection },
|
||||
$sort,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -101,6 +81,11 @@ export const buildJoinAggregation = async ({
|
||||
})
|
||||
}
|
||||
|
||||
let polymorphicSuffix = ''
|
||||
if (Array.isArray(join.targetField.relationTo)) {
|
||||
polymorphicSuffix = '.value'
|
||||
}
|
||||
|
||||
if (adapter.payload.config.localization && locale === 'all') {
|
||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
||||
@@ -109,7 +94,7 @@ export const buildJoinAggregation = async ({
|
||||
{
|
||||
$lookup: {
|
||||
as: `${as}.docs`,
|
||||
foreignField: `${join.field.on}${code}`,
|
||||
foreignField: `${join.field.on}${code}${polymorphicSuffix}`,
|
||||
from: adapter.collections[slug].collection.name,
|
||||
localField: versions ? 'parent' : '_id',
|
||||
pipeline,
|
||||
@@ -150,7 +135,7 @@ export const buildJoinAggregation = async ({
|
||||
{
|
||||
$lookup: {
|
||||
as: `${as}.docs`,
|
||||
foreignField: `${join.field.on}${localeSuffix}`,
|
||||
foreignField: `${join.field.on}${localeSuffix}${polymorphicSuffix}`,
|
||||
from: adapter.collections[slug].collection.name,
|
||||
localField: versions ? 'parent' : '_id',
|
||||
pipeline,
|
||||
@@ -184,8 +169,8 @@ export const buildJoinAggregation = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (projection) {
|
||||
aggregate.push({ $project: projection })
|
||||
if (!aggregate.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return aggregate
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user