Compare commits

..

94 Commits

Author SHA1 Message Date
Elliot DeNolf
d08e85d08c chore(release): plugin-nested-docs/1.0.11 [skip ci] 2024-01-09 16:32:02 -05:00
Elliot DeNolf
abcbf9974d chore(release): plugin-form-builder/1.1.1 [skip ci] 2024-01-09 16:31:52 -05:00
Elliot DeNolf
d01437d212 chore(release): plugin-seo/1.1.0 [skip ci] 2024-01-09 16:31:01 -05:00
Elliot DeNolf
06729a0a73 chore(release): db-postgres/0.3.0 [skip ci] 2024-01-09 16:30:49 -05:00
Elliot DeNolf
2bd7822a16 chore(release): db-mongodb/1.3.0 [skip ci] 2024-01-09 16:30:37 -05:00
Elliot DeNolf
bc7daf6b49 chore(release): payload/2.7.0 [skip ci] 2024-01-09 16:28:34 -05:00
James Mikrut
feab679ef7 Merge pull request #4615 from payloadcms/fix/relationship-field-number-ids-untitled
fix: relations with number based ids (postgres) show untitled ID: x
2024-01-09 15:30:41 -05:00
James Mikrut
be39ed4317 Merge pull request #4633 from payloadcms/fix/#3839-postgres-exist-json
fix(db-postgres): incorrect results querying json field using exists
2024-01-09 15:27:22 -05:00
James Mikrut
570e192eb4 Merge pull request #4741 from payloadcms/fix/#4591-migrate-down-batches
fix(db-postgres): migrate down not limited to latest batch
2024-01-09 13:50:45 -05:00
James Mikrut
22f4967dd4 Merge pull request #4726 from payloadcms/feat/migration-transactions
feat: improve transaction support by passing req to migrations
2024-01-09 13:50:00 -05:00
James Mikrut
4873c36129 Merge pull request #4722 from payloadcms/fix/#4719-migration-transaction-options
fix(db-mongodb): migration errors with transactionOptions false
2024-01-09 13:46:06 -05:00
James Mikrut
f0ec21cdda Merge pull request #4624 from payloadcms/fix/#3692-plugin-nested-docs-overrides
fix(plugin-nested-docs): custom overrides of breadcrumb and parent fields
2024-01-09 13:45:46 -05:00
dependabot[bot]
da737bdf8e chore(deps): bump follow-redirects in /examples/testing (#4735)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:18:29 -05:00
dependabot[bot]
40508880c1 chore(deps): bump follow-redirects in /templates/blank (#4736)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:18:18 -05:00
dependabot[bot]
8f420d841a chore(deps): bump follow-redirects in /templates/website (#4737)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:18:08 -05:00
dependabot[bot]
9022e27308 chore(deps): bump follow-redirects in /templates/ecommerce (#4734)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 13:17:59 -05:00
Patrik
acf2e41312 docs: updates verify to verifyEmail in local api (#4728) 2024-01-09 13:15:31 -05:00
Dan Ribbens
6acfae8ee7 fix(db-postgres): migrate down only runs latest batch size 2024-01-09 10:21:45 -05:00
Alessio Gravili
20bdd91da4 chore: upgrade @types/nodemailer from v6.4.8 to v6.4.14 (#4733) 2024-01-09 14:19:45 +01:00
Alessio Gravili
50502834c9 chore(richtext-lexical): upgrade lexical from v0.12.5 to v0.12.6 (#4732)
* chore(richtext-lexical): upgrade all lexical packages from 0.12.5 to 0.12.6

* fix(richtext-lexical): fix TypeScript errors

* fix indenting
2024-01-09 09:35:18 +01:00
Dan Ribbens
983733ad74 merge main 2024-01-08 17:12:02 -05:00
Dan Ribbens
555d02769a feat(db-postgres): improve transaction support by passing req to migrations 2024-01-08 15:50:22 -05:00
Dan Ribbens
682eca2186 feat(db-mongodb): improve transaction support by passing req to migrations 2024-01-08 15:50:09 -05:00
Dan Ribbens
1d14d9f8b8 feat: improve transaction support by passing req to migrations 2024-01-08 12:34:55 -05:00
Dan Ribbens
0abaddc2ef chore: remove payload migration endpoints 2024-01-08 12:24:53 -05:00
Dan Ribbens
21b9453cf4 fix(db-mongodb): migration error calling beginTransaction with transactionOptions false 2024-01-08 12:03:45 -05:00
Alessio Gravili
136993ec2b chore: undo adding DocumentInfo state to ActionsProvider (#4707) 2024-01-05 22:58:16 +01:00
Dan Ribbens
63bc4cabe1 fix(db-mongodb): querying plan for collections ignoring indexes (#4655)
* fix(db-mongodb): querying plan for collections ignoring indexes

* chore(db-mongodb): improve index hint comment
2024-01-05 16:21:19 -05:00
Alessio Gravili
6a8a6e4ef4 feat: provide document info to ActionsProvider (#4696) 2024-01-05 21:12:21 +01:00
Jessica Chowdhury
9828772890 fix: prevents row overflow (#4704) 2024-01-05 15:04:30 -05:00
Jessica Chowdhury
6116573164 docs: corrects type in useField example (#4705) 2024-01-05 15:03:38 -05:00
Dan Ribbens
cab6babd60 fix(db-postgres): validation prevents group fields in blocks (#4699)
* fix(db-postgres): validation prevents using group fields within blocks

* fix(db-postgres): validation of non-matching blocks in reverse order
2024-01-05 15:03:08 -05:00
Paul
55399424a1 fix(plugin-nested-docs): children wrongly publishing draft data (#4692)
* fix(plugin-nested-docs): handles child parent doc publishing

* Add tests

* Fix error failing on save hook

---------

Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
2024-01-05 15:02:15 -05:00
Jessica Chowdhury
28a30120dd fix(plugin-form-builder): slate serializer should replace curly braces in links (#4703) 2024-01-05 13:01:03 -05:00
Elliot DeNolf
40a0921597 docs: update 'accessing files outside of payload cloud' 2024-01-05 11:53:51 -05:00
Paul
0b80e4a403 chore: bump the mongodb-memory-server dependency to 9.x (#4693) 2024-01-05 09:35:35 -05:00
Seied Ali Mirkarimi
b378532ddf chore: rtl locale selector and sidebar button (#4684)
* chore: rtl locale popup selector and sidebar button

* chore(plugin-seo): add persian translation
2024-01-04 21:45:39 -05:00
Patrik
d419275fb5 fix: adds objectID validation to isValidID if of type text (#4689)
* fix: adds updated object-id validation to isValidID

* chore: adds check to see if value is of type string or object

* chore: needs to return false if value not of type object or string
2024-01-04 15:08:55 -05:00
Jessica Chowdhury
0fb3a9ca89 fix: allow json field to be saved empty and reflect value changes (#4687)
* fix: allow json field to be saved empty and reflect value changes

* fix: reverts change to json field validation

* chore: wraps more JSON field logic with a try/catch
2024-01-04 14:47:10 -05:00
Gokulsck
f43cf185d4 feat: hasMany property for text fields (#4605)
* fix for supporting hasMany property in text field

* Updated docs

* handle text case types for schema and graphql schema

* fix unit test for required failing

* add unit test for has many text field

* add end to end test for has many on text field creation

* support has many feature for text field on postgres

---------

Co-authored-by: Chris Heinz <chrisi.heinz@web.de>
2024-01-04 14:45:00 -05:00
Dan Ribbens
5d15955f83 fix: custom ids in versions (#4680)
* chore: scaffolds out fix for postgres issues with custom ids in versions

* fix(db-postgres): queryDrafts returns undefined doc.id

* chore(db-postgres): fix build

* fix: removes extra custom id field from  versions buildCollectionFields

* chore: comments test/versions seeding back in

* fix buildCollectionFields version group fields

* fix: id field can be edited after saving a document with custom ids

* chore: updates versions custom ID test

---------

Co-authored-by: PatrikKozak <patrik@payloadcms.com>
2024-01-04 13:05:10 -05:00
Elliot DeNolf
2d35e06667 ci(templates): generate types in ci (#4685) 2024-01-04 10:04:44 -05:00
Elliot DeNolf
d2de6db449 chore(release): eslint-config-payload/1.1.0 2024-01-04 09:56:42 -05:00
Alessio Gravili
a3e78161b5 fix: non-boolean condition result causes infinite looping (#4579) 2024-01-04 09:51:08 -05:00
Hulpoi George-Valentin
d543665995 fix: unlock user condition always passes due to seconds conversion (#4610)
* fix: unlock condition is always true

* test: extra call for locking user, therefor won't be a condition issue
2024-01-04 09:43:10 -05:00
Alessio Gravili
db7dddf1c5 chore: commit intellij run configurations (#4653)
* chore: update .gitignore

* chore: update .gitignore

* chore: commit IntelliJ run configurations
2024-01-04 09:35:01 -05:00
Paul
3027a03ad1 feat(plugin-seo): add i18n (#4665)
* Add i18n to plugin SEO

* Add new translations and e2e tests for the SEO plugin

* Update e2e tests to utilise a shared page ID from a create function
2024-01-04 09:18:09 -05:00
Paul
85e38b7cfd fix: sidebar fields not disabled by access permissions (#4682)
* Pass operation to sidebar fields too

* Add a test for sidebar field update permission
2024-01-03 20:04:40 -05:00
Elliot DeNolf
9090540ece chore(release): richtext-lexical/0.5.1 [skip ci] 2024-01-03 15:58:05 -05:00
Elliot DeNolf
46ef284f6b chore(release): db-postgres/0.2.3 [skip ci] 2024-01-03 15:57:54 -05:00
Elliot DeNolf
0727dcd963 chore(release): db-mongodb/1.2.0 [skip ci] 2024-01-03 15:57:22 -05:00
Elliot DeNolf
52f8d4f9f0 chore(release): payload/2.6.0 [skip ci] 2024-01-03 15:55:34 -05:00
Jessica Chowdhury
f1fa374ed1 fix: tab field error when using the same interface name (#4657)
* fix: tab field error when using the same interface name

* fix: removes unused tab types
2024-01-03 15:50:07 -05:00
Alessio Gravili
6b691eee43 chore(eslint-config-payload): improve perfectionist object sort order (#4678) 2024-01-03 21:45:34 +01:00
Paul
be3beabb9b fix: navigation locks when modal is closed with esc (#4664) 2024-01-03 12:06:09 -05:00
Seied Ali Mirkarimi
1fa00cc25c chore: rtl header locale selector (#4670) 2024-01-03 12:04:39 -05:00
James
f70943524b fix(templates): #4662, templates not building after having types generated 2024-01-02 19:49:09 -05:00
Jessica Chowdhury
a67080a291 Merge pull request #4574 from jschuur/patch-1
fix: adjusts json field joi schema to allow editorOptions
2024-01-02 21:58:34 +00:00
Jarrod Flesch
69a99445c9 fix: detect language from request headers accept-language (#4656) 2024-01-02 15:17:00 -05:00
Alessio Gravili
00d8480062 fix: "The punycode module is deprecated" warning by updating nodemailer 2024-01-02 18:26:52 +01:00
Dan Ribbens
7424ba9090 test: e2e await fix (#4646) 2024-01-01 14:09:48 -05:00
Dan Ribbens
ec4d2f97cb fix(db-postgres): query on json properties 2023-12-29 16:42:32 -05:00
Dan Ribbens
9d9ac0ec28 fix(db-postgres): incorrect results querying json field using exists operator 2023-12-29 11:35:12 -05:00
Dan Ribbens
635e7c26e8 fix(plugin-nested-docs): custom parent field slug 2023-12-28 14:30:36 -05:00
Dan Ribbens
c4a4678afb fix(plugin-nested-docs): parent filterOptions errors when specifying breadcrumbsFieldSlug 2023-12-28 14:01:31 -05:00
Dan Ribbens
a5a91c08a9 fix(plugin-nested-docs): breadcrumbsFieldSlug used in resaveSelfAfterCreate hook 2023-12-28 13:05:53 -05:00
Dan Ribbens
7db58b482b fix: custom overrides of breadcrumb and parent fields 2023-12-28 12:56:30 -05:00
Dan Ribbens
1b914083c8 fix: relations with number based ids (postgres) show untitled ID: x 2023-12-27 15:30:30 -05:00
Anthony Bouch
657d14c07b chore(plugin-search): adjusts code comment when attaching hooks (#4595) 2023-12-23 19:24:33 -05:00
yuc
fbf8ab72a4 docs: fix typo in Select Field example (#4593) 2023-12-23 19:15:44 -05:00
Zakher Masri
997f158149 chore(plugin-stripe): fixes broken link in README (#4602) 2023-12-23 19:08:25 -05:00
James Mikrut
c3be5d1d5e Merge pull request #4560 from payloadcms/fix/#4484-graphql-multiple-locales
fix: graphql cannot query multiple locales
2023-12-21 15:29:14 -05:00
James Mikrut
250bcd8189 Merge pull request #4526 from payloadcms/feat/locale-specific-fallbacks
feat: extend locales to have fallbackLocales
2023-12-21 15:25:55 -05:00
Jesse Sivonen
a71d37b398 fix(db-postgres): Wait for transaction to complete on commit (#4582)
* fix(db-postgres): Wait for transaction to complete on commit
* fix session types
2023-12-21 11:03:27 -05:00
Patrik
5c5523195c fix: resets actions array when navigating out of view with actions (#4585) 2023-12-21 10:48:04 -05:00
Joost Schuur
bff4cf518f fix: adjusts json field joi schema to allow editorOptions
Previous fix was not applied to json fields: https://github.com/payloadcms/payload/pull/2731/files
2023-12-21 00:47:14 +08:00
Alessio Gravili
8015e999cd fix(richtext-lexical): z-index issues (#4570) 2023-12-20 15:10:18 +01:00
Sajarin M
0c905f0da7 docs: typo in transactions page (#4565) 2023-12-20 01:31:50 -05:00
Dan Ribbens
e691a90a4c chore: fix failed test 2023-12-19 15:53:20 -05:00
James Mikrut
0b2da4fba7 Merge pull request #4467 from payloadcms/feat/mongodb-transaction-options
feat(db-mongodb): add transactionOptions
2023-12-19 15:30:23 -05:00
Elliot DeNolf
1c6d6788a3 chore: update changelog [skip ci] 2023-12-19 15:03:44 -05:00
Elliot DeNolf
ecc7978184 chore(release): plugin-nested-docs/1.0.10 [skip ci] 2023-12-19 14:49:38 -05:00
Elliot DeNolf
1b9ee64a67 chore(release): live-preview/0.2.2 [skip ci] 2023-12-19 14:48:20 -05:00
Dan Ribbens
365047a3fb Merge branch 'feat/locale-specific-fallbacks' into fix/#4484-graphql-multiple-locales 2023-12-19 14:22:09 -05:00
Dan Ribbens
42c06acd18 docs: transaction options 2023-12-19 14:19:28 -05:00
Dan Ribbens
f2c8ac4a9a feat(db-mongodb): add transactionOptions 2023-12-19 14:19:12 -05:00
Dan Ribbens
35191bdd66 docs: improve docs for locales 2023-12-19 14:11:35 -05:00
Dan Ribbens
98890eee1f fix: graphql multiple locales 2023-12-19 14:00:06 -05:00
Dan Ribbens
c703497924 test: improve e2e locale change selector 2023-12-19 09:57:46 -05:00
Dan Ribbens
5caad706bb chore: consistent locale and fallback locale for globals 2023-12-19 09:40:26 -05:00
Dan Ribbens
aa048d5409 fix: req.locale and req.fallbackLocale get reassigned in local operations 2023-12-18 16:50:17 -05:00
Dan Ribbens
aafd538cf8 fix failing e2e test 2023-12-16 00:49:04 -05:00
Dan Ribbens
1b42bd207d fix failing tests 2023-12-16 00:18:59 -05:00
Dan Ribbens
9fac2ef24e feat: extend locales to have fallbackLocales 2023-12-15 23:52:12 -05:00
232 changed files with 4467 additions and 2380 deletions

View File

@@ -306,9 +306,10 @@ jobs:
with: with:
mongodb-version: 6.0 mongodb-version: 6.0
- name: Build Website - name: Build Template
run: | run: |
cd templates/${{ matrix.template }} cd templates/${{ matrix.template }}
cp .env.example .env cp .env.example .env
yarn install yarn install
yarn build yarn build
yarn generate:types

101
.gitignore vendored
View File

@@ -1,7 +1,9 @@
coverage coverage
package-lock.json package-lock.json
dist dist
.idea /.idea/*
!/.idea/runConfigurations
test-results test-results
.devcontainer .devcontainer
/migrations /migrations
@@ -230,119 +232,24 @@ GitHub.sublime-settings
.history .history
.ionide .ionide
### WebStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake # CMake
cmake-build-*/ cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format # File-based project format
*.iws *.iws
# IntelliJ # IntelliJ
out/ out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin # JIRA plugin
atlassian-ide-plugin.xml atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ) # Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### WebStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Windows ### ### Windows ###
# Windows thumbnail cache files # Windows thumbnail cache files
Thumbs.db Thumbs.db
@@ -371,4 +278,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode # End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
/build /build

View File

@@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev Fields" type="NodeJSConfigurationType" application-parameters="fields" path-to-js-file="node_modules/.pnpm/nodemon@3.0.1/node_modules/nodemon/bin/nodemon.js" working-dir="$PROJECT_DIR$">
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev _community" type="NodeJSConfigurationType" application-parameters="_community" path-to-js-file="node_modules/.pnpm/nodemon@3.0.1/node_modules/nodemon/bin/nodemon.js" working-dir="$PROJECT_DIR$">
<method v="2" />
</configuration>
</component>

View File

@@ -1,3 +1,61 @@
## [2.7.0](https://github.com/payloadcms/payload/compare/v2.6.0...v2.7.0) (2024-01-09)
### Features
* **db-mongodb:** improve transaction support by passing req to migrations ([682eca2](https://github.com/payloadcms/payload/commit/682eca21860a4e2b2ab0bfd85613818790247224))
* **db-postgres:** improve transaction support by passing req to migrations ([555d027](https://github.com/payloadcms/payload/commit/555d02769a8731aeebbff9b67f9b0e1022904ade))
* hasMany property for text fields ([#4605](https://github.com/payloadcms/payload/issues/4605)) ([f43cf18](https://github.com/payloadcms/payload/commit/f43cf185d45b3c75fa0d78acd91e6cb60d87f166))
* improve transaction support by passing req to migrations ([1d14d9f](https://github.com/payloadcms/payload/commit/1d14d9f8b8ed077691175030182f094bb300ed17))
* **plugin-seo:** add i18n ([#4665](https://github.com/payloadcms/payload/issues/4665)) ([3027a03](https://github.com/payloadcms/payload/commit/3027a03ad11ecd679278e44a013e4dea4aa42b8d))
* provide document info to ActionsProvider ([#4696](https://github.com/payloadcms/payload/issues/4696)) ([6a8a6e4](https://github.com/payloadcms/payload/commit/6a8a6e4ef4913e0889e4d2eac82b28b9e4e8db22))
### Bug Fixes
* adds objectID validation to isValidID if of type `text` ([#4689](https://github.com/payloadcms/payload/issues/4689)) ([d419275](https://github.com/payloadcms/payload/commit/d419275fb50f0922307f2d3b4c0fcf80ac5ec98b))
* allow json field to be saved empty and reflect value changes ([#4687](https://github.com/payloadcms/payload/issues/4687)) ([0fb3a9c](https://github.com/payloadcms/payload/commit/0fb3a9ca89d1b63faea179bfa9b5b3d0a69c9398))
* custom ids in versions ([#4680](https://github.com/payloadcms/payload/issues/4680)) ([5d15955](https://github.com/payloadcms/payload/commit/5d15955f839d3f0cc557d8a8d7cc3a9e52e2f6b1))
* custom overrides of breadcrumb and parent fields ([7db58b4](https://github.com/payloadcms/payload/commit/7db58b482bba7e715c5be23cfe1a84295e95da29))
* **db-mongodb:** migration error calling beginTransaction with transactionOptions false ([21b9453](https://github.com/payloadcms/payload/commit/21b9453cf4e6eebf145d89a0190942015658413d))
* **db-mongodb:** querying plan for collections ignoring indexes ([#4655](https://github.com/payloadcms/payload/issues/4655)) ([63bc4ca](https://github.com/payloadcms/payload/commit/63bc4cabe1dea5f233aa1d9d4e64f3af93a8e081))
* **db-postgres:** incorrect results querying json field using exists operator ([9d9ac0e](https://github.com/payloadcms/payload/commit/9d9ac0ec28c97281bfdc7d6fb78c52baea492380))
* **db-postgres:** migrate down only runs latest batch size ([6acfae8](https://github.com/payloadcms/payload/commit/6acfae8ee7614746797e1fa91e1fd41c0240fdcd))
* **db-postgres:** query on json properties ([ec4d2f9](https://github.com/payloadcms/payload/commit/ec4d2f97cbf1c89d837372059bf3bb77f3ea6594))
* **db-postgres:** validation prevents group fields in blocks ([#4699](https://github.com/payloadcms/payload/issues/4699)) ([cab6bab](https://github.com/payloadcms/payload/commit/cab6babd608daeaabf9b63b1b446fded6804b60f))
* non-boolean condition result causes infinite looping ([#4579](https://github.com/payloadcms/payload/issues/4579)) ([a3e7816](https://github.com/payloadcms/payload/commit/a3e78161b551e8286063a173645a1d3dee162ad1))
* **plugin-form-builder:** slate serializer should replace curly braces in links ([#4703](https://github.com/payloadcms/payload/issues/4703)) ([28a3012](https://github.com/payloadcms/payload/commit/28a30120dd1aa3279fb2133aa0a0b1638d144be4))
* **plugin-nested-docs:** breadcrumbsFieldSlug used in resaveSelfAfterCreate hook ([a5a91c0](https://github.com/payloadcms/payload/commit/a5a91c08a9ade1482c512d3fa4c4f519ad85cf74))
* **plugin-nested-docs:** children wrongly publishing draft data ([#4692](https://github.com/payloadcms/payload/issues/4692)) ([5539942](https://github.com/payloadcms/payload/commit/55399424a13b1e0532d9eeefd09d442c107c3eda))
* **plugin-nested-docs:** custom parent field slug ([635e7c2](https://github.com/payloadcms/payload/commit/635e7c26e8b3b5138cf5a9bcb29e8ddd4b1e69b6))
* **plugin-nested-docs:** parent filterOptions errors when specifying breadcrumbsFieldSlug ([c4a4678](https://github.com/payloadcms/payload/commit/c4a4678afb097cf94c682595a78e416767a1fea8))
* prevents row overflow ([#4704](https://github.com/payloadcms/payload/issues/4704)) ([9828772](https://github.com/payloadcms/payload/commit/98287728900cb88fa6a465899f030f81df28fc69))
* relations with number based ids (postgres) show untitled ID: x ([1b91408](https://github.com/payloadcms/payload/commit/1b914083c8ee0c1b1d64fa7d4471ede0a24cfdb7))
* sidebar fields not disabled by access permissions ([#4682](https://github.com/payloadcms/payload/issues/4682)) ([85e38b7](https://github.com/payloadcms/payload/commit/85e38b7cfd5c0772344c4a8fb5100f7c48eb508f))
* unlock user condition always passes due to seconds conversion ([#4610](https://github.com/payloadcms/payload/issues/4610)) ([d543665](https://github.com/payloadcms/payload/commit/d543665995410256f77fe136173339aee6dcc7da))
## [2.6.0](https://github.com/payloadcms/payload/compare/v2.5.0...v2.6.0) (2024-01-03)
### Features
* **db-mongodb:** add transactionOptions ([f2c8ac4](https://github.com/payloadcms/payload/commit/f2c8ac4a9aa9120339af6759170f5a708469698d))
* extend locales to have fallbackLocales ([9fac2ef](https://github.com/payloadcms/payload/commit/9fac2ef24e2ade4cf55b0d6a0e7f67e0edf57539))
### Bug Fixes
* "The punycode module is deprecated" warning by updating nodemailer ([00d8480](https://github.com/payloadcms/payload/commit/00d8480062d99dee56ef61a955f48a92efa6cbea))
* adjusts json field joi schema to allow editorOptions ([bff4cf5](https://github.com/payloadcms/payload/commit/bff4cf518f748efb9179f112c606d11d25db3d99))
* **db-postgres:** Wait for transaction to complete on commit ([#4582](https://github.com/payloadcms/payload/issues/4582)) ([a71d37b](https://github.com/payloadcms/payload/commit/a71d37b39806cd5956378a10246802d01d06c2dd))
* detect language from request headers accept-language ([#4656](https://github.com/payloadcms/payload/issues/4656)) ([69a9944](https://github.com/payloadcms/payload/commit/69a99445c9f1638a962a9c08ffe0bdc22e538bf6))
* graphql multiple locales ([98890ee](https://github.com/payloadcms/payload/commit/98890eee1f527c8f245b2353d7e1caca4d2a7d8c))
* navigation locks when modal is closed with esc ([#4664](https://github.com/payloadcms/payload/issues/4664)) ([be3beab](https://github.com/payloadcms/payload/commit/be3beabb9bafa137aa89e84cf47246017e969be8))
* req.locale and req.fallbackLocale get reassigned in local operations ([aa048d5](https://github.com/payloadcms/payload/commit/aa048d5409acd42b8f56367a16934085df9fbce2))
* resets actions array when navigating out of view with actions ([#4585](https://github.com/payloadcms/payload/issues/4585)) ([5c55231](https://github.com/payloadcms/payload/commit/5c5523195ccfa94a9bf42441e2a378f87836e64d))
* **richtext-lexical:** z-index issues ([#4570](https://github.com/payloadcms/payload/issues/4570)) ([8015e99](https://github.com/payloadcms/payload/commit/8015e999cd5834f532a200ef03fd392d04b3209f))
* tab field error when using the same interface name ([#4657](https://github.com/payloadcms/payload/issues/4657)) ([f1fa374](https://github.com/payloadcms/payload/commit/f1fa374ed12b50fdf210f17ae1dda603f09a9726))
## [2.5.0](https://github.com/payloadcms/payload/compare/v2.4.0...v2.5.0) (2023-12-19) ## [2.5.0](https://github.com/payloadcms/payload/compare/v2.4.0...v2.5.0) (2023-12-19)
@@ -61,10 +119,67 @@
#### @payloadcms/richtext-lexical #### @payloadcms/richtext-lexical
* **richtext-lexical:** rename TreeviewFeature into TreeViewFeature (#4520) * **richtext-lexical:** rename TreeviewFeature into TreeViewFeature ([#4520](https://github.com/payloadcms/payload/issues/4520)) ([c49fd66](https://github.com/payloadcms/payload/commit/c49fd6692231b68ca61b079103a0fd7aa4673be1))
* **richtext-lexical:** link node: change doc data format to be consistent with relationship field (#4504)
* **richtext-lexical:** improve floating select menu Dropdown classNames (#4444) If you import TreeviewFeature, you have to rename the import to use TreeViewFeature (capitalized "V")
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server (#4290)
* **richtext-lexical:** link node: change doc data format to be consistent with relationship field ([#4504](https://github.com/payloadcms/payload/issues/4504)) ([cc0ba89](https://github.com/payloadcms/payload/commit/cc0ba895188f40181c6ba3779f66d547d4ea66f9))
An unpopulated, internal link node no longer saves the doc id under fields.doc.value.id. Now, it saves it under fields.doc.value.
Migration inside of payload is automatic. If you are reading from the link node inside of your frontend though, you will have to adjust it.
* **richtext-lexical:** improve floating select menu Dropdown classNames ([#4444](https://github.com/payloadcms/payload/issues/4444)) ([9331204](https://github.com/payloadcms/payload/commit/9331204295bfeaf7dd10bc075f42995b2cab2de4))
Dropdown component has a new mandatory sectionKey prop
* **richtext-lexical:** lazy import React components to prevent client-only code from leaking into the server ([#4290](https://github.com/payloadcms/payload/issues/4290)) ([5de347f](https://github.com/payloadcms/payload/commit/5de347ffffca3bf38315d3d87d2ccc5c28cd2723))
1. Most important: If you are updating `@payloadcms/richtext-lexical` to v0.4.0 or higher, you will HAVE to update payload to the latest version as well. If you don't update it, payload likely won't start up due to validation errors. It's generally good practice to upgrade packages prefixed with @payloadcms/ together with payload and keep the versions in sync.
2. `@payloadcms/richtext-slate` is not affected by this.
3. Every single property in the `Feature` interface which accepts a React component now no longer accepts a React component, but a function which imports a React component instead. This is done to ensure no unnecessary client-only code is leaked to the server when importing Features on a server.
Here's an example migration:
Old:
```ts
import { BlockIcon } from '../../lexical/ui/icons/Block'
...
Icon: BlockIcon,
```
New:
```ts
// import { BlockIcon } from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
```
Or alternatively, if you're using default exports instead of named exports:
```ts
// import BlockIcon from '../../lexical/ui/icons/Block' // <= Remove this import
...
Icon: () =>
// @ts-expect-error
import('../../lexical/ui/icons/Block'),
```
4. The types for `SanitizedEditorConfig` and `EditorConfig` have changed. Their respective `lexical` property no longer expects the `LexicalEditorConfig`. It now expects a function returning the `LexicalEditorConfig`. You will have to adjust this if you adjusted that property anywhere, e.g. when initializing the lexical field editor property, or when initializing a new headless editor.
5. The following exports are now exported from the `@payloadcms/richtext-lexical/components` subpath exports instead of `@payloadcms/richtext-lexical`:
- `ToolbarButton`
- `ToolbarDropdown`
- `RichTextCell`
- `RichTextField`
- `defaultEditorLexicalConfig`
You will have to adjust your imports, only if you import any of those properties in your project.
## @payloadcms/richtext-* ## @payloadcms/richtext-*

View File

@@ -534,14 +534,13 @@ When swapping out the `Field` component, you'll be responsible for sending and r
```tsx ```tsx
import { useField } from 'payload/components/forms' import { useField } from 'payload/components/forms'
type Props = { path: string } const CustomTextField: React.FC<{ path: string }> = ({ path }) => {
const CustomTextField: React.FC<Props> = ({ path }) => {
// highlight-start // highlight-start
const { value, setValue } = useField<Props>({ path }) const { value, setValue } = useField<string>({ path })
// highlight-end // highlight-end
return <input onChange={(e) => setValue(e.target.value)} value={value.path} />
return <input onChange={(e) => setValue(e.target.value)} value={value} />
} }
``` ```

View File

@@ -30,6 +30,26 @@ Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/plugin-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud. AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/plugin-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
#### Accessing Files Outside of Payload Cloud
If you'd like to access your files outside of Payload Cloud, you'll need to retrieve some values from your project's settings and put them into your environment variables. In Payload Cloud, navigate to the File Storage tab and copy the values using the copy button. Put these values in your .env file. Also copy the Cognito Password value separately and put into your .env file as well.
When you are done, you should have the following values in your .env file:
```env
PAYLOAD_CLOUD=true
PAYLOAD_CLOUD_ENVIRONMENT=prod
PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID=
PAYLOAD_CLOUD_COGNITO_USER_POOL_ID=
PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID=
PAYLOAD_CLOUD_PROJECT_ID=
PAYLOAD_CLOUD_BUCKET=
PAYLOAD_CLOUD_BUCKET_REGION=
PAYLOAD_CLOUD_COGNITO_PASSWORD=
```
The plugin will pick up these values and use them to access your files.
### Build Settings ### Build Settings
You can update settings from your Projects Settings tab. Changes to your build settings will trigger a redeployment of your project. You can update settings from your Projects Settings tab. Changes to your build settings will trigger a redeployment of your project.

View File

@@ -6,11 +6,13 @@ desc: Add and maintain as many locales as you need by adding Localization to you
keywords: localization, internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express keywords: localization, internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
--- ---
Payload features deep field-based localization support. Maintaining as many locales as you need is easy. All localization support is opt-in by default. To do so, follow the two steps below. Payload features deep field-based localization support. Maintaining as many locales as you need is easy. All
localization support is opt-in by default. To do so, follow the two steps below.
### Enabling in the Payload config ### Enabling in the Payload config
Add the `localization` property to your Payload config to enable localization project-wide. You'll need to provide a list of all locales that you'd like to support as well as set a few other options. Add the `localization` property to your Payload config to enable localization project-wide. You'll need to provide a
list of all locales that you'd like to support as well as set a few other options.
**Example Payload config set up for localization:** **Example Payload config set up for localization:**
@@ -57,7 +59,8 @@ export default buildConfig({
}) })
``` ```
**Example Payload config set up for localization with full locales objects (including [internationalization](/docs/configuration/i18n) support):** **Example Payload config set up for localization with full locales objects (
including [internationalization](/docs/configuration/i18n) support):**
```ts ```ts
import { buildConfig } from 'payload/config' import { buildConfig } from 'payload/config'
@@ -93,35 +96,60 @@ export default buildConfig({
**`locales`** **`locales`**
Array-based list of all locales that you would like to support. These can be strings of locale codes or objects with a `label`, a locale `code`, and the `rtl` (right-to-left) property. The locale codes do not need to be in any specific format. It's up to you to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter language and country codes (ISO 31661) such as `en-US`, `en-UK`, `es-MX`, etc. Array-based list of all the languages that you would like to support. This can be an array containing strings for each
language code you want your project to store and serve or objects with a `label`, a locale `code`, `rtl` (
right-to-left), and `fallbackLocale` property. The locale codes do not need to be in any specific format. It's up to you
to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter
language and country codes (ISO 31661) such as `en-US`, `en-UK`, `es-MX`, etc.
### Locale Properties:
| Option | Description |
|----------------------|--------------------------------------------------------------------------------------------------------------------------------|
| **`code`** \* | Unique code to identify the language throughout the APIs for `locale` and `fallbackLocale` |
| **`label`** | A string to use for the selector when choosing a language, or an object keyed on the i18n keys for different languages in use. |
| **`rtl`** | A boolean that when true will make the admin UI display in Right-To-Left. |
| **`fallbackLocale`** | The code for this language to fallback to when properties of a document are not present. |
_\* An asterisk denotes that a property is required._
**`defaultLocale`** **`defaultLocale`**
Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. Required string that matches one of the locale codes from the array provided. By default, if no locale is specified,
documents will be returned in this locale.
**`fallback`** **`fallback`**
Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated. Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a
localized value corresponding to the requested locale, then if this property is enabled, the document will automatically
fall back to the fallback locale value. If this property is not enabled, the value will not be populated.
### Field by field localization ### Field by field localization
Payload localization works on a **field** level—not a document level. In addition to configuring the base Payload config to support localization, you need to specify each field that you would like to localize. Payload localization works on a **field** level—not a document level. In addition to configuring the base Payload config
to support localization, you need to specify each field that you would like to localize.
**Here is an example of how to enable localization for a field:** **Here is an example of how to enable localization for a field:**
```js ```js
{ {
name: 'title', name: 'title',
type: 'text', type
// highlight-start :
localized: true, 'text',
// highlight-start
localized
:
true,
// highlight-end // highlight-end
} }
``` ```
With the above configuration, the `title` field will now be saved in the database as an object of all locales instead of a single string. With the above configuration, the `title` field will now be saved in the database as an object of all locales instead of
a single string.
All field types with a `name` property support the `localized` property—even the more complex field types like `array`s and `block`s. All field types with a `name` property support the `localized` property—even the more complex field types like `array`s
and `block`s.
<Banner> <Banner>
<strong>Note:</strong> <strong>Note:</strong>
@@ -143,7 +171,8 @@ All field types with a `name` property support the `localized` property—even t
### Retrieving localized docs ### Retrieving localized docs
When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be used. When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be
used.
##### REST API ##### REST API
@@ -155,7 +184,8 @@ Specify your desired locale by providing the `locale` query parameter directly i
**`?fallback-locale=`** **`?fallback-locale=`**
Specify fallback locale to be used by providing the `fallback-locale` query parameter. This can be provided as either a valid locale as provided to your base Payload config, or `'null'`, `'false'`, or `'none'` to disable falling back. Specify fallback locale to be used by providing the `fallback-locale` query parameter. This can be provided as either a
valid locale as provided to your base Payload config, or `'null'`, `'false'`, or `'none'` to disable falling back.
**Example:** **Example:**
@@ -167,7 +197,9 @@ fetch('https://localhost:3000/api/pages?locale=es&fallback-locale=none');
In the GraphQL API, you can specify `locale` and `fallbackLocale` args to all relevant queries and mutations. In the GraphQL API, you can specify `locale` and `fallbackLocale` args to all relevant queries and mutations.
The `locale` arg will only accept valid locales, but locales will be formatted automatically as valid GraphQL enum values (dashes or special characters will be converted to underscores, spaces will be removed, etc.). If you are curious to see how locales are auto-formatted, you can use the [GraphQL playground](/docs/graphql/overview#graphql-playground). The `locale` arg will only accept valid locales, but locales will be formatted automatically as valid GraphQL enum
values (dashes or special characters will be converted to underscores, spaces will be removed, etc.). If you are curious
to see how locales are auto-formatted, you can use the [GraphQL playground](/docs/graphql/overview#graphql-playground).
The `fallbackLocale` arg will accept valid locales as well as `none` to disable falling back. The `fallbackLocale` arg will accept valid locales as well as `none` to disable falling back.
@@ -175,11 +207,11 @@ The `fallbackLocale` arg will accept valid locales as well as `none` to disable
```graphql ```graphql
query { query {
Posts(locale: de, fallbackLocale: none) { Posts(locale: de, fallbackLocale: none) {
docs { docs {
title title
}
} }
}
} }
``` ```
@@ -191,7 +223,9 @@ query {
##### Local API ##### Local API
You can specify `locale` as well as `fallbackLocale` within the Local API as well as properties on the `options` argument. The `locale` property will accept any valid locale, and the `fallbackLocale` property will accept any valid locale as well as `'null'`, `'false'`, `false`, and `'none'`. You can specify `locale` as well as `fallbackLocale` within the Local API as well as properties on the `options`
argument. The `locale` property will accept any valid locale, and the `fallbackLocale` property will accept any valid
locale as well as `'null'`, `'false'`, `false`, and `'none'`.
**Example:** **Example:**

View File

@@ -6,7 +6,8 @@ keywords: database, migrations, ddl, sql, mongodb, postgres, documentation, Cont
desc: Payload features first-party database migrations all done in TypeScript. desc: Payload features first-party database migrations all done in TypeScript.
--- ---
Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via the `npm run payload` command in your project directory. Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via
the `npm run payload` command in your project directory.
Ensure you have an npm script called "payload" in your `package.json` file. Ensure you have an npm script called "payload" in your `package.json` file.
@@ -24,33 +25,42 @@ Ensure you have an npm script called "payload" in your `package.json` file.
### Migration file contents ### Migration file contents
Payload stores all created migrations in a folder that you can specify. By default, migrations are stored in `./src/migrations`. Payload stores all created migrations in a folder that you can specify. By default, migrations are stored
in `./src/migrations`.
A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function that will be called if for some reason the migration fails to complete successfully. The `up` function should contain all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make. A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function
that will be called if for some reason the migration fails to complete successfully. The `up` function should contain
all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make.
For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions). For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions). Migration
functions should make use of the `req` by adding it to the arguments of your payload local API calls such
as `payload.create` and database adapter methods like `payload.db.create`.
Here is an example migration file: Here is an example migration file:
```ts ```ts
import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/your-db-adapter' import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/your-db-adapter'
export async function up({ payload }: MigrateUpArgs): Promise<void> { export async function up ({ payload, req }: MigrateUpArgs): Promise<void> {
// Perform changes to your database here. // Perform changes to your database here.
// You have access to `payload` as an argument, and // You have access to `payload` as an argument, and
// everything is done in TypeScript. // everything is done in TypeScript.
}; };
export async function down({ payload }: MigrateDownArgs): Promise<void> { export async function down ({ payload, req }: MigrateDownArgs): Promise<void> {
// Do whatever you need to revert changes if the `up` function fails // Do whatever you need to revert changes if the `up` function fails
}; };
``` ```
### Migrations Directory ### Migrations Directory
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc. Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be
stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your
migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
All database adapters should implement similar migration patterns, but there will be small differences based on the adapter and its specific needs. Below is a list of all migration commands that should be supported by your database adapter. All database adapters should implement similar migration patterns, but there will be small differences based on the
adapter and its specific needs. Below is a list of all migration commands that should be supported by your database
adapter.
## Commands ## Commands
@@ -64,7 +74,8 @@ npm run payload migrate
### Create ### Create
Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By default, migrations will be named using a timestamp. Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By
default, migrations will be named using a timestamp.
```text ```text
npm run payload migrate:create optional-name-here npm run payload migrate:create optional-name-here
@@ -72,7 +83,8 @@ npm run payload migrate:create optional-name-here
### Status ### Status
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run, and which migrations have not yet run. The `migrate:status` command will check the status of migrations and output a table of which migrations have been run,
and which migrations have not yet run.
`payload migrate:status` `payload migrate:status`

View File

@@ -6,7 +6,8 @@ desc: Payload has supported MongoDB natively since we started. The flexible natu
keywords: MongoDB, documentation, typescript, Content Management System, cms, headless, javascript, node, react, express keywords: MongoDB, documentation, typescript, Content Management System, cms, headless, javascript, node, react, express
--- ---
To use Payload with MongoDB, install the package `@payloadcms/db-mongodb`. It will come with everything you need to store your Payload data in MongoDB. To use Payload with MongoDB, install the package `@payloadcms/db-mongodb`. It will come with everything you need to
store your Payload data in MongoDB.
Then from there, pass it to your Payload config as follows: Then from there, pass it to your Payload config as follows:
@@ -29,16 +30,18 @@ export default buildConfig({
### Options ### Options
| Option | Description | | Option | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. | | `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. | | `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 | | `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. | | `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. | |
### Access to Mongoose models ### Access to Mongoose models
After Payload is initialized, this adapter exposes all of your Mongoose models and they are available for you to work with directly. After Payload is initialized, this adapter exposes all of your Mongoose models and they are available for you to work
with directly.
You can access Mongoose models as follows: You can access Mongoose models as follows:

View File

@@ -48,7 +48,7 @@ const afterChange: CollectionAfterChangeHook = async ({ req }) => {
}) })
// Should this call fail, it will not rollback other changes // Should this call fail, it will not rollback other changes
// because the req (and its transationID) is not passed through // because the req (and its transactionID) is not passed through
const safelyIgnoredAsync = req.payload.create({ const safelyIgnoredAsync = req.payload.create({
collection: 'my-slug', collection: 'my-slug',
data: { data: {

View File

@@ -133,7 +133,7 @@ import * as React from 'react';
import { SelectInput, useField } from 'payload/components/forms'; import { SelectInput, useField } from 'payload/components/forms';
import { useAuth } from 'payload/components/utilities'; import { useAuth } from 'payload/components/utilities';
type customSelectProps = { type CustomSelectProps = {
path: string; path: string;
options: { options: {
label: string; label: string;
@@ -164,7 +164,7 @@ export const CustomSelectComponent: React.FC<CustomSelectProps> = ({ path, optio
name={path} name={path}
options={adjustedOptions} options={adjustedOptions}
value={value} value={value}
onChange={() => setValue(e.value)} onChange={(e) => setValue(e.value)}
/> />
</div> </div>
) )

View File

@@ -38,7 +38,9 @@ keywords: text, fields, config, configuration, documentation, Content Management
| **`required`** | Require this field to have a value. | | **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`hasMany`** | Makes this field an ordered array of text instead of just a single text. |
| **`minRows`** | Minimum number of texts in the array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of texts in the array, if `hasMany` is set to true. |
_\* An asterisk denotes that a property is required._ _\* An asterisk denotes that a property is required._
### Admin config ### Admin config

View File

@@ -357,7 +357,7 @@ const result = await payload.unlock({
```js ```js
// Returned result will be a boolean representing success or failure // Returned result will be a boolean representing success or failure
const result = await payload.verify({ const result = await payload.verifyEmail({
collection: 'users', // required collection: 'users', // required
token: 'afh3o2jf2p3f...', // the token saved on the user as `_verificationToken` token: 'afh3o2jf2p3f...', // the token saved on the user as `_verificationToken`
}) })

View File

@@ -8,11 +8,21 @@ keywords: plugins, nested, documents, parent, child, sibling, relationship
[![NPM](https://img.shields.io/npm/v/@payloadcms/plugin-nested-docs)](https://www.npmjs.com/package/@payloadcms/plugin-nested-docs) [![NPM](https://img.shields.io/npm/v/@payloadcms/plugin-nested-docs)](https://www.npmjs.com/package/@payloadcms/plugin-nested-docs)
This plugin allows you to easily nest the documents of your application inside of one another. It does so by adding a new `parent` field onto each of your documents that, when selected, attaches itself to the parent's tree. When you edit the great-great-grandparent of a document, for instance, all of its descendants are recursively updated. This is an extremely powerful way of achieving hierarchy within a collection, such as parent/child relationship between pages. This plugin allows you to easily nest the documents of your application inside of one another. It does so by adding a
new `parent` field onto each of your documents that, when selected, attaches itself to the parent's tree. When you edit
the great-great-grandparent of a document, for instance, all of its descendants are recursively updated. This is an
extremely powerful way of achieving hierarchy within a collection, such as parent/child relationship between pages.
Documents also receive a new `breadcrumbs` field. Once a parent is assigned, these breadcrumbs are populated based on each ancestor up the tree. Breadcrumbs allow you to dynamically generate labels and URLs based on the document's position in the hierarchy. Even if the slug of a parent document changes, or the entire tree is nested another level deep, changes will cascade down the entire tree and all breadcrumbs will reflect those changes. Documents also receive a new `breadcrumbs` field. Once a parent is assigned, these breadcrumbs are populated based on
each ancestor up the tree. Breadcrumbs allow you to dynamically generate labels and URLs based on the document's
position in the hierarchy. Even if the slug of a parent document changes, or the entire tree is nested another level
deep, changes will cascade down the entire tree and all breadcrumbs will reflect those changes.
With this pattern you can perform whatever side-effects your applications needs on even the most deeply nested documents. For example, you could easily add a custom `fullTitle` field onto each document and inject the parent's title onto it, such as "Parent Title > Child Title". This would allow you to then perform searches and filters based on _that_ field instead of the original title. This is especially useful if you happen to have two documents with identical titles but different parents. With this pattern you can perform whatever side-effects your applications needs on even the most deeply nested
documents. For example, you could easily add a custom `fullTitle` field onto each document and inject the parent's title
onto it, such as "Parent Title > Child Title". This would allow you to then perform searches and filters based on _that_
field instead of the original title. This is especially useful if you happen to have two documents with identical titles
but different parents.
<Banner type="info"> <Banner type="info">
This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-nested-docs). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20nested-docs&template=bug_report.md&title=plugin-nested-docs%3A) with as much detail as possible. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-nested-docs). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20nested-docs&template=bug_report.md&title=plugin-nested-docs%3A) with as much detail as possible.
@@ -29,7 +39,8 @@ With this pattern you can perform whatever side-effects your applications needs
## Installation ## Installation
Install the plugin using any JavaScript package manager like [Yarn](https://yarnpkg.com), [NPM](https://npmjs.com), or [PNPM](https://pnpm.io): Install the plugin using any JavaScript package manager like [Yarn](https://yarnpkg.com), [NPM](https://npmjs.com),
or [PNPM](https://pnpm.io):
```bash ```bash
yarn add @payloadcms/plugin-nested-docs yarn add @payloadcms/plugin-nested-docs
@@ -37,7 +48,8 @@ Install the plugin using any JavaScript package manager like [Yarn](https://yarn
## Basic Usage ## Basic Usage
In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options): In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin
with [options](#options):
```ts ```ts
import { buildConfig } from 'payload/config' import { buildConfig } from 'payload/config'
@@ -75,16 +87,18 @@ export default config
#### Parent #### Parent
The `parent` relationship field is automatically added to every document which allows editors to choose another document from the same collection to act as the direct parent. The `parent` relationship field is automatically added to every document which allows editors to choose another document
from the same collection to act as the direct parent.
#### Breadcrumbs #### Breadcrumbs
The `breadcrumbs` field is an array which dynamically populates all parent relationships of a document up to the top level and stores the following fields. The `breadcrumbs` field is an array which dynamically populates all parent relationships of a document up to the top
level and stores the following fields.
| Field | Description | | Field | Description |
| ------------ | --------------------------------------------------------------------------- | |---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `label` | The label of the breadcrumb. This field is automatically set to either the `collection.admin.useAsTitle` (if defined) or is set to the `ID` of the document. You can also dynamically define the `label` by passing a function to the options property of [`generateLabel`](#generateLabel). | | `label` | The label of the breadcrumb. This field is automatically set to either the `collection.admin.useAsTitle` (if defined) or is set to the `ID` of the document. You can also dynamically define the `label` by passing a function to the options property of [`generateLabel`](#generateLabel). |
| `url` | The URL of the breadcrumb. By default, this field is undefined. You can manually define this field by passing a property called function to the plugin options property of [`generateURL`](#generateURL). | | `url` | The URL of the breadcrumb. By default, this field is undefined. You can manually define this field by passing a property called function to the plugin options property of [`generateURL`](#generateURL). |
### Options ### Options
@@ -94,7 +108,8 @@ An array of collections slugs to enable nested docs.
#### `generateLabel` #### `generateLabel`
Each `breadcrumb` has a required `label` field. By default, its value will be set to the collection's `admin.useAsTitle` or fallback the the `ID` of the document. Each `breadcrumb` has a required `label` field. By default, its value will be set to the collection's `admin.useAsTitle`
or fallback the the `ID` of the document.
You can also pass a function to dynamically set the `label` of your breadcrumb. You can also pass a function to dynamically set the `label` of your breadcrumb.
@@ -108,14 +123,16 @@ nestedDocs({
The function takes two arguments and returns a string: The function takes two arguments and returns a string:
| Argument | Type | Description | | Argument | Type | Description |
| ------------ | -------- | --------------------------------------------------------------------------- | |----------|----------|----------------------------------------------|
| `docs` | `Array` | An array of the breadcrumbs up to that point | | `docs` | `Array` | An array of the breadcrumbs up to that point |
| `doc` | `Object` | The current document being edited | | `doc` | `Object` | The current document being edited |
#### `generateURL` #### `generateURL`
A function that allows you to dynamically generate each breadcrumb `url`. Each `breadcrumb` has an optional `url` field which is undefined by default. For example, you might want to format a full URL to contain all of the breadcrumbs up to that point, like `/about-us/company/our-team`. A function that allows you to dynamically generate each breadcrumb `url`. Each `breadcrumb` has an optional `url` field
which is undefined by default. For example, you might want to format a full URL to contain all breadcrumbs up to
that point, like `/about-us/company/our-team`.
```ts ```ts
// payload.config.ts // payload.config.ts
@@ -125,18 +142,21 @@ nestedDocs({
}) })
``` ```
| Argument | Type | Description | | Argument | Type | Description |
| ------------ | -------- | --------------------------------------------------------------------------- | |----------|----------|----------------------------------------------|
| `docs` | `Array` | An array of the breadcrumbs up to that point | | `docs` | `Array` | An array of the breadcrumbs up to that point |
| `doc` | `Object` | The current document being edited | | `doc` | `Object` | The current document being edited |
#### `parentFieldSlug` #### `parentFieldSlug`
When defined, the `parent` field will not be provided for you automatically, and instead, expects you to add your own `parent` field to each collection manually. This gives you complete control over where you put the field in your admin dashboard, etc. Set this property to the `name` of your custom field. When defined, the `parent` field will not be provided for you automatically, and instead, expects you to add your
own `parent` field to each collection manually. This gives you complete control over where you put the field in your
admin dashboard, etc. Set this property to the `name` of your custom field.
#### `breadcrumbsFieldSlug` #### `breadcrumbsFieldSlug`
When defined, the `breadcrumbs` field will not be provided for you, and instead, expects your to add your own `breadcrumbs` field to each collection manually. Set this property to the `name` of your custom field. When defined, the `breadcrumbs` field will not be provided for you, and instead, expects you to add your
own `breadcrumbs` field to each collection manually. Set this property to the `name` of your custom field.
<Banner type="info"> <Banner type="info">
<strong>Note:</strong> <strong>Note:</strong>
@@ -146,7 +166,8 @@ When defined, the `breadcrumbs` field will not be provided for you, and instead,
## Overrides ## Overrides
You can also extend the built-in `parent` and `breadcrumbs` fields per collection by using the `createParentField` and `createBreadcrumbField` methods. They will merge your customizations overtop the plugin's base field configurations. You can also extend the built-in `parent` and `breadcrumbs` fields per collection by using the `createParentField`
and `createBreadcrumbField` methods. They will merge your customizations overtop the plugin's base field configurations.
```ts ```ts
import { CollectionConfig } from "payload/types"; import { CollectionConfig } from "payload/types";
@@ -187,9 +208,17 @@ const examplePageConfig: CollectionConfig = {
}; };
``` ```
<Banner type="warning">
<strong>Note:</strong>
<br />
If overriding the `name` of either `breadcrumbs` or `parent` fields, you must specify the `breadcrumbsFieldSlug` or `parentFieldSlug` respectively.
</Banner>
## Localization ## Localization
This plugin supports localization by default. If the `localization` property is set in your Payload config, the `breadcrumbs` field is automatically localized. For more details on how localization works in Payload, see the [Localization](https://payloadcms.com/docs/localization/overview) docs. This plugin supports localization by default. If the `localization` property is set in your Payload config,
the `breadcrumbs` field is automatically localized. For more details on how localization works in Payload, see
the [Localization](https://payloadcms.com/docs/localization/overview) docs.
## TypeScript ## TypeScript
@@ -201,4 +230,10 @@ import { PluginConfig, GenerateURL, GenerateLabel } from '@payloadcms/plugin-nes
## Examples ## Examples
The [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) contains an official [Nested Docs Plugin Example](https://github.com/payloadcms/payload/tree/main/examples/nested-docs) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere), both of which use this plugin. The [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) contains an
official [Nested Docs Plugin Example](https://github.com/payloadcms/payload/tree/main/examples/nested-docs) which
demonstrates exactly how to configure this plugin in Payload and implement it on your front-end.
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an
official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website)
and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere), both of which use this
plugin.

View File

@@ -287,5 +287,5 @@ import {
## Examples ## Examples
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template. The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template.

View File

@@ -4301,9 +4301,9 @@ focus-trap@^6.9.2:
tabbable "^5.3.3" tabbable "^5.3.3"
follow-redirects@^1.15.2: follow-redirects@^1.15.2:
version "1.15.2" version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3: for-each@^0.3.3:
version "0.3.3" version "0.3.3"

View File

@@ -80,7 +80,7 @@
"lexical": "0.12.5", "lexical": "0.12.5",
"lint-staged": "^14.0.1", "lint-staged": "^14.0.1",
"minimist": "1.2.8", "minimist": "1.2.8",
"mongodb-memory-server": "8.13.0", "mongodb-memory-server": "^9",
"node-fetch": "2.6.12", "node-fetch": "2.6.12",
"nodemon": "3.0.2", "nodemon": "3.0.2",
"prettier": "^3.0.3", "prettier": "^3.0.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/db-mongodb", "name": "@payloadcms/db-mongodb",
"version": "1.1.1", "version": "1.3.0",
"description": "The officially supported MongoDB database adapter for Payload", "description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload", "repository": "https://github.com/payloadcms/payload",
"license": "MIT", "license": "MIT",
@@ -23,7 +23,7 @@
"bson-objectid": "2.0.4", "bson-objectid": "2.0.4",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"get-port": "5.1.1", "get-port": "5.1.1",
"mongoose": "6.12.0", "mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6", "mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22", "mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2", "prompts": "2.4.2",
@@ -33,7 +33,8 @@
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.9", "@types/mongoose-aggregate-paginate-v2": "1.0.9",
"mongodb-memory-server": "8.13.0", "mongodb": "4.17.1",
"mongodb-memory-server": "^9",
"payload": "workspace:*" "payload": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -48,6 +48,13 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
try { try {
this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection
const client = this.connection.getClient()
if (!client.options.replicaSet || this.transactionOptions === false) {
this.transactionOptions = false
this.beginTransaction = undefined
}
if (process.env.PAYLOAD_DROP_DATABASE === 'true') { if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info('---- DROPPING DATABASE ----') this.payload.logger.info('---- DROPPING DATABASE ----')
await mongoose.connection.dropDatabase() await mongoose.connection.dropDatabase()

View File

@@ -55,8 +55,11 @@ export const find: Find = async function find(
useEstimatedCount, useEstimatedCount,
} }
if (!useEstimatedCount && this.disableIndexHints !== true) { 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. // 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 = () => { paginationOptions.useCustomCountFn = () => {
return Promise.resolve( return Promise.resolve(
Model.countDocuments(query, { Model.countDocuments(query, {

View File

@@ -74,8 +74,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
useEstimatedCount, useEstimatedCount,
} }
if (!useEstimatedCount && this.disableIndexHints !== true) { 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. // 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 = () => { paginationOptions.useCustomCountFn = () => {
return Promise.resolve( return Promise.resolve(
Model.countDocuments(query, { Model.countDocuments(query, {

View File

@@ -71,8 +71,11 @@ export const findVersions: FindVersions = async function findVersions(
useEstimatedCount, useEstimatedCount,
} }
if (!useEstimatedCount && this.disableIndexHints !== true) { 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. // 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 = () => { paginationOptions.useCustomCountFn = () => {
return Promise.resolve( return Promise.resolve(
Model.countDocuments(query, { Model.countDocuments(query, {

View File

@@ -1,3 +1,4 @@
import type { TransactionOptions } from 'mongodb'
import type { ClientSession, ConnectOptions, Connection } from 'mongoose' import type { ClientSession, ConnectOptions, Connection } from 'mongoose'
import type { Payload } from 'payload' import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database' import type { BaseDatabaseAdapter } from 'payload/database'
@@ -7,8 +8,6 @@ import mongoose from 'mongoose'
import path from 'path' import path from 'path'
import { createDatabaseAdapter } from 'payload/database' import { createDatabaseAdapter } from 'payload/database'
export type { MigrateDownArgs, MigrateUpArgs } from './types'
import type { CollectionModel, GlobalModel } from './types' import type { CollectionModel, GlobalModel } from './types'
import { connect } from './connect' import { connect } from './connect'
@@ -39,6 +38,8 @@ import { updateGlobalVersion } from './updateGlobalVersion'
import { updateOne } from './updateOne' import { updateOne } from './updateOne'
import { updateVersion } from './updateVersion' import { updateVersion } from './updateVersion'
export type { MigrateDownArgs, MigrateUpArgs } from './types'
export interface Args { export interface Args {
/** Set to false to disable auto-pluralization of collection names, Defaults to true */ /** Set to false to disable auto-pluralization of collection names, Defaults to true */
autoPluralization?: boolean autoPluralization?: boolean
@@ -50,6 +51,7 @@ export interface Args {
/** 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 */ /** 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 disableIndexHints?: boolean
migrationDir?: string migrationDir?: string
transactionOptions?: TransactionOptions | false
/** The URL to connect to MongoDB or false to start payload and prevent connecting */ /** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string url: false | string
} }
@@ -81,6 +83,7 @@ declare module 'payload' {
globals: GlobalModel globals: GlobalModel
mongoMemoryServer: any mongoMemoryServer: any
sessions: Record<number | string, ClientSession> sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
versions: { versions: {
[slug: string]: CollectionModel [slug: string]: CollectionModel
} }
@@ -92,15 +95,21 @@ export function mongooseAdapter({
connectOptions, connectOptions,
disableIndexHints = false, disableIndexHints = false,
migrationDir: migrationDirArg, migrationDir: migrationDirArg,
transactionOptions,
url, url,
}: Args): MongooseAdapterResult { }: Args): MongooseAdapterResult {
function adapter({ payload }: { payload: Payload }) { function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(migrationDirArg) const migrationDir = findMigrationDir(migrationDirArg)
let beginTransactionFunction = beginTransaction
mongoose.set('strictQuery', false) mongoose.set('strictQuery', false)
extendWebpackConfig(payload.config) extendWebpackConfig(payload.config)
extendViteConfig(payload.config) extendViteConfig(payload.config)
if (transactionOptions === false) {
beginTransactionFunction = () => null
}
return createDatabaseAdapter<MongooseAdapter>({ return createDatabaseAdapter<MongooseAdapter>({
name: 'mongoose', name: 'mongoose',
@@ -113,11 +122,12 @@ export function mongooseAdapter({
globals: undefined, globals: undefined,
mongoMemoryServer: undefined, mongoMemoryServer: undefined,
sessions: {}, sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url, url,
versions: {}, versions: {},
// DatabaseAdapter // DatabaseAdapter
beginTransaction, beginTransaction: beginTransactionFunction,
commitTransaction, commitTransaction,
connect, connect,
create, create,

View File

@@ -1,6 +1,9 @@
import type { PayloadRequest } from 'payload/types' import type { PayloadRequest } from 'payload/types'
import { readMigrationFiles } from 'payload/database' import { readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import prompts from 'prompts' import prompts from 'prompts'
import type { MongooseAdapter } from '.' import type { MongooseAdapter } from '.'
@@ -14,9 +17,9 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
const { confirm: acceptWarning } = await prompts( const { confirm: acceptWarning } = await prompts(
{ {
name: 'confirm', name: 'confirm',
type: 'confirm',
initial: false, initial: false,
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`, message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
type: 'confirm',
}, },
{ {
onCancel: () => { onCancel: () => {
@@ -40,29 +43,29 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
msg: `Found ${migrationFiles.length} migration files.`, msg: `Found ${migrationFiles.length} migration files.`,
}) })
let transactionID const req = {} as PayloadRequest
// Run all migrate up // Run all migrate up
for (const migration of migrationFiles) { for (const migration of migrationFiles) {
payload.logger.info({ msg: `Migrating: ${migration.name}` }) payload.logger.info({ msg: `Migrating: ${migration.name}` })
try { try {
const start = Date.now() const start = Date.now()
transactionID = await this.beginTransaction() await initTransaction(req)
await migration.up({ payload }) await migration.up({ payload, req })
await payload.create({ await payload.create({
collection: 'payload-migrations', collection: 'payload-migrations',
data: { data: {
name: migration.name, name: migration.name,
batch: 1, batch: 1,
}, },
req: { req,
transactionID,
} as PayloadRequest,
}) })
await this.commitTransaction(transactionID)
await commitTransaction(req)
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
} catch (err: unknown) { } catch (err: unknown) {
await this.rollbackTransaction(transactionID) await killTransaction(req)
payload.logger.error({ payload.logger.error({
err, err,
msg: `Error running migration ${migration.name}. Rolling back.`, msg: `Error running migration ${migration.name}. Rolling back.`,

View File

@@ -547,7 +547,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
config: SanitizedConfig, config: SanitizedConfig,
buildSchemaOptions: BuildSchemaOptions, buildSchemaOptions: BuildSchemaOptions,
): void => { ): void => {
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String } const baseSchema = {
...formatBaseSchema(field, buildSchemaOptions),
type: field.hasMany ? [String] : String,
}
schema.add({ schema.add({
[field.name]: localizeSchema(field, baseSchema, config.localization), [field.name]: localizeSchema(field, baseSchema, config.localization),

View File

@@ -58,8 +58,15 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
useEstimatedCount, useEstimatedCount,
} }
if (!useEstimatedCount && this.disableIndexHints !== true) { if (
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding a hint. !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 = () => { paginationOptions.useCustomCountFn = () => {
return Promise.resolve( return Promise.resolve(
VersionModel.countDocuments(versionQuery, { VersionModel.countDocuments(versionQuery, {

View File

@@ -1,34 +1,30 @@
// @ts-expect-error // TODO: Fix this import
import type { TransactionOptions } from 'mongodb' import type { TransactionOptions } from 'mongodb'
import type { BeginTransaction } from 'payload/database' import type { BeginTransaction } from 'payload/database'
import { APIError } from 'payload/errors' import { APIError } from 'payload/errors'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
let transactionsNotAvailable: boolean import type { MongooseAdapter } from '../index'
export const beginTransaction: BeginTransaction = async function beginTransaction( export const beginTransaction: BeginTransaction = async function beginTransaction(
options: TransactionOptions = {}, this: MongooseAdapter,
options: TransactionOptions,
) { ) {
let id = null
if (!this.connection) { if (!this.connection) {
throw new APIError('beginTransaction called while no connection to the database exists') throw new APIError('beginTransaction called while no connection to the database exists')
} }
if (transactionsNotAvailable) return id
const client = this.connection.getClient() const client = this.connection.getClient()
if (!client.options.replicaSet) { const id = uuid()
transactionsNotAvailable = true
} else { if (!this.sessions[id]) {
id = uuid() this.sessions[id] = client.startSession()
if (!this.sessions[id]) {
this.sessions[id] = await client.startSession()
}
if (this.sessions[id].inTransaction()) {
this.payload.logger.warn('beginTransaction called while transaction already exists')
} else {
await this.sessions[id].startTransaction(options)
}
} }
if (this.sessions[id].inTransaction()) {
this.payload.logger.warn('beginTransaction called while transaction already exists')
} else {
this.sessions[id].startTransaction(options || (this.transactionOptions as TransactionOptions))
}
return id return id
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/db-postgres", "name": "@payloadcms/db-postgres",
"version": "0.2.2", "version": "0.3.0",
"description": "The officially supported Postgres database adapter for Payload", "description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload", "repository": "https://github.com/payloadcms/payload",
"license": "MIT", "license": "MIT",

View File

@@ -33,6 +33,16 @@ export const buildFindManyArgs = ({
}, },
} }
if (adapter.tables[`${tableName}_texts`]) {
result.with._texts = {
columns: {
id: false,
parent: false,
},
orderBy: ({ order }, { asc: ASC }) => [ASC(order)],
}
}
if (adapter.tables[`${tableName}_numbers`]) { if (adapter.tables[`${tableName}_numbers`]) {
result.with._numbers = { result.with._numbers = {
columns: { columns: {

View File

@@ -9,6 +9,7 @@ import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build' import { buildTable } from './schema/build'
import { getConfigIDType } from './schema/getConfigIDType'
export const init: Init = async function init(this: PostgresAdapter) { export const init: Init = async function init(this: PostgresAdapter) {
if (this.payload.config.localization) { if (this.payload.config.localization) {
@@ -23,6 +24,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({ buildTable({
adapter: this, adapter: this,
buildTexts: true,
buildNumbers: true, buildNumbers: true,
buildRelationships: true, buildRelationships: true,
disableNotNull: !!collection?.versions?.drafts, disableNotNull: !!collection?.versions?.drafts,
@@ -36,8 +38,11 @@ export const init: Init = async function init(this: PostgresAdapter) {
const versionsTableName = `_${tableName}_v` const versionsTableName = `_${tableName}_v`
const versionFields = buildVersionCollectionFields(collection) const versionFields = buildVersionCollectionFields(collection)
const versionsParentIDColType = getConfigIDType(collection.fields)
buildTable({ buildTable({
adapter: this, adapter: this,
buildTexts: true,
buildNumbers: true, buildNumbers: true,
buildRelationships: true, buildRelationships: true,
disableNotNull: !!collection.versions?.drafts, disableNotNull: !!collection.versions?.drafts,
@@ -54,6 +59,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({ buildTable({
adapter: this, adapter: this,
buildTexts: true,
buildNumbers: true, buildNumbers: true,
buildRelationships: true, buildRelationships: true,
disableNotNull: !!global?.versions?.drafts, disableNotNull: !!global?.versions?.drafts,
@@ -69,6 +75,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({ buildTable({
adapter: this, adapter: this,
buildTexts: true,
buildNumbers: true, buildNumbers: true,
buildRelationships: true, buildRelationships: true,
disableNotNull: !!global.versions?.drafts, disableNotNull: !!global.versions?.drafts,

View File

@@ -1,8 +1,12 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */ /* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { Payload } from 'payload' import type { Payload } from 'payload'
import type { Migration } from 'payload/database' import type { Migration } from 'payload/database'
import type { PayloadRequest } from 'payload/dist/express/types'
import { readMigrationFiles } from 'payload/database' import { readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import prompts from 'prompts' import prompts from 'prompts'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
@@ -42,11 +46,11 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
const { confirm: runMigrations } = await prompts( const { confirm: runMigrations } = await prompts(
{ {
name: 'confirm', name: 'confirm',
type: 'confirm',
initial: false, initial: false,
message: message:
"It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" + "It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" +
"If you'd like to run migrations, data loss will occur. Would you like to proceed?", "If you'd like to run migrations, data loss will occur. Would you like to proceed?",
type: 'confirm',
}, },
{ {
onCancel: () => { onCancel: () => {
@@ -79,6 +83,7 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
const { generateDrizzleJson } = require('drizzle-kit/utils') const { generateDrizzleJson } = require('drizzle-kit/utils')
const start = Date.now() const start = Date.now()
const req = {} as PayloadRequest
payload.logger.info({ msg: `Migrating: ${migration.name}` }) payload.logger.info({ msg: `Migrating: ${migration.name}` })
@@ -86,7 +91,8 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
const drizzleJSON = generateDrizzleJson(pgAdapter.schema) const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
try { try {
await migration.up({ payload }) await initTransaction(req)
await migration.up({ payload, req })
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
await payload.create({ await payload.create({
collection: 'payload-migrations', collection: 'payload-migrations',
@@ -95,8 +101,11 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
batch, batch,
schema: drizzleJSON, schema: drizzleJSON,
}, },
req,
}) })
await commitTransaction(req)
} catch (err: unknown) { } catch (err: unknown) {
await killTransaction(req)
payload.logger.error({ payload.logger.error({
err, err,
msg: parseError(err, `Error running migration ${migration.name}`), msg: parseError(err, `Error running migration ${migration.name}`),

View File

@@ -2,6 +2,9 @@
import type { PayloadRequest } from 'payload/types' import type { PayloadRequest } from 'payload/types'
import { getMigrations, readMigrationFiles } from 'payload/database' import { getMigrations, readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
@@ -25,19 +28,21 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`, msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
}) })
for (const migration of existingMigrations) { const latestBatchMigrations = existingMigrations.filter(({ batch }) => batch === latestBatch)
for (const migration of latestBatchMigrations) {
const migrationFile = migrationFiles.find((m) => m.name === migration.name) const migrationFile = migrationFiles.find((m) => m.name === migration.name)
if (!migrationFile) { if (!migrationFile) {
throw new Error(`Migration ${migration.name} not found locally.`) throw new Error(`Migration ${migration.name} not found locally.`)
} }
const start = Date.now() const start = Date.now()
let transactionID const req = {} as PayloadRequest
try { try {
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` }) payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })
transactionID = await this.beginTransaction() await initTransaction(req)
await migrationFile.down({ payload }) await migrationFile.down({ payload, req })
payload.logger.info({ payload.logger.info({
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
}) })
@@ -47,15 +52,13 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
await payload.delete({ await payload.delete({
id: migration.id, id: migration.id,
collection: 'payload-migrations', collection: 'payload-migrations',
req: { req,
transactionID,
} as PayloadRequest,
}) })
} }
await this.commitTransaction(transactionID) await commitTransaction(req)
} catch (err: unknown) { } catch (err: unknown) {
await this.rollbackTransaction(transactionID) await killTransaction(req)
payload.logger.error({ payload.logger.error({
err, err,

View File

@@ -2,6 +2,9 @@ import type { PayloadRequest } from 'payload/types'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
import { readMigrationFiles } from 'payload/database' import { readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import prompts from 'prompts' import prompts from 'prompts'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
@@ -17,9 +20,9 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
const { confirm: acceptWarning } = await prompts( const { confirm: acceptWarning } = await prompts(
{ {
name: 'confirm', name: 'confirm',
type: 'confirm',
initial: false, initial: false,
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`, message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
type: 'confirm',
}, },
{ {
onCancel: () => { onCancel: () => {
@@ -36,36 +39,35 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
msg: `Dropping database.`, msg: `Dropping database.`,
}) })
await this.drizzle.execute(sql`drop schema public cascade;\ncreate schema public;`) await this.drizzle.execute(sql`drop schema public cascade;
create schema public;`)
const migrationFiles = await readMigrationFiles({ payload }) const migrationFiles = await readMigrationFiles({ payload })
payload.logger.debug({ payload.logger.debug({
msg: `Found ${migrationFiles.length} migration files.`, msg: `Found ${migrationFiles.length} migration files.`,
}) })
let transactionID const req = {} as PayloadRequest
// Run all migrate up // Run all migrate up
for (const migration of migrationFiles) { for (const migration of migrationFiles) {
payload.logger.info({ msg: `Migrating: ${migration.name}` }) payload.logger.info({ msg: `Migrating: ${migration.name}` })
try { try {
const start = Date.now() const start = Date.now()
transactionID = await this.beginTransaction() await initTransaction(req)
await migration.up({ payload }) await migration.up({ payload, req })
await payload.create({ await payload.create({
collection: 'payload-migrations', collection: 'payload-migrations',
data: { data: {
name: migration.name, name: migration.name,
batch: 1, batch: 1,
}, },
req: { req,
transactionID,
} as PayloadRequest,
}) })
await this.commitTransaction(transactionID) await commitTransaction(req)
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
} catch (err: unknown) { } catch (err: unknown) {
await this.rollbackTransaction(transactionID) await killTransaction(req)
payload.logger.error({ payload.logger.error({
err, err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back`), msg: parseError(err, `Error running migration ${migration.name}. Rolling back`),

View File

@@ -2,7 +2,9 @@
import type { PayloadRequest } from 'payload/types' import type { PayloadRequest } from 'payload/types'
import { getMigrations, readMigrationFiles } from 'payload/database' import { getMigrations, readMigrationFiles } from 'payload/database'
import { DatabaseError } from 'pg' import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
@@ -29,7 +31,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`, msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
}) })
let transactionID const req = {} as PayloadRequest
// Reverse order of migrations to rollback // Reverse order of migrations to rollback
existingMigrations.reverse() existingMigrations.reverse()
@@ -43,8 +45,8 @@ export async function migrateRefresh(this: PostgresAdapter) {
payload.logger.info({ msg: `Migrating down: ${migration.name}` }) payload.logger.info({ msg: `Migrating down: ${migration.name}` })
const start = Date.now() const start = Date.now()
transactionID = await this.beginTransaction() await initTransaction(req)
await migrationFile.down({ payload }) await migrationFile.down({ payload, req })
payload.logger.info({ payload.logger.info({
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`, msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
}) })
@@ -53,9 +55,7 @@ export async function migrateRefresh(this: PostgresAdapter) {
if (tableExists) { if (tableExists) {
await payload.delete({ await payload.delete({
collection: 'payload-migrations', collection: 'payload-migrations',
req: { req,
transactionID,
} as PayloadRequest,
where: { where: {
name: { name: {
equals: migration.name, equals: migration.name,
@@ -63,8 +63,9 @@ export async function migrateRefresh(this: PostgresAdapter) {
}, },
}) })
} }
await commitTransaction(req)
} catch (err: unknown) { } catch (err: unknown) {
await this.rollbackTransaction(transactionID) await killTransaction(req)
payload.logger.error({ payload.logger.error({
err, err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`), msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),
@@ -78,23 +79,21 @@ export async function migrateRefresh(this: PostgresAdapter) {
payload.logger.info({ msg: `Migrating: ${migration.name}` }) payload.logger.info({ msg: `Migrating: ${migration.name}` })
try { try {
const start = Date.now() const start = Date.now()
transactionID = await this.beginTransaction() await initTransaction(req)
await migration.up({ payload }) await migration.up({ payload, req })
await payload.create({ await payload.create({
collection: 'payload-migrations', collection: 'payload-migrations',
data: { data: {
name: migration.name, name: migration.name,
executed: true, executed: true,
}, },
req: { req,
transactionID,
} as PayloadRequest,
}) })
await this.commitTransaction(transactionID) await commitTransaction(req)
payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` }) payload.logger.info({ msg: `Migrated: ${migration.name} (${Date.now() - start}ms)` })
} catch (err: unknown) { } catch (err: unknown) {
await this.rollbackTransaction(transactionID) await killTransaction(req)
payload.logger.error({ payload.logger.error({
err, err,
msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`), msg: parseError(err, `Error running migration ${migration.name}. Rolling back.`),

View File

@@ -2,6 +2,9 @@
import type { PayloadRequest } from 'payload/types' import type { PayloadRequest } from 'payload/types'
import { getMigrations, readMigrationFiles } from 'payload/database' import { getMigrations, readMigrationFiles } from 'payload/database'
import { commitTransaction } from 'payload/dist/utilities/commitTransaction'
import { initTransaction } from 'payload/dist/utilities/initTransaction'
import { killTransaction } from 'payload/dist/utilities/killTransaction'
import type { PostgresAdapter } from './types' import type { PostgresAdapter } from './types'
@@ -21,10 +24,10 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
return return
} }
const req = {} as PayloadRequest
// Rollback all migrations in order // Rollback all migrations in order
for (const migration of existingMigrations) { for (const migration of existingMigrations) {
let transactionID
const migrationFile = migrationFiles.find((m) => m.name === migration.name) const migrationFile = migrationFiles.find((m) => m.name === migration.name)
try { try {
if (!migrationFile) { if (!migrationFile) {
@@ -33,8 +36,8 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
const start = Date.now() const start = Date.now()
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` }) payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })
transactionID = await this.beginTransaction() await initTransaction(req)
await migrationFile.down({ payload }) await migrationFile.down({ payload, req })
payload.logger.info({ payload.logger.info({
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`, msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
}) })
@@ -44,19 +47,17 @@ export async function migrateReset(this: PostgresAdapter): Promise<void> {
await payload.delete({ await payload.delete({
id: migration.id, id: migration.id,
collection: 'payload-migrations', collection: 'payload-migrations',
req: { req,
transactionID,
} as PayloadRequest,
}) })
} }
await this.commitTransaction(transactionID) await commitTransaction(req)
} catch (err: unknown) { } catch (err: unknown) {
let msg = `Error running migration ${migrationFile.name}.` let msg = `Error running migration ${migrationFile.name}.`
if (err instanceof Error) msg += ` ${err.message}` if (err instanceof Error) msg += ` ${err.message}`
await this.rollbackTransaction(transactionID) await killTransaction(req)
payload.logger.error({ payload.logger.error({
err, err,
msg, msg,

View File

@@ -107,7 +107,11 @@ export async function parseParams({
} }
}) })
if (['json', 'richText'].includes(field.type) && Array.isArray(pathSegments)) { if (
['json', 'richText'].includes(field.type) &&
Array.isArray(pathSegments) &&
pathSegments.length > 1
) {
const segments = pathSegments.slice(1) const segments = pathSegments.slice(1)
segments.unshift(table[columnName].name) segments.unshift(table[columnName].name)
@@ -121,12 +125,28 @@ export async function parseParams({
}) })
constraints.push(sql.raw(jsonQuery)) constraints.push(sql.raw(jsonQuery))
break
} }
if (field.type === 'json') { const jsonQuery = convertPathToJSONTraversal(pathSegments)
const jsonQuery = convertPathToJSONTraversal(pathSegments) const operatorKeys = {
constraints.push(sql.raw(`${table[columnName].name}${jsonQuery} = '%${val}%'`)) contains: { operator: 'ilike', wildcard: '%' },
equals: { operator: '=', wildcard: '' },
exists: { operator: val === true ? 'is not null' : 'is null' },
like: { operator: 'like', wildcard: '%' },
not_equals: { operator: '<>', wildcard: '' },
} }
let formattedValue = `'${operatorKeys[operator].wildcard}${val}${operatorKeys[operator].wildcard}'`
if (operator === 'exists') {
formattedValue = ''
}
constraints.push(
sql.raw(
`${table[columnName].name}${jsonQuery} ${operatorKeys[operator].operator} ${formattedValue}`,
),
)
break break
} }

View File

@@ -19,6 +19,7 @@ import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, PostgresAdapter } from '../types' import type { GenericColumns, GenericTable, PostgresAdapter } from '../types'
import { getConfigIDType } from './getConfigIDType'
import { parentIDColumnMap } from './parentIDColumnMap' import { parentIDColumnMap } from './parentIDColumnMap'
import { traverseFields } from './traverseFields' import { traverseFields } from './traverseFields'
@@ -26,6 +27,7 @@ type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder> baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder> baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
buildTexts?: boolean
buildNumbers?: boolean buildNumbers?: boolean
buildRelationships?: boolean buildRelationships?: boolean
disableNotNull: boolean disableNotNull: boolean
@@ -40,6 +42,7 @@ type Args = {
} }
type Result = { type Result = {
hasManyTextField: 'index' | boolean
hasManyNumberField: 'index' | boolean hasManyNumberField: 'index' | boolean
relationsToBuild: Map<string, string> relationsToBuild: Map<string, string>
} }
@@ -48,6 +51,7 @@ export const buildTable = ({
adapter, adapter,
baseColumns = {}, baseColumns = {},
baseExtraConfig = {}, baseExtraConfig = {},
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
disableNotNull, disableNotNull,
@@ -66,12 +70,15 @@ export const buildTable = ({
let hasLocalizedField = false let hasLocalizedField = false
let hasLocalizedRelationshipField = false let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasManyNumberField: 'index' | boolean = false let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasLocalizedManyNumberField = false let hasLocalizedManyNumberField = false
const localesColumns: Record<string, PgColumnBuilder> = {} const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {} const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable let localesTable: GenericTable
let textsTable: GenericTable
let numbersTable: GenericTable let numbersTable: GenericTable
// Relationships to the base collection // Relationships to the base collection
@@ -82,30 +89,25 @@ export const buildTable = ({
// Drizzle relations // Drizzle relations
const relationsToBuild: Map<string, string> = new Map() const relationsToBuild: Map<string, string> = new Map()
const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id') const idColType = getConfigIDType(fields)
let idColType = 'integer'
if (idField) { const idColTypeMap = {
if (idField.type === 'number') { integer: serial,
idColType = 'numeric' numeric,
columns.id = numeric('id').primaryKey() varchar,
}
if (idField.type === 'text') {
idColType = 'varchar'
columns.id = varchar('id').primaryKey()
}
} else {
columns.id = serial('id').primaryKey()
} }
columns.id = idColTypeMap[idColType]('id').primaryKey()
;({ ;({
hasLocalizedField, hasLocalizedField,
hasLocalizedManyTextField,
hasLocalizedManyNumberField, hasLocalizedManyNumberField,
hasLocalizedRelationshipField, hasLocalizedRelationshipField,
hasManyTextField,
hasManyNumberField, hasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
columns, columns,
@@ -190,6 +192,50 @@ export const buildTable = ({
adapter.relations[`relations_${localeTableName}`] = localesTableRelations adapter.relations[`relations_${localeTableName}`] = localesTableRelations
} }
if (hasManyTextField && buildTexts) {
const textsTableName = `${rootTableName}_texts`
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
text: varchar('text'),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
path: varchar('path').notNull(),
}
if (hasLocalizedManyTextField) {
columns.locale = adapter.enums.enum__locales('locale')
}
textsTable = pgTable(textsTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
orderParentIdx: index('order_parent_idx').on(cols.order, cols.parent),
}
if (hasManyTextField === 'index') {
indexes.text_idx = index('text_idx').on(cols.text)
}
if (hasLocalizedManyTextField) {
indexes.localeParent = index('locale_parent').on(cols.locale, cols.parent)
}
return indexes
})
adapter.tables[textsTableName] = textsTable
const textsTableRelations = relations(textsTable, ({ one }) => ({
parent: one(table, {
fields: [textsTable.parent],
references: [table.id],
}),
}))
adapter.relations[`relations_${textsTableName}`] = textsTableRelations
}
if (hasManyNumberField && buildNumbers) { if (hasManyNumberField && buildNumbers) {
const numbersTableName = `${rootTableName}_numbers` const numbersTableName = `${rootTableName}_numbers`
const columns: Record<string, PgColumnBuilder> = { const columns: Record<string, PgColumnBuilder> = {
@@ -317,6 +363,9 @@ export const buildTable = ({
result._locales = many(localesTable) result._locales = many(localesTable)
} }
if (hasManyTextField) {
result._texts = many(textsTable)
}
if (hasManyNumberField) { if (hasManyNumberField) {
result._numbers = many(numbersTable) result._numbers = many(numbersTable)
} }
@@ -332,5 +381,5 @@ export const buildTable = ({
adapter.relations[`relations_${tableName}`] = tableRelations adapter.relations[`relations_${tableName}`] = tableRelations
return { hasManyNumberField, relationsToBuild } return { hasManyTextField, hasManyNumberField, relationsToBuild }
} }

View File

@@ -0,0 +1,17 @@
import { type Field, fieldAffectsData } from 'payload/types'
export const getConfigIDType = (fields: Field[]): string => {
const idField = fields.find((field) => fieldAffectsData(field) && field.name === 'id')
if (idField) {
if (idField.type === 'number') {
return 'numeric'
}
if (idField.type === 'text') {
return 'varchar'
}
}
return 'integer'
}

View File

@@ -32,6 +32,7 @@ import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdent
type Args = { type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
buildTexts: boolean
buildNumbers: boolean buildNumbers: boolean
buildRelationships: boolean buildRelationships: boolean
columnPrefix?: string columnPrefix?: string
@@ -55,13 +56,16 @@ type Args = {
type Result = { type Result = {
hasLocalizedField: boolean hasLocalizedField: boolean
hasLocalizedManyTextField: boolean
hasLocalizedManyNumberField: boolean hasLocalizedManyNumberField: boolean
hasLocalizedRelationshipField: boolean hasLocalizedRelationshipField: boolean
hasManyTextField: 'index' | boolean
hasManyNumberField: 'index' | boolean hasManyNumberField: 'index' | boolean
} }
export const traverseFields = ({ export const traverseFields = ({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
columnPrefix, columnPrefix,
@@ -84,6 +88,8 @@ export const traverseFields = ({
}: Args): Result => { }: Args): Result => {
let hasLocalizedField = false let hasLocalizedField = false
let hasLocalizedRelationshipField = false let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasManyNumberField: 'index' | boolean = false let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyNumberField = false let hasLocalizedManyNumberField = false
@@ -135,7 +141,28 @@ export const traverseFields = ({
} }
switch (field.type) { switch (field.type) {
case 'text': case 'text': {
if (field.hasMany) {
if (field.localized) {
hasLocalizedManyTextField = true
}
if (field.index) {
hasManyTextField = 'index'
} else if (!hasManyTextField) {
hasManyTextField = true
}
if (field.unique) {
throw new InvalidConfiguration(
'Unique is not supported in Postgres for hasMany text fields.',
)
}
} else {
targetTable[fieldName] = varchar(columnName)
}
break
}
case 'email': case 'email':
case 'code': case 'code':
case 'textarea': { case 'textarea': {
@@ -286,21 +313,28 @@ export const traverseFields = ({
baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale) baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale)
} }
const { hasManyNumberField: subHasManyNumberField, relationsToBuild: subRelationsToBuild } = const {
buildTable({ hasManyTextField: subHasManyTextField,
adapter, hasManyNumberField: subHasManyNumberField,
baseColumns, relationsToBuild: subRelationsToBuild,
baseExtraConfig, } = buildTable({
disableNotNull: disableNotNullFromHere, adapter,
disableUnique, baseColumns,
fields: disableUnique ? idToUUID(field.fields) : field.fields, baseExtraConfig,
rootRelationsToBuild, disableNotNull: disableNotNullFromHere,
rootRelationships: relationships, disableUnique,
rootTableIDColType, fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootTableName, rootRelationsToBuild,
tableName: arrayTableName, rootRelationships: relationships,
}) rootTableIDColType,
rootTableName,
tableName: arrayTableName,
})
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
}
if (subHasManyNumberField) { if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index') if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField hasManyNumberField = subHasManyNumberField
@@ -361,6 +395,7 @@ export const traverseFields = ({
} }
const { const {
hasManyTextField: subHasManyTextField,
hasManyNumberField: subHasManyNumberField, hasManyNumberField: subHasManyNumberField,
relationsToBuild: subRelationsToBuild, relationsToBuild: subRelationsToBuild,
} = buildTable({ } = buildTable({
@@ -377,6 +412,11 @@ export const traverseFields = ({
tableName: blockTableName, tableName: blockTableName,
}) })
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
}
if (subHasManyNumberField) { if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index') if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField hasManyNumberField = subHasManyNumberField
@@ -425,11 +465,14 @@ export const traverseFields = ({
if (!('name' in field)) { if (!('name' in field)) {
const { const {
hasLocalizedField: groupHasLocalizedField, hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField, hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField, hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
hasManyTextField: groupHasManyTextField,
hasManyNumberField: groupHasManyNumberField, hasManyNumberField: groupHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
columnPrefix, columnPrefix,
@@ -453,6 +496,8 @@ export const traverseFields = ({
if (groupHasLocalizedField) hasLocalizedField = true if (groupHasLocalizedField) hasLocalizedField = true
if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (groupHasManyTextField) hasManyTextField = true
if (groupHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (groupHasManyNumberField) hasManyNumberField = true if (groupHasManyNumberField) hasManyNumberField = true
if (groupHasLocalizedManyNumberField) hasLocalizedManyNumberField = true if (groupHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break break
@@ -462,11 +507,14 @@ export const traverseFields = ({
const { const {
hasLocalizedField: groupHasLocalizedField, hasLocalizedField: groupHasLocalizedField,
hasLocalizedManyTextField: groupHasLocalizedManyTextField,
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField, hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
hasLocalizedRelationshipField: groupHasLocalizedRelationshipField, hasLocalizedRelationshipField: groupHasLocalizedRelationshipField,
hasManyTextField: groupHasManyTextField,
hasManyNumberField: groupHasManyNumberField, hasManyNumberField: groupHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
columnPrefix: `${columnName}_`, columnPrefix: `${columnName}_`,
@@ -490,6 +538,8 @@ export const traverseFields = ({
if (groupHasLocalizedField) hasLocalizedField = true if (groupHasLocalizedField) hasLocalizedField = true
if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true if (groupHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (groupHasManyTextField) hasManyTextField = true
if (groupHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (groupHasManyNumberField) hasManyNumberField = true if (groupHasManyNumberField) hasManyNumberField = true
if (groupHasLocalizedManyNumberField) hasLocalizedManyNumberField = true if (groupHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break break
@@ -500,11 +550,14 @@ export const traverseFields = ({
const { const {
hasLocalizedField: tabHasLocalizedField, hasLocalizedField: tabHasLocalizedField,
hasLocalizedManyTextField: tabHasLocalizedManyTextField,
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField, hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
hasLocalizedRelationshipField: tabHasLocalizedRelationshipField, hasLocalizedRelationshipField: tabHasLocalizedRelationshipField,
hasManyTextField: tabHasManyTextField,
hasManyNumberField: tabHasManyNumberField, hasManyNumberField: tabHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
columnPrefix, columnPrefix,
@@ -528,9 +581,10 @@ export const traverseFields = ({
if (tabHasLocalizedField) hasLocalizedField = true if (tabHasLocalizedField) hasLocalizedField = true
if (tabHasLocalizedRelationshipField) hasLocalizedRelationshipField = true if (tabHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (tabHasManyTextField) hasManyTextField = true
if (tabHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (tabHasManyNumberField) hasManyNumberField = true if (tabHasManyNumberField) hasManyNumberField = true
if (tabHasLocalizedManyNumberField) hasLocalizedManyNumberField = true if (tabHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break break
} }
@@ -539,11 +593,14 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const { const {
hasLocalizedField: rowHasLocalizedField, hasLocalizedField: rowHasLocalizedField,
hasLocalizedManyTextField: rowHasLocalizedManyTextField,
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField, hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
hasLocalizedRelationshipField: rowHasLocalizedRelationshipField, hasLocalizedRelationshipField: rowHasLocalizedRelationshipField,
hasManyTextField: rowHasManyTextField,
hasManyNumberField: rowHasManyNumberField, hasManyNumberField: rowHasManyNumberField,
} = traverseFields({ } = traverseFields({
adapter, adapter,
buildTexts,
buildNumbers, buildNumbers,
buildRelationships, buildRelationships,
columnPrefix, columnPrefix,
@@ -567,6 +624,8 @@ export const traverseFields = ({
if (rowHasLocalizedField) hasLocalizedField = true if (rowHasLocalizedField) hasLocalizedField = true
if (rowHasLocalizedRelationshipField) hasLocalizedRelationshipField = true if (rowHasLocalizedRelationshipField) hasLocalizedRelationshipField = true
if (rowHasManyTextField) hasManyTextField = true
if (rowHasLocalizedManyTextField) hasLocalizedManyTextField = true
if (rowHasManyNumberField) hasManyNumberField = true if (rowHasManyNumberField) hasManyNumberField = true
if (rowHasLocalizedManyNumberField) hasLocalizedManyNumberField = true if (rowHasLocalizedManyNumberField) hasLocalizedManyNumberField = true
break break
@@ -604,8 +663,10 @@ export const traverseFields = ({
return { return {
hasLocalizedField, hasLocalizedField,
hasLocalizedManyTextField,
hasLocalizedManyNumberField, hasLocalizedManyNumberField,
hasLocalizedRelationshipField, hasLocalizedRelationshipField,
hasManyTextField,
hasManyNumberField, hasManyNumberField,
} }
} }

View File

@@ -1,7 +1,7 @@
import type { Block } from 'payload/types' import type { Block, Field } from 'payload/types'
import { InvalidConfiguration } from 'payload/errors' import { InvalidConfiguration } from 'payload/errors'
import { flattenTopLevelFields } from 'payload/utilities' import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/types'
import type { GenericTable } from '../types' import type { GenericTable } from '../types'
@@ -12,6 +12,42 @@ type Args = {
table: GenericTable table: GenericTable
} }
const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[] => {
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
if (field.type === 'blocks') {
return fieldsToUse
}
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}.` : prefix
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
}
if (field.type === 'tabs') {
return [
...fieldsToUse,
...field.tabs.reduce((tabFields, tab) => {
fieldPrefix = 'name' in tab ? `${prefix}.${tab.name}` : prefix
return [
...tabFields,
...(tabHasName(tab)
? [{ ...tab, type: 'tab' }]
: getFlattenedFieldNames(tab.fields, fieldPrefix)),
]
}, []),
]
}
if (fieldAffectsData(field)) {
return [...fieldsToUse, `${fieldPrefix?.replace('.', '_') || ''}${field.name}`]
}
return fieldsToUse
}, [])
}
export const validateExistingBlockIsIdentical = ({ export const validateExistingBlockIsIdentical = ({
block, block,
localized, localized,
@@ -19,17 +55,23 @@ export const validateExistingBlockIsIdentical = ({
table, table,
}: Args): void => { }: Args): void => {
if (table) { if (table) {
const fieldNames = flattenTopLevelFields(block.fields).flatMap((field) => field.name) const fieldNames = getFlattenedFieldNames(block.fields)
Object.keys(table).forEach((fieldName) => { const missingField =
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) { // ensure every field from the config is in the matching table
if (fieldNames.indexOf(fieldName) === -1) { fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) ||
throw new InvalidConfiguration( // ensure every table column is matched for every field from the config
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${fieldName}, while the other block does not.`, Object.keys(table).find((fieldName) => {
) if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
return fieldNames.indexOf(fieldName) === -1
} }
} })
})
if (missingField) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${missingField}, while the other block does not.`,
)
}
if (Boolean(localized) !== Boolean(table._locale)) { if (Boolean(localized) !== Boolean(table._locale)) {
throw new InvalidConfiguration( throw new InvalidConfiguration(

View File

@@ -11,11 +11,11 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
try { try {
id = uuid() id = uuid()
let reject: (value?: unknown) => void let reject: () => Promise<void>
let resolve: (value?: unknown) => void let resolve: () => Promise<void>
let transaction: DrizzleTransaction let transaction: DrizzleTransaction
let transactionReady: (value?: unknown) => void let transactionReady: () => void
// Drizzle only exposes a transactions API that is sufficient if you // Drizzle only exposes a transactions API that is sufficient if you
// can directly pass around the `tx` argument. But our operations are spread // can directly pass around the `tx` argument. But our operations are spread
@@ -24,13 +24,19 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
// and will call them in our respective transaction methods // and will call them in our respective transaction methods
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.drizzle const done = this.drizzle
.transaction(async (tx) => { .transaction(async (tx) => {
transaction = tx transaction = tx
await new Promise((res, rej) => { await new Promise<void>((res, rej) => {
resolve = () => {
res()
return done
}
reject = () => {
rej()
return done
}
transactionReady() transactionReady()
resolve = res
reject = rej
}) })
}) })
.catch(() => { .catch(() => {
@@ -39,7 +45,7 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
// Need to wait until the transaction is ready // Need to wait until the transaction is ready
// before binding its `resolve` and `reject` methods below // before binding its `resolve` and `reject` methods below
await new Promise((resolve) => (transactionReady = resolve)) await new Promise<void>((resolve) => (transactionReady = resolve))
this.sessions[id] = { this.sessions[id] = {
db: transaction, db: transaction,

View File

@@ -7,9 +7,9 @@ export const commitTransaction: CommitTransaction = async function commitTransac
} }
try { try {
this.sessions[id].resolve() await this.sessions[id].resolve()
} catch (err: unknown) { } catch (err: unknown) {
this.sessions[id].reject() await this.sessions[id].reject()
} }
delete this.sessions[id] delete this.sessions[id]

View File

@@ -0,0 +1,19 @@
/* eslint-disable no-param-reassign */
import type { TextField } from 'payload/types'
type Args = {
field: TextField
locale?: string
textRows: Record<string, unknown>[]
ref: Record<string, unknown>
}
export const transformHasManyText = ({ field, locale, textRows, ref }: Args) => {
const result = textRows.map(({ text }) => text)
if (locale) {
ref[field.name][locale] = result
} else {
ref[field.name] = result
}
}

View File

@@ -18,6 +18,7 @@ type TransformArgs = {
// into the shape Payload expects based on field schema // into the shape Payload expects based on field schema
export const transform = <T extends TypeWithID>({ config, data, fields }: TransformArgs): T => { export const transform = <T extends TypeWithID>({ config, data, fields }: TransformArgs): T => {
let relationships: Record<string, Record<string, unknown>[]> = {} let relationships: Record<string, Record<string, unknown>[]> = {}
let texts: Record<string, Record<string, unknown>[]> = {}
let numbers: Record<string, Record<string, unknown>[]> = {} let numbers: Record<string, Record<string, unknown>[]> = {}
if ('_rels' in data) { if ('_rels' in data) {
@@ -25,6 +26,11 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
delete data._rels delete data._rels
} }
if ('_texts' in data) {
texts = createPathMap(data._texts)
delete data._texts
}
if ('_numbers' in data) { if ('_numbers' in data) {
numbers = createPathMap(data._numbers) numbers = createPathMap(data._numbers)
delete data._numbers delete data._numbers
@@ -42,6 +48,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields, fields,
texts,
numbers, numbers,
path: '', path: '',
relationships, relationships,

View File

@@ -8,6 +8,7 @@ import type { BlocksMap } from '../../utilities/createBlocksMap'
import { transformHasManyNumber } from './hasManyNumber' import { transformHasManyNumber } from './hasManyNumber'
import { transformRelationship } from './relationship' import { transformRelationship } from './relationship'
import { transformHasManyText } from './hasManyText'
type TraverseFieldsArgs = { type TraverseFieldsArgs = {
/** /**
@@ -34,6 +35,10 @@ type TraverseFieldsArgs = {
* An array of Payload fields to traverse * An array of Payload fields to traverse
*/ */
fields: (Field | TabAsField)[] fields: (Field | TabAsField)[]
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
/** /**
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path * All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/ */
@@ -61,6 +66,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix, fieldPrefix,
fields, fields,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -77,6 +83,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix, fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -96,6 +103,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix, fieldPrefix,
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -127,6 +135,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}.${row._order - 1}`, path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships, relationships,
@@ -151,6 +160,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}.${i}`, path: `${sanitizedPath}${field.name}.${i}`,
relationships, relationships,
@@ -194,6 +204,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: block.fields, fields: block.fields,
texts,
numbers, numbers,
path: `${blockFieldPath}.${row._order - 1}`, path: `${blockFieldPath}.${row._order - 1}`,
relationships, relationships,
@@ -224,6 +235,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: '', fieldPrefix: '',
fields: block.fields, fields: block.fields,
texts,
numbers, numbers,
path: `${blockFieldPath}.${i}`, path: `${blockFieldPath}.${i}`,
relationships, relationships,
@@ -285,6 +297,40 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result return result
} }
if (field.type === 'text' && field?.hasMany) {
const textPathMatch = texts[`${sanitizedPath}${field.name}`]
if (!textPathMatch) return result
if (field.localized) {
result[field.name] = {}
const textsByLocale: Record<string, Record<string, unknown>[]> = {}
textPathMatch.forEach((row) => {
if (typeof row.locale === 'string') {
if (!textsByLocale[row.locale]) textsByLocale[row.locale] = []
textsByLocale[row.locale].push(row)
}
})
Object.entries(textsByLocale).forEach(([locale, texts]) => {
transformHasManyText({
field,
locale,
textRows: texts,
ref: result,
})
})
} else {
transformHasManyText({
field,
textRows: textPathMatch,
ref: result,
})
}
return result
}
if (field.type === 'number' && field.hasMany) { if (field.type === 'number' && field.hasMany) {
const numberPathMatch = numbers[`${sanitizedPath}${field.name}`] const numberPathMatch = numbers[`${sanitizedPath}${field.name}`]
if (!numberPathMatch) return result if (!numberPathMatch) return result
@@ -374,6 +420,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: groupFieldPrefix, fieldPrefix: groupFieldPrefix,
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}`, path: `${sanitizedPath}${field.name}`,
relationships, relationships,
@@ -390,6 +437,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions, deletions,
fieldPrefix: groupFieldPrefix, fieldPrefix: groupFieldPrefix,
fields: field.fields, fields: field.fields,
texts,
numbers, numbers,
path: `${sanitizedPath}${field.name}`, path: `${sanitizedPath}${field.name}`,
relationships, relationships,
@@ -400,6 +448,21 @@ export const traverseFields = <T extends Record<string, unknown>>({
break break
} }
case 'text': {
let val = fieldData
if (typeof fieldData === 'string') {
val = String(fieldData)
}
if (typeof locale === 'string') {
ref[locale] = val
} else {
result[field.name] = val
}
break
}
case 'number': { case 'number': {
let val = fieldData let val = fieldData
if (typeof fieldData === 'string') { if (typeof fieldData === 'string') {

View File

@@ -18,6 +18,7 @@ type Args = {
data: unknown data: unknown
field: ArrayField field: ArrayField
locale?: string locale?: string
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[] numbers: Record<string, unknown>[]
path: string path: string
relationships: Record<string, unknown>[] relationships: Record<string, unknown>[]
@@ -36,6 +37,7 @@ export const transformArray = ({
data, data,
field, field,
locale, locale,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -86,6 +88,7 @@ export const transformArray = ({
fieldPrefix: '', fieldPrefix: '',
fields: field.fields, fields: field.fields,
locales: newRow.locales, locales: newRow.locales,
texts,
numbers, numbers,
parentTableName: arrayTableName, parentTableName: arrayTableName,
path: `${path || ''}${field.name}.${i}.`, path: `${path || ''}${field.name}.${i}.`,

View File

@@ -18,6 +18,7 @@ type Args = {
data: Record<string, unknown>[] data: Record<string, unknown>[]
field: BlockField field: BlockField
locale?: string locale?: string
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[] numbers: Record<string, unknown>[]
path: string path: string
relationships: Record<string, unknown>[] relationships: Record<string, unknown>[]
@@ -34,6 +35,7 @@ export const transformBlocks = ({
data, data,
field, field,
locale, locale,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -84,6 +86,7 @@ export const transformBlocks = ({
fieldPrefix: '', fieldPrefix: '',
fields: matchedBlock.fields, fields: matchedBlock.fields,
locales: newRow.locales, locales: newRow.locales,
texts,
numbers, numbers,
parentTableName: blockTableName, parentTableName: blockTableName,
path: `${path || ''}${field.name}.${i}.`, path: `${path || ''}${field.name}.${i}.`,

View File

@@ -27,6 +27,7 @@ export const transformForWrite = ({
blocks: {}, blocks: {},
blocksToDelete: new Set(), blocksToDelete: new Set(),
locales: {}, locales: {},
texts: [],
numbers: [], numbers: [],
relationships: [], relationships: [],
relationshipsToDelete: [], relationshipsToDelete: [],
@@ -47,6 +48,7 @@ export const transformForWrite = ({
fieldPrefix: '', fieldPrefix: '',
fields, fields,
locales: rowToInsert.locales, locales: rowToInsert.locales,
texts: rowToInsert.texts,
numbers: rowToInsert.numbers, numbers: rowToInsert.numbers,
parentTableName: tableName, parentTableName: tableName,
path, path,

View File

@@ -0,0 +1,15 @@
type Args = {
baseRow: Record<string, unknown>
data: unknown[]
texts: Record<string, unknown>[]
}
export const transformTexts = ({ baseRow, data, texts }: Args) => {
data.forEach((val, i) => {
texts.push({
...baseRow,
text: val,
order: i + 1,
})
})
}

View File

@@ -13,6 +13,7 @@ import { transformBlocks } from './blocks'
import { transformNumbers } from './numbers' import { transformNumbers } from './numbers'
import { transformRelationship } from './relationships' import { transformRelationship } from './relationships'
import { transformSelects } from './selects' import { transformSelects } from './selects'
import { transformTexts } from './texts'
type Args = { type Args = {
adapter: PostgresAdapter adapter: PostgresAdapter
@@ -44,6 +45,7 @@ type Args = {
locales: { locales: {
[locale: string]: Record<string, unknown> [locale: string]: Record<string, unknown>
} }
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[] numbers: Record<string, unknown>[]
/** /**
* This is the name of the parent table * This is the name of the parent table
@@ -71,6 +73,7 @@ export const traverseFields = ({
fields, fields,
forcedLocale, forcedLocale,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path, path,
@@ -108,6 +111,7 @@ export const traverseFields = ({
data: localeData, data: localeData,
field, field,
locale: localeKey, locale: localeKey,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -128,6 +132,7 @@ export const traverseFields = ({
blocksToDelete, blocksToDelete,
data: data[field.name], data: data[field.name],
field, field,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -158,6 +163,7 @@ export const traverseFields = ({
data: localeData, data: localeData,
field, field,
locale: localeKey, locale: localeKey,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -175,6 +181,7 @@ export const traverseFields = ({
blocksToDelete, blocksToDelete,
data: fieldData, data: fieldData,
field, field,
texts,
numbers, numbers,
path, path,
relationships, relationships,
@@ -203,6 +210,7 @@ export const traverseFields = ({
fields: field.fields, fields: field.fields,
forcedLocale: localeKey, forcedLocale: localeKey,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${field.name}.`, path: `${path || ''}${field.name}.`,
@@ -225,6 +233,7 @@ export const traverseFields = ({
fieldPrefix: `${fieldName}_`, fieldPrefix: `${fieldName}_`,
fields: field.fields, fields: field.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${field.name}.`, path: `${path || ''}${field.name}.`,
@@ -258,6 +267,7 @@ export const traverseFields = ({
fields: tab.fields, fields: tab.fields,
forcedLocale: localeKey, forcedLocale: localeKey,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${tab.name}.`, path: `${path || ''}${tab.name}.`,
@@ -280,6 +290,7 @@ export const traverseFields = ({
fieldPrefix: `${fieldPrefix || ''}${tab.name}_`, fieldPrefix: `${fieldPrefix || ''}${tab.name}_`,
fields: tab.fields, fields: tab.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path: `${path || ''}${tab.name}.`, path: `${path || ''}${tab.name}.`,
@@ -303,6 +314,7 @@ export const traverseFields = ({
fieldPrefix, fieldPrefix,
fields: tab.fields, fields: tab.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path, path,
@@ -328,6 +340,7 @@ export const traverseFields = ({
fieldPrefix, fieldPrefix,
fields: field.fields, fields: field.fields,
locales, locales,
texts,
numbers, numbers,
parentTableName, parentTableName,
path, path,
@@ -382,6 +395,37 @@ export const traverseFields = ({
return return
} }
if (field.type === 'text' && field.hasMany) {
const textPath = `${path || ''}${field.name}`
if (field.localized) {
if (typeof fieldData === 'object') {
Object.entries(fieldData).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
transformTexts({
baseRow: {
locale: localeKey,
path: textPath,
},
data: localeData,
texts,
})
}
})
}
} else if (Array.isArray(fieldData)) {
transformTexts({
baseRow: {
path: textPath,
},
data: fieldData,
texts,
})
}
return
}
if (field.type === 'number' && field.hasMany) { if (field.type === 'number' && field.hasMany) {
const numberPath = `${path || ''}${field.name}` const numberPath = `${path || ''}${field.name}`

View File

@@ -34,6 +34,7 @@ export type RowToInsert = {
locales: { locales: {
[locale: string]: Record<string, unknown> [locale: string]: Record<string, unknown>
} }
texts: Record<string, unknown>[]
numbers: Record<string, unknown>[] numbers: Record<string, unknown>[]
relationships: Record<string, unknown>[] relationships: Record<string, unknown>[]
relationshipsToDelete: RelationshipToDelete[] relationshipsToDelete: RelationshipToDelete[]

View File

@@ -56,8 +56,8 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
sessions: { sessions: {
[id: string]: { [id: string]: {
db: DrizzleTransaction db: DrizzleTransaction
reject: () => void reject: () => Promise<void>
resolve: () => void resolve: () => Promise<void>
} }
} }
tables: Record<string, GenericTable> tables: Record<string, GenericTable>
@@ -86,8 +86,8 @@ declare module 'payload' {
sessions: { sessions: {
[id: string]: { [id: string]: {
db: DrizzleTransaction db: DrizzleTransaction
reject: () => void reject: () => Promise<void>
resolve: () => void resolve: () => Promise<void>
} }
} }
tables: Record<string, GenericTable> tables: Record<string, GenericTable>

View File

@@ -68,6 +68,7 @@ export const upsertRow = async <T extends TypeWithID>({
const localesToInsert: Record<string, unknown>[] = [] const localesToInsert: Record<string, unknown>[] = []
const relationsToInsert: Record<string, unknown>[] = [] const relationsToInsert: Record<string, unknown>[] = []
const textsToInsert: Record<string, unknown>[] = []
const numbersToInsert: Record<string, unknown>[] = [] const numbersToInsert: Record<string, unknown>[] = []
const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {} const blocksToInsert: { [blockType: string]: BlockRowToInsert[] } = {}
const selectsToInsert: { [selectTableName: string]: Record<string, unknown>[] } = {} const selectsToInsert: { [selectTableName: string]: Record<string, unknown>[] } = {}
@@ -89,6 +90,14 @@ export const upsertRow = async <T extends TypeWithID>({
}) })
} }
// If there are texts, add parent to each
if (rowToInsert.texts.length > 0) {
rowToInsert.texts.forEach((textRow) => {
textRow.parent = insertedRow.id
textsToInsert.push(textRow)
})
}
// If there are numbers, add parent to each // If there are numbers, add parent to each
if (rowToInsert.numbers.length > 0) { if (rowToInsert.numbers.length > 0) {
rowToInsert.numbers.forEach((numberRow) => { rowToInsert.numbers.forEach((numberRow) => {
@@ -161,6 +170,29 @@ export const upsertRow = async <T extends TypeWithID>({
await db.insert(adapter.tables[relationshipsTableName]).values(relationsToInsert) await db.insert(adapter.tables[relationshipsTableName]).values(relationsToInsert)
} }
// //////////////////////////////////
// INSERT hasMany TEXTS
// //////////////////////////////////
const textsTableName = `${tableName}_texts`
if (operation === 'update') {
await deleteExistingRowsByPath({
adapter,
db,
localeColumnName: 'locale',
parentColumnName: 'parent',
parentID: insertedRow.id,
pathColumnName: 'path',
rows: textsToInsert,
tableName: textsTableName,
})
}
if (textsToInsert.length > 0) {
await db.insert(adapter.tables[textsTableName]).values(textsToInsert).returning()
}
// ////////////////////////////////// // //////////////////////////////////
// INSERT hasMany NUMBERS // INSERT hasMany NUMBERS
// ////////////////////////////////// // //////////////////////////////////

View File

@@ -62,7 +62,7 @@ module.exports = {
'partition-by-comment': true, 'partition-by-comment': true,
groups: ['top', 'unknown'], groups: ['top', 'unknown'],
'custom-groups': { 'custom-groups': {
top: ['_id', 'id', 'name'], top: ['_id', 'id', 'name', 'slug', 'type'],
}, },
}, },
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/eslint-config", "name": "@payloadcms/eslint-config",
"version": "1.0.0", "version": "1.1.0",
"description": "Payload styles for ESLint and Prettier", "description": "Payload styles for ESLint and Prettier",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@payloadcms/live-preview", "name": "@payloadcms/live-preview",
"version": "0.2.1", "version": "0.2.2",
"description": "The official live preview JavaScript SDK for Payload", "description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload", "repository": "https://github.com/payloadcms/payload",
"license": "MIT", "license": "MIT",

View File

@@ -1,6 +1,6 @@
{ {
"name": "payload", "name": "payload",
"version": "2.5.0", "version": "2.7.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework", "description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT", "license": "MIT",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -105,7 +105,7 @@
"minimist": "1.2.8", "minimist": "1.2.8",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.38.0", "monaco-editor": "0.38.0",
"nodemailer": "6.9.4", "nodemailer": "6.9.8",
"object-to-formdata": "4.5.1", "object-to-formdata": "4.5.1",
"passport": "0.6.0", "passport": "0.6.0",
"passport-anonymous": "1.0.1", "passport-anonymous": "1.0.1",
@@ -166,7 +166,7 @@
"@types/minimist": "1.2.2", "@types/minimist": "1.2.2",
"@types/mkdirp": "1.0.2", "@types/mkdirp": "1.0.2",
"@types/node-fetch": "2.6.4", "@types/node-fetch": "2.6.4",
"@types/nodemailer": "6.4.8", "@types/nodemailer": "6.4.14",
"@types/passport": "1.0.12", "@types/passport": "1.0.12",
"@types/passport-anonymous": "1.0.3", "@types/passport-anonymous": "1.0.3",
"@types/passport-jwt": "3.0.9", "@types/passport-jwt": "3.0.9",

View File

@@ -10,6 +10,7 @@ import { filterFields } from '../../forms/RenderFields/filterFields'
import { Gutter } from '../Gutter' import { Gutter } from '../Gutter'
import ViewDescription from '../ViewDescription' import ViewDescription from '../ViewDescription'
import './index.scss' import './index.scss'
import { useOperation } from '../../utilities/OperationProvider'
const baseClass = 'document-fields' const baseClass = 'document-fields'
@@ -34,12 +35,15 @@ export const DocumentFields: React.FC<{
permissions, permissions,
} = props } = props
const operation = useOperation()
const sidebarFields = filterFields({ const sidebarFields = filterFields({
fieldSchema: fields, fieldSchema: fields,
fieldTypes, fieldTypes,
filter: (field) => field?.admin?.position === 'sidebar', filter: (field) => field?.admin?.position === 'sidebar',
permissions: permissions.fields, permissions: permissions.fields,
readOnly: !hasSavePermission, readOnly: !hasSavePermission,
operation,
}) })
const hasSidebarFields = sidebarFields && sidebarFields.length > 0 const hasSidebarFields = sidebarFields && sidebarFields.length > 0

View File

@@ -54,7 +54,10 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
? checkIsActive ? checkIsActive
: location.pathname.startsWith(href) : location.pathname.startsWith(href)
if (!condition || (condition && condition({ collection, config, documentInfo, global }))) { if (
!condition ||
(condition && Boolean(condition({ collection, config, documentInfo, global })))
) {
const labelToRender = typeof label === 'function' ? label({ t }) : label const labelToRender = typeof label === 'function' ? label({ t }) : label
const pillToRender = typeof pillLabel === 'function' ? pillLabel({ versions }) : pillLabel const pillToRender = typeof pillLabel === 'function' ? pillLabel({ versions }) : pillLabel

View File

@@ -25,6 +25,12 @@ $header-height: base(5);
} }
} }
[dir='rtl']
&__actions {
margin-right: auto;
margin-left: unset;
}
&__actions { &__actions {
min-width: 350px; min-width: 350px;
margin-left: auto; margin-left: auto;

View File

@@ -12,6 +12,10 @@
// place the localizer outside the `overflow: hidden` container so that the popup is visible // place the localizer outside the `overflow: hidden` container so that the popup is visible
// this means we need to use a placeholder div so that the space is retained in the DOM // this means we need to use a placeholder div so that the space is retained in the DOM
[dir='rtl'] &__localizer {
right: unset;
left: base(4.5)
}
&__localizer { &__localizer {
position: absolute; position: absolute;
top: 50%; top: 50%;

View File

@@ -133,7 +133,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1 const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
useEffect(() => { useEffect(() => {
const { admin: { listSearchableFields } = {}, slug } = selectedCollectionConfig const { slug, admin: { listSearchableFields } = {} } = selectedCollectionConfig
const params: { const params: {
cacheBust?: number cacheBust?: number
limit?: number limit?: number

View File

@@ -6,7 +6,10 @@
flex-direction: column; flex-direction: column;
text-align: left; text-align: left;
gap: 3px; gap: 3px;
[dir='rtl']
&__text-align--left {
text-align: right;
}
&__text-align--left { &__text-align--left {
text-align: left; text-align: left;
} }
@@ -14,11 +17,15 @@
&__text-align--center { &__text-align--center {
text-align: center; text-align: center;
} }
[dir='rtl']
&__text-align--right {
text-align: left;
}
&__text-align--right { &__text-align--right {
text-align: right; text-align: right;
} }
&__button { &__button {
@extend %btn-reset; @extend %btn-reset;
padding-left: var(--list-button-padding); padding-left: var(--list-button-padding);

View File

@@ -80,13 +80,18 @@
//////////////////////////////// ////////////////////////////////
// HORIZONTAL ALIGNMENT // HORIZONTAL ALIGNMENT
//////////////////////////////// ////////////////////////////////
[dir='rtl']
&--h-align-left {
.popup__caret {
right: var(--popup-padding);
left: unset
}
}
&--h-align-left { &--h-align-left {
.popup__caret { .popup__caret {
left: var(--popup-padding); left: var(--popup-padding);
} }
} }
&--h-align-center { &--h-align-center {
.popup__content { .popup__content {
left: 50%; left: 50%;
@@ -99,6 +104,19 @@
} }
} }
[dir='rtl']
&--h-align-right {
.popup__content {
right: unset;
left:0
}
.popup__caret {
right: unset;
left: var(--popup-padding)
}
}
&--h-align-right { &--h-align-right {
.popup__content { .popup__content {
right: 0; right: 0;

View File

@@ -97,7 +97,7 @@ const RelationshipField: React.FC<Props> = (props) => {
} }
} }
} else { } else {
setErrorLoading(t('errors:unspecific')) setErrorLoading(t('error:unspecific'))
} }
} }
}, Promise.resolve()) }, Promise.resolve())

View File

@@ -47,7 +47,7 @@ export const iterateFields = async ({
if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) { if (!fieldIsPresentationalOnly(field) && !field?.admin?.disabled) {
const passesCondition = Boolean( const passesCondition = Boolean(
(field?.admin?.condition (field?.admin?.condition
? field.admin.condition(fullData || {}, initialData || {}, { user }) ? Boolean(field.admin.condition(fullData || {}, initialData || {}, { user }))
: true) && parentPassesCondition, : true) && parentPassesCondition,
) )

View File

@@ -54,10 +54,10 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
// Besides those who still fail their own conditions // Besides those who still fail their own conditions
if (passesCondition && field.condition) { if (passesCondition && field.condition) {
passesCondition = field.condition( passesCondition = Boolean(
reduceFieldsToValues(state, true), field.condition(reduceFieldsToValues(state, true), getSiblingData(state, path), {
getSiblingData(state, path), user,
{ user }, }),
) )
} }

View File

@@ -13,6 +13,7 @@
& > .field-type { & > .field-type {
margin-bottom: var(--spacing-field); margin-bottom: var(--spacing-field);
max-width: 100%;
&[type='hidden'] { &[type='hidden'] {
margin-bottom: 0; margin-bottom: 0;

View File

@@ -56,12 +56,11 @@ const JSONField: React.FC<Props> = (props) => {
const handleChange = useCallback( const handleChange = useCallback(
(val) => { (val) => {
if (readOnly) return
console.log(val)
setStringValue(val)
try { try {
setValue(JSON.parse(val)) if (readOnly) return
setStringValue(val)
setValue(val ? JSON.parse(val) : '')
setJsonError(undefined) setJsonError(undefined)
} catch (e) { } catch (e) {
setJsonError(e) setJsonError(e)
@@ -71,10 +70,18 @@ const JSONField: React.FC<Props> = (props) => {
) )
useEffect(() => { useEffect(() => {
if (hasLoadedValue) return try {
setStringValue(JSON.stringify(value ? value : initialValue, null, 2)) const hasValue = value && value.toString().length > 0
setHasLoadedValue(true) if (hasLoadedValue) {
}, [initialValue, value]) setStringValue(hasValue ? JSON.stringify(value, null, 2) : '')
} else {
setStringValue(JSON.stringify(hasValue ? value : initialValue, null, 2))
setHasLoadedValue(true)
}
} catch (e) {
setJsonError(e)
}
}, [initialValue, value, hasLoadedValue])
return ( return (
<div <div

View File

@@ -4,9 +4,11 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { TextField } from '../../../../../fields/config/types' import type { TextField } from '../../../../../fields/config/types'
import type { Option } from '../../../elements/ReactSelect/types'
import type { Description } from '../../FieldDescription/types' import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation' import { getTranslation } from '../../../../../utilities/getTranslation'
import ReactSelect from '../../../elements/ReactSelect'
import DefaultError from '../../Error' import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription' import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label' import DefaultLabel from '../../Label'
@@ -21,6 +23,7 @@ export type TextInputProps = Omit<TextField, 'type'> & {
className?: string className?: string
description?: Description description?: Description
errorMessage?: string errorMessage?: string
hasMany?: boolean
inputRef?: React.MutableRefObject<HTMLInputElement> inputRef?: React.MutableRefObject<HTMLInputElement>
onChange?: (e: ChangeEvent<HTMLInputElement>) => void onChange?: (e: ChangeEvent<HTMLInputElement>) => void
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement> onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
@@ -32,6 +35,7 @@ export type TextInputProps = Omit<TextField, 'type'> & {
showError?: boolean showError?: boolean
style?: React.CSSProperties style?: React.CSSProperties
value?: string value?: string
valueToRender?: Option[]
width?: string width?: string
} }
@@ -44,8 +48,11 @@ const TextInput: React.FC<TextInputProps> = (props) => {
className, className,
description, description,
errorMessage, errorMessage,
hasMany,
inputRef, inputRef,
label, label,
maxRows,
minRows,
onChange, onChange,
onKeyDown, onKeyDown,
path, path,
@@ -56,17 +63,25 @@ const TextInput: React.FC<TextInputProps> = (props) => {
showError, showError,
style, style,
value, value,
valueToRender,
width, width,
} = props } = props
const { i18n } = useTranslation() const { i18n, t } = useTranslation()
const ErrorComp = Error || DefaultError const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel const LabelComp = Label || DefaultLabel
return ( return (
<div <div
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only'] className={[
fieldBaseClass,
'text',
className,
showError && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
style={{ style={{
@@ -78,18 +93,45 @@ const TextInput: React.FC<TextInputProps> = (props) => {
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} /> <LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper"> <div className="input-wrapper">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<input {hasMany ? (
data-rtl={rtl} <ReactSelect
disabled={readOnly} className={`field-${path.replace(/\./g, '__')}`}
id={`field-${path.replace(/\./g, '__')}`} disabled={readOnly}
name={path} filterOption={(option, rawInput) => {
onChange={onChange} const isOverHasMany = Array.isArray(value) && value.length >= maxRows
onKeyDown={onKeyDown} return !isOverHasMany
placeholder={getTranslation(placeholder, i18n)} }}
ref={inputRef} isClearable
type="text" isCreatable
value={value || ''} isMulti
/> isSortable
noOptionsMessage={({ inputValue }) => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return t('general:noOptions')
}}
onChange={onChange}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender}
/>
) : (
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
)}
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div> </div>
<FieldDescription <FieldDescription

View File

@@ -3,8 +3,10 @@
.field-type.text { .field-type.text {
position: relative; position: relative;
input { &:not(.has-many) {
@include formInput; input {
@include formInput;
}
} }
} }

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import type { Props } from './types' import type { Props } from './types'
@@ -24,11 +24,14 @@ const Text: React.FC<Props> = (props) => {
style, style,
width, width,
} = {}, } = {},
hasMany,
inputRef, inputRef,
label, label,
localized, localized,
maxLength, maxLength,
maxRows,
minLength, minLength,
minRows,
path: pathFromProps, path: pathFromProps,
required, required,
validate = text, validate = text,
@@ -58,6 +61,50 @@ const Text: React.FC<Props> = (props) => {
validate: memoizedValidate, validate: memoizedValidate,
}) })
const handleOnChange = (e) => {
setValue(e.target.value)
}
const handleHasManyChange = useCallback(
(selectedOption) => {
if (!readOnly) {
let newValue
if (!selectedOption) {
newValue = []
} else if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => option.value?.value || option.value)
} else {
newValue = [selectedOption.value?.value || selectedOption.value]
}
setValue(newValue)
}
},
[readOnly, setValue],
)
const [valueToRender, setValueToRender] = useState<
{ id: string; label: string; value: { value: string } }[]
>([]) // Only for hasMany
// useeffect update valueToRender:
useEffect(() => {
if (hasMany && Array.isArray(value)) {
setValueToRender(
value.map((val, index) => {
return {
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
label: `${val}`,
value: {
toString: () => `${val}${index}`,
value: val?.value || val,
}, // You're probably wondering, why the hell is this done that way? Well, React-select automatically uses "label-value" as a key, so we will get that react duplicate key warning if we just pass in the value as multiple values can be the same. So we need to append the index to the toString() of the value to avoid that warning, as it uses that as the key.
}
}),
)
}
}, [value, hasMany])
return ( return (
<TextInput <TextInput
Error={Error} Error={Error}
@@ -67,12 +114,13 @@ const Text: React.FC<Props> = (props) => {
className={className} className={className}
description={description} description={description}
errorMessage={errorMessage} errorMessage={errorMessage}
hasMany={hasMany}
inputRef={inputRef} inputRef={inputRef}
label={label} label={label}
maxRows={maxRows}
minRows={minRows}
name={name} name={name}
onChange={(e) => { onChange={hasMany ? handleHasManyChange : handleOnChange}
setValue(e.target.value)
}}
path={path} path={path}
placeholder={placeholder} placeholder={placeholder}
readOnly={readOnly} readOnly={readOnly}
@@ -81,6 +129,7 @@ const Text: React.FC<Props> = (props) => {
showError={showError} showError={showError}
style={style} style={style}
value={value} value={value}
valueToRender={valueToRender}
width={width} width={width}
/> />
) )

View File

@@ -36,7 +36,7 @@ export const WatchCondition: React.FC<Props> = ({
data.id = id data.id = id
const hasCondition = Boolean(condition) const hasCondition = Boolean(condition)
const isPassingCondition = hasCondition ? condition(data, siblingData, { user }) : true const isPassingCondition = hasCondition ? Boolean(condition(data, siblingData, { user })) : true
const field = fields[path] const field = fields[path]
const wasPassingCondition = field?.passesCondition const wasPassingCondition = field?.passesCondition

View File

@@ -17,9 +17,16 @@ const Component: React.FC<{
onCancel: () => void onCancel: () => void
onConfirm: () => void onConfirm: () => void
}> = ({ isActive, onCancel, onConfirm }) => { }> = ({ isActive, onCancel, onConfirm }) => {
const { closeModal, openModal } = useModal() const { closeModal, openModal, modalState } = useModal()
const { t } = useTranslation('general') const { t } = useTranslation('general')
// Manually check for modal state as 'esc' key will not trigger the nav inactivity
useEffect(() => {
if (!modalState?.[modalSlug]?.isOpen && isActive) {
onCancel()
}
}, [modalState])
useEffect(() => { useEffect(() => {
if (isActive) openModal(modalSlug) if (isActive) openModal(modalSlug)
else closeModal(modalSlug) else closeModal(modalSlug)

View File

@@ -21,6 +21,11 @@
} }
} }
} }
[dir='rtl']
&__nav-toggler-wrapper {
left: unset;
right: 0;
}
&__nav-toggler-wrapper { &__nav-toggler-wrapper {
position: fixed; position: fixed;

View File

@@ -219,6 +219,10 @@ export const API: React.FC<EditViewProps> = (props) => {
editConfig && 'API' in editConfig && 'actions' in editConfig.API ? editConfig.API.actions : [] editConfig && 'API' in editConfig && 'actions' in editConfig.API ? editConfig.API.actions : []
setViewActions(apiActions) setViewActions(apiActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions]) }, [collection, global, setViewActions])
const localeOptions = const localeOptions =

View File

@@ -25,23 +25,27 @@ const AccountView: React.FC = () => {
const { user } = useAuth() const { user } = useAuth()
const userRef = useRef(user) const userRef = useRef(user)
const [internalState, setInternalState] = useState<Fields>() const [internalState, setInternalState] = useState<Fields>()
const { id, docPermissions, getDocPermissions, getDocPreferences, preferencesKey, slug } = const {
useDocumentInfo() id,
slug,
collection,
docPermissions,
getDocPermissions,
getDocPreferences,
preferencesKey,
} = useDocumentInfo()
const { getPreference } = usePreferences() const { getPreference } = usePreferences()
const config = useConfig() const config = useConfig()
const { const {
admin: { components: { views: { Account: CustomAccountComponent } = {} } = {} }, admin: { components: { views: { Account: CustomAccountComponent } = {} } = {} },
collections,
routes: { api }, routes: { api },
serverURL, serverURL,
} = useConfig() } = useConfig()
const { t } = useTranslation('authentication') const { t } = useTranslation('authentication')
const collection = collections.find((coll) => coll.slug === slug)
const { fields } = collection || {} const { fields } = collection || {}
const [{ data, isLoading: isLoadingData }] = usePayloadAPI(`${serverURL}${api}/${slug}/${id}`, { const [{ data, isLoading: isLoadingData }] = usePayloadAPI(`${serverURL}${api}/${slug}/${id}`, {

View File

@@ -58,6 +58,10 @@ const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
: [] : []
setViewActions(defaultActions) setViewActions(defaultActions)
return () => {
setViewActions([])
}
}, [global.slug, location.pathname, global?.admin?.components?.views?.Edit, setViewActions]) }, [global.slug, location.pathname, global?.admin?.components?.views?.Edit, setViewActions])
return ( return (

View File

@@ -40,7 +40,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const { reportUpdate } = useDocumentEvents() const { reportUpdate } = useDocumentEvents()
const { admin: { components: { views: { Edit: Edit } = {} } = {} } = {}, fields, slug } = global const { slug, admin: { components: { views: { Edit: Edit } = {} } = {} } = {}, fields } = global
const onSave = useCallback( const onSave = useCallback(
async (json) => { async (json) => {

View File

@@ -193,6 +193,10 @@ export const LivePreviewView: React.FC<
: [] : []
setViewActions(livePreviewActions) setViewActions(livePreviewActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions]) }, [collection, global, setViewActions])
const breakpoints: LivePreviewConfig['breakpoints'] = [ const breakpoints: LivePreviewConfig['breakpoints'] = [

View File

@@ -184,6 +184,10 @@ const VersionView: React.FC<Props> = ({ collection, global }) => {
: [] : []
setViewActions(versionActions) setViewActions(versionActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions]) }, [collection, global, setViewActions])
let metaTitle: string let metaTitle: string

View File

@@ -136,6 +136,10 @@ const VersionsView: React.FC<IndexProps> = (props) => {
: [] : []
setViewActions(versionsActions) setViewActions(versionsActions)
return () => {
setViewActions([])
}
}, [collection, global, setViewActions]) }, [collection, global, setViewActions])
return ( return (

View File

@@ -91,6 +91,10 @@ const DefaultEditView: React.FC<DefaultEditViewProps> = (props) => {
: [] : []
setViewActions(defaultActions) setViewActions(defaultActions)
return () => {
setViewActions([])
}
}, [id, location.pathname, collection?.admin?.components?.views?.Edit, setViewActions]) }, [id, location.pathname, collection?.admin?.components?.views?.Edit, setViewActions])
return ( return (

View File

@@ -5,7 +5,7 @@ import { fieldAffectsData } from '../../../../../fields/config/types'
const formatFields = (collection: SanitizedCollectionConfig, isEditing?: boolean): Field[] => const formatFields = (collection: SanitizedCollectionConfig, isEditing?: boolean): Field[] =>
isEditing isEditing
? collection.fields.filter((field) => (fieldAffectsData(field) && field.name !== 'id') || true) ? collection.fields.filter((field) => !fieldAffectsData(field) || field.name !== 'id')
: collection.fields : collection.fields
export default formatFields export default formatFields

View File

@@ -26,7 +26,7 @@ import formatFields from './formatFields'
const EditView: React.FC<IndexProps> = (props) => { const EditView: React.FC<IndexProps> = (props) => {
const { collection: incomingCollection, isEditing } = props const { collection: incomingCollection, isEditing } = props
const { admin: { components: { views: { Edit } = {} } = {} } = {}, slug: collectionSlug } = const { slug: collectionSlug, admin: { components: { views: { Edit } = {} } = {} } = {} } =
incomingCollection incomingCollection
const [fields] = useState(() => formatFields(incomingCollection, isEditing)) const [fields] = useState(() => formatFields(incomingCollection, isEditing))

View File

@@ -83,6 +83,10 @@ const ListView: React.FC<ListIndexProps> = (props) => {
if (CustomList && typeof CustomList === 'object' && 'actions' in CustomList) { if (CustomList && typeof CustomList === 'object' && 'actions' in CustomList) {
setViewActions(CustomList.actions || []) setViewActions(CustomList.actions || [])
} }
return () => {
setViewActions([])
}
}, [CustomList, setViewActions]) }, [CustomList, setViewActions])
useEffect(() => { useEffect(() => {

View File

@@ -40,8 +40,14 @@ export const formatUseAsTitle = (args: {
} }
const field = fieldFromProps || getObjectDotNotation<FormField>(doc, collection.admin.useAsTitle) const field = fieldFromProps || getObjectDotNotation<FormField>(doc, collection.admin.useAsTitle)
let title: string
let title = typeof field === 'string' ? field : (field?.value as string) if (typeof field === 'string') {
title = field
} else if (typeof field === 'number') {
title = String(field)
} else {
title = field?.value as string
}
const fieldConfig = collection?.fields?.find((f) => 'name' in f && f?.name === useAsTitle) const fieldConfig = collection?.fields?.find((f) => 'name' in f && f?.name === useAsTitle)
const isDate = fieldConfig?.type === 'date' const isDate = fieldConfig?.type === 'date'

View File

@@ -1,7 +1,8 @@
import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload' import type { Payload } from '../../../payload'
import formatName from '../../../graphql/utilities/formatName' import formatName from '../../../graphql/utilities/formatName'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import access from '../../operations/access' import access from '../../operations/access'
const formatConfigNames = (results, configs) => { const formatConfigNames = (results, configs) => {
@@ -19,7 +20,7 @@ const formatConfigNames = (results, configs) => {
function accessResolver(payload: Payload) { function accessResolver(payload: Payload) {
async function resolver(_, args, context) { async function resolver(_, args, context) {
const options = { const options = {
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
} }
const accessResults = await access(options) const accessResults = await access(options)

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import forgotPassword from '../../operations/forgotPassword' import forgotPassword from '../../operations/forgotPassword'
function forgotPasswordResolver(collection: Collection): any { function forgotPasswordResolver(collection: Collection): any {
@@ -12,7 +13,7 @@ function forgotPasswordResolver(collection: Collection): any {
}, },
disableEmail: args.disableEmail, disableEmail: args.disableEmail,
expiration: args.expiration, expiration: args.expiration,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
} }
await forgotPassword(options) await forgotPassword(options)

View File

@@ -1,11 +1,13 @@
import isolateTransactionID from '../../../utilities/isolateTransactionID' import type { PayloadRequest } from '../../../express/types'
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import init from '../../operations/init' import init from '../../operations/init'
function initResolver(collection: string) { function initResolver(collection: string) {
async function resolver(_, args, context) { async function resolver(_, args, context) {
const options = { const options = {
collection, collection,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
} }
return init(options) return init(options)

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import login from '../../operations/login' import login from '../../operations/login'
function loginResolver(collection: Collection) { function loginResolver(collection: Collection) {
@@ -12,7 +13,7 @@ function loginResolver(collection: Collection) {
password: args.password, password: args.password,
}, },
depth: 0, depth: 0,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
res: context.res, res: context.res,
} }

View File

@@ -1,13 +1,14 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import logout from '../../operations/logout' import logout from '../../operations/logout'
function logoutResolver(collection: Collection): any { function logoutResolver(collection: Collection): any {
async function resolver(_, args, context) { async function resolver(_, args, context) {
const options = { const options = {
collection, collection,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
res: context.res, res: context.res,
} }

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import me from '../../operations/me' import me from '../../operations/me'
function meResolver(collection: Collection): any { function meResolver(collection: Collection): any {
@@ -8,7 +9,7 @@ function meResolver(collection: Collection): any {
const options = { const options = {
collection, collection,
depth: 0, depth: 0,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
} }
return me(options) return me(options)
} }

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import getExtractJWT from '../../getExtractJWT' import getExtractJWT from '../../getExtractJWT'
import refresh from '../../operations/refresh' import refresh from '../../operations/refresh'
@@ -18,7 +19,7 @@ function refreshResolver(collection: Collection) {
const options = { const options = {
collection, collection,
depth: 0, depth: 0,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
res: context.res, res: context.res,
token, token,
} }

View File

@@ -1,20 +1,24 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import resetPassword from '../../operations/resetPassword' import resetPassword from '../../operations/resetPassword'
function resetPasswordResolver(collection: Collection) { function resetPasswordResolver(collection: Collection) {
async function resolver(_, args, context) { async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale let { req } = context
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale req = isolateObjectProperty(req, 'locale')
req = isolateObjectProperty(req, 'fallbackLocale')
if (args.locale) req.locale = args.locale
if (args.fallbackLocale) req.fallbackLocale = args.fallbackLocale
const options = { const options = {
api: 'GraphQL', api: 'GraphQL',
collection, collection,
data: args, data: args,
depth: 0, depth: 0,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(req, 'transactionID'),
res: context.res, res: context.res,
} }

View File

@@ -1,6 +1,7 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import unlock from '../../operations/unlock' import unlock from '../../operations/unlock'
function unlockResolver(collection: Collection) { function unlockResolver(collection: Collection) {
@@ -8,7 +9,7 @@ function unlockResolver(collection: Collection) {
const options = { const options = {
collection, collection,
data: { email: args.email }, data: { email: args.email },
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
} }
const result = await unlock(options) const result = await unlock(options)

View File

@@ -1,18 +1,22 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import isolateTransactionID from '../../../utilities/isolateTransactionID' import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
import verifyEmail from '../../operations/verifyEmail' import verifyEmail from '../../operations/verifyEmail'
function verifyEmailResolver(collection: Collection) { function verifyEmailResolver(collection: Collection) {
async function resolver(_, args, context) { async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale let { req } = context
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale req = isolateObjectProperty(req, 'locale')
req = isolateObjectProperty(req, 'fallbackLocale')
if (args.locale) req.locale = args.locale
if (args.fallbackLocale) req.fallbackLocale = args.fallbackLocale
const options = { const options = {
api: 'GraphQL', api: 'GraphQL',
collection, collection,
req: isolateTransactionID(context.req), req: isolateObjectProperty<PayloadRequest>(req, 'transactionID'),
res: context.res, res: context.res,
token: args.token, token: args.token,
} }

View File

@@ -36,8 +36,8 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
context, context,
data, data,
depth, depth,
fallbackLocale, fallbackLocale: fallbackLocaleArg = options?.req?.fallbackLocale,
locale, locale: localeArg = null,
overrideAccess = true, overrideAccess = true,
req = {} as PayloadRequest, req = {} as PayloadRequest,
res, res,
@@ -46,6 +46,12 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
setRequestContext(req, context) setRequestContext(req, context)
const collection = payload.collections[collectionSlug] const collection = payload.collections[collectionSlug]
const localizationConfig = payload?.config?.localization
const defaultLocale = localizationConfig ? localizationConfig.defaultLocale : null
const locale = localeArg || req?.locale || defaultLocale
const fallbackLocale = localizationConfig
? localizationConfig.locales.find(({ code }) => locale === code)?.fallbackLocale
: null
if (!collection) { if (!collection) {
throw new APIError( throw new APIError(
@@ -56,8 +62,6 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
req.payloadAPI = req.payloadAPI || 'local' req.payloadAPI = req.payloadAPI || 'local'
req.payload = payload req.payload = payload
req.i18n = i18nInit(payload.config.i18n) req.i18n = i18nInit(payload.config.i18n)
req.locale = undefined
req.fallbackLocale = undefined
if (!req.t) req.t = req.i18n.t if (!req.t) req.t = req.i18n.t
if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req) if (!req.payloadDataLoader) req.payloadDataLoader = getDataLoader(req)
@@ -73,7 +77,10 @@ async function localLogin<TSlug extends keyof GeneratedTypes['collections']>(
} }
if (locale) args.req.locale = locale if (locale) args.req.locale = locale
if (fallbackLocale) args.req.fallbackLocale = fallbackLocale if (fallbackLocale) {
args.req.fallbackLocale =
typeof fallbackLocaleArg !== 'undefined' ? fallbackLocaleArg : fallbackLocale || defaultLocale
}
return login<TSlug>(args) return login<TSlug>(args)
} }

View File

@@ -20,7 +20,7 @@ export const incrementLoginAttempts = async ({
} = collection } = collection
if ('lockUntil' in doc && typeof doc.lockUntil === 'string') { if ('lockUntil' in doc && typeof doc.lockUntil === 'string') {
const lockUntil = Math.floor(new Date(doc.lockUntil).getTime() / 1000) const lockUntil = new Date(doc.lockUntil).getTime()
// Expired lock, restart count at 1 // Expired lock, restart count at 1
if (lockUntil < Date.now()) { if (lockUntil < Date.now()) {

Some files were not shown because too many files have changed in this diff Show More