Compare commits

..

191 Commits

Author SHA1 Message Date
Elliot DeNolf
71f19fba58 chore(release): v3.0.0-beta.15 [skip ci] 2024-04-24 13:41:28 -04:00
Paul
24b18fb0fd feat!: removed getDataAndFile and getLocales from createPayloadRequest in favour of new utilities addDataAndFileToRequest and addLocalesToRequest (#5999) 2024-04-24 13:31:54 -03:00
Elliot DeNolf
5731241a5c fix(db-postgres): postgres uuid (#6003)
Co-authored-by: James <james@trbl.design>
2024-04-24 11:59:39 -04:00
Dan Ribbens
47e70abb4e fix: type collection config missing dbName (#5983) 2024-04-24 11:32:59 -04:00
Paul
0ede95f375 fix: issues creating the first user (#5986)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-24 11:30:52 -04:00
Jarrod Flesch
b723efdd3b chore: fixing flakey tests (#5984) 2024-04-24 00:44:43 -04:00
Elliot DeNolf
14c513690d ci: lint pr titles (#5988) 2024-04-23 23:40:55 -04:00
Alessio Gravili
88f239e784 feat(richtext-lexical)!: rework population behavior and allow richText adapter field hooks (#5893)
BREAKING:

- Unpopulated lexical relationship, link and upload nodes now save the relationTo document ID under value instead of value.id. This matches the behavior of core relationship fields. This changes the shape of the saved JSON data
- Any custom features which add their own population promises need to be reworked. populationPromises no longer accepts the promises as a return value. Instead, it expects you to mutate the promises array which is passed through, which mimics the way it works in core
2024-04-23 20:43:07 -04:00
Alessio Gravili
a1f6bf8a67 fix(richtext-lexical): Heading feature: enabledHeadingSizes not being applied 2024-04-23 20:37:11 -04:00
Alessio Gravili
912dcd38df fix(richtext-lexical): add missing HorizontalRuleFeature export 2024-04-23 20:25:12 -04:00
Alessio Gravili
da5028cdee feat(richtext-lexical): show loading indicator while block nodes are loading 2024-04-23 20:22:18 -04:00
Elliot DeNolf
899faa62f1 chore: update pnpm-lock 2024-04-23 17:01:57 -04:00
Alessio Gravili
9df6a644c9 chore: update lockfile 2024-04-23 16:37:02 -04:00
Alessio Gravili
1a6d9eaa11 Merge remote-tracking branch 'origin/beta' into fix/lexical-localization 2024-04-23 16:33:54 -04:00
Alessio Gravili
7d447af277 chore: add remaining missing preferences prop to validations 2024-04-23 15:46:01 -04:00
Elliot DeNolf
d8baaab849 chore(release): v3.0.0-beta.14 [skip ci] 2024-04-23 15:29:35 -04:00
Jarrod Flesch
3e1523f007 fix: move graphql-http from devDep to dep in next package (#5982)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-04-23 15:27:43 -04:00
Alessio Gravili
fa38af025f Merge branch 'beta' into fix/lexical-localization 2024-04-23 15:20:56 -04:00
Elliot DeNolf
6ca9ff847f chore(release): v3.0.0-beta.13 [skip ci] 2024-04-23 15:15:21 -04:00
Alessio Gravili
a8824b2b51 fix: incorrect value for empty preferences passed into buildStateFromSchema 2024-04-23 15:12:55 -04:00
Alessio Gravili
6aa3752b16 feat(richtext-lexical): allow richtext adapters to hook into field hooks 2024-04-23 15:10:35 -04:00
Elliot DeNolf
c483a439bf build: adjust pnpm engines version 2024-04-23 15:01:00 -04:00
Jarrod Flesch
74bdf1c681 chore: reduces graphql dependencies (#5979)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-04-23 15:00:09 -04:00
James Mikrut
7437d9fe58 Fix/postgres relation names (#5976) 2024-04-23 14:56:43 -04:00
Elliot DeNolf
51f7351962 fix(cpa): install db adapter in package.json (#5921) 2024-04-23 14:03:01 -04:00
Elliot DeNolf
d01fcb921b chore: tsconfig.json back to default 2024-04-23 13:18:45 -04:00
Elliot DeNolf
6179c938bf ci: remove email e2e tests, ethereal calls failing 2024-04-23 13:18:10 -04:00
Elliot DeNolf
dbbcb658a9 fix(deps): proper deps for storage-s3 and storage-vercel-blob (#5975) 2024-04-23 13:17:00 -04:00
James
16f97ad7c3 chore: disables forced pg for tests 2024-04-23 13:13:59 -04:00
James
bc7445ed99 Merge branch 'beta' of github.com:payloadcms/payload into fix/postgres-relation-names 2024-04-23 12:43:32 -04:00
James
e4d024cd0d chore: properly destroys db in postgres 2024-04-23 12:43:25 -04:00
James
1005de8295 fix(db-postgres): shortens relation names 2024-04-23 12:14:01 -04:00
Elliot DeNolf
c79289cedf chore(release): v3.0.0-beta.12 [skip ci] (#5972) 2024-04-23 11:02:08 -04:00
Jarrod Flesch
6a745be036 chore: pass mock req through with validate function to slate richText validation function (#5971) 2024-04-23 10:57:36 -04:00
Elliot DeNolf
cee9cc33ed chore(release): v3.0.0-beta.12 [skip ci] 2024-04-23 10:55:51 -04:00
Elliot DeNolf
9a5e9313cd ci: remove warning for no artifacts found 2024-04-23 10:47:17 -04:00
Elliot DeNolf
5401af5812 chore(storage-*): set disableLocalStorage true for enabled collections (#5970) 2024-04-23 10:46:33 -04:00
Elliot DeNolf
6305a1d1c2 chore: remove NodemailerAdapter type imports 2024-04-23 10:09:35 -04:00
Jarrod Flesch
95b96e3e9e chore: adjust headersWithCors for req without payload (#5963) 2024-04-23 09:50:41 -04:00
Elliot DeNolf
95b3f6d40d chore(scripts): add new packages to getPackageRegistryVersions 2024-04-23 09:10:25 -04:00
Elliot DeNolf
c258a4bef1 chore(scripts): add throttling to release script, optional git commit arg 2024-04-23 09:09:55 -04:00
Elliot DeNolf
647544a0c6 chore: fix build:tests filter [skip ci] 2024-04-23 08:46:15 -04:00
Elliot DeNolf
7e0a2a879c chore: adjust nodemailer type export 2024-04-23 08:39:32 -04:00
Elliot DeNolf
471e1388ae ci: bump pnpm version in gh action, use variable 2024-04-22 22:29:07 -04:00
Elliot DeNolf
1da430b042 ci: bump pnpm version 2024-04-22 22:01:56 -04:00
Elliot DeNolf
56ac06c563 fix: disallow importing from ts extensions 2024-04-22 21:15:13 -04:00
Elliot DeNolf
4dec4bb61c fix: resave media using cloud storage plugin (#5959) 2024-04-22 19:58:57 -04:00
Elliot DeNolf
99a09c49a3 ci: start docker for plugin-cloud-storage e2e 2024-04-22 16:59:57 -04:00
James Mikrut
88fd46bfea fix(db-postgres): row table names were not being built properly (#5960) 2024-04-22 16:55:12 -04:00
Elliot DeNolf
8a6603b3d8 test: add plugin-cloud-storage e2e 2024-04-22 16:43:54 -04:00
PatrikKozak
f6c9f454a5 Merge branch 'beta' of https://github.com/payloadcms/payload into fix/row-table-names 2024-04-22 16:18:24 -04:00
PatrikKozak
d8a5426c37 chore: adds array within row in tabsDoc data 2024-04-22 16:18:14 -04:00
Elliot DeNolf
c9011dcbfd fix(plugin-cloud-storage): resave media 2024-04-22 16:11:19 -04:00
Jarrod Flesch
43089fd13c chore: adds cors headers to routeErrors (#5957) 2024-04-22 15:48:42 -04:00
Elliot DeNolf
bb3bd9c395 chore: adjust email adapter messaging 2024-04-22 15:42:21 -04:00
James
ba423ab424 fix: row table names were not being built properly 2024-04-22 15:10:59 -04:00
Elliot DeNolf
c23984cac3 feat(plugin-cloud-storage): implement storage packages (#5928) 2024-04-22 14:31:20 -04:00
Elliot DeNolf
6685a0fa7e feat!: email adapter (#5901) 2024-04-22 14:26:12 -04:00
Jarrod Flesch
ac4750d016 chore: adds fallbackFileType functionality (#5958) 2024-04-22 14:20:02 -04:00
Elliot DeNolf
951e9fd7f2 test: email e2e updated nodemailer usage 2024-04-22 14:13:38 -04:00
Elliot DeNolf
cbd1554589 chore: adjust email pattern 2024-04-22 13:32:33 -04:00
Jacob Fletcher
80c545933f fix(next): adds CORS headers to API Responses (#5906)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-22 12:13:06 -04:00
Paul
594f319fc6 chore!: admin now takes a client side custom property and custom is server only (#5926) 2024-04-22 12:22:32 -03:00
Elliot DeNolf
102feb9576 chore: updates telemetry (#5883) 2024-04-22 09:44:34 -04:00
Simon Vreman
68274d2862 fix(plugin-cloud-storage)!: Pass filename to utility function getting file prefix (#5934) 2024-04-21 07:32:11 -04:00
Dan Ribbens
8945b7a4fa fix(db-postgres): nested groups in nested blocks validation (#5941)
Co-authored-by: Ricardo Domingues <rfdomingues98@gmail.com>
2024-04-21 00:38:55 -04:00
Dan Ribbens
d5ef93b2ba fix(db-postgres): v3 #5938 extra version suffix table names (#5940) 2024-04-20 23:23:06 -04:00
Ritsu
cb0f0dba3a chore: removes comment and unused type import (#5935) 2024-04-20 23:06:43 -04:00
Paul
7b263be01b chore: add missing translations (#5929) 2024-04-20 14:57:22 -04:00
Dan Ribbens
56df60f520 chore: fixes e2e test running on windows (#5927) 2024-04-20 14:54:18 -04:00
Ritsu
d5cbbc472d feat: add count operation to collections (#5930) 2024-04-20 14:45:44 -04:00
Dan Ribbens
d987e5628a feat(live-preview-vue): new live-preview-vue package (#5933)
Co-authored-by: Christian Gil <mrcgam.christian@gmail.com>
2024-04-20 07:52:00 -04:00
Dan Ribbens
1383191f15 fix: v3 update many with drafts (#5900)
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
2024-04-19 16:32:59 -04:00
Ritsu
27297284cf fix: Passes correct path to import modules on Windows started with file:// (#5919) 2024-04-19 16:28:41 -04:00
Kendell Joseph
3af3a91c87 feat: json field schemas (#5898) 2024-04-19 13:35:59 -04:00
Paul
23c5b71f95 chore(payload,ui)!:update custom config to separate client and server bundles (#5914) 2024-04-19 11:52:55 -03:00
Dan Ribbens
2ee6a8ec3a fix(db-mongodb): ignore end session errors (#5905) 2024-04-19 09:19:55 -04:00
Elliot DeNolf
10819b8693 chore: proper SendMailOptions export 2024-04-18 15:43:10 -04:00
Elliot DeNolf
83c617b452 test: clean up email-nodemailer config 2024-04-18 14:17:36 -04:00
Elliot DeNolf
4acb133655 chore: export SendMailOptions 2024-04-18 14:15:55 -04:00
Elliot DeNolf
6e4135e790 test: add nodemailer adapter to email test config 2024-04-18 13:36:48 -04:00
Elliot DeNolf
f0198b62f3 feat: implement stdout email adapter, use if no adapter configured 2024-04-18 11:59:03 -04:00
Jessica Chowdhury
3ff8063ab8 chore:(i18n): adds translation for document/s key (#5890) 2024-04-18 10:18:06 +01:00
Elliot DeNolf
8d52f1b279 chore: add payload to dev deps 2024-04-18 02:27:43 -04:00
Elliot DeNolf
24072d222c chore: clean up types, remove logMockEmailCredentials 2024-04-18 02:07:54 -04:00
Elliot DeNolf
55c59e71da chore: remove nodemailer from payload completely 2024-04-18 01:44:35 -04:00
Elliot DeNolf
62233788e0 feat(plugin-cloud): use nodemailer adapter 2024-04-18 01:44:20 -04:00
Elliot DeNolf
b297c5499d chore(email): strict true 2024-04-18 00:02:05 -04:00
Elliot DeNolf
fb7925f272 feat: create email-nodemailer package 2024-04-17 21:58:24 -04:00
Patrik
221e873862 chore(translations): adds localsNotSaved_one & localsNotSaved_other translations (#5903) 2024-04-17 16:34:10 -04:00
Patrik
e7143e02e2 fix: adds type error validations for email and password in login operation (#5899) 2024-04-17 16:33:19 -04:00
Elliot DeNolf
a1d68bd951 feat: abstract nodemailer into email adapter interface 2024-04-17 16:10:51 -04:00
Jarrod Flesch
93ee452a2d fix(next): do not require handlers, attempt to read filesystem or throw (#5896) 2024-04-17 15:12:57 -04:00
Jarrod Flesch
1abaa5fc17 chore(next): bump next@^14.3.0-canary.7 (#5894) 2024-04-17 13:07:40 -04:00
Alessio Gravili
999059bc61 fix(richtext-lexical): properly validate block node nested fields, fixes one failing e2e test suite we previously skipped 2024-04-17 11:47:24 -04:00
Alessio Gravili
39ba39c237 feat(richtext-lexical)!: rework how population works and saves data, improve node typing 2024-04-17 11:46:47 -04:00
Jarrod Flesch
009e6c2066 chore(test): fix flakey relationship tests (#5892) 2024-04-17 11:44:07 -04:00
Ritsu
8bf03ae706 fix(next): pass a corrent content-type header in getFile route (#5799)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2024-04-17 11:40:02 -04:00
Kendell Joseph
a2fe3f66e3 fix: accepts empty cell data in json field (#5876) 2024-04-17 11:07:28 -04:00
Jacob Fletcher
6cd5b253f1 fix(next): admin access control (#5887) 2024-04-17 10:31:39 -04:00
Elliot DeNolf
abf0461d80 ci: add exports pattern to codeowners 2024-04-17 10:24:02 -04:00
Elliot DeNolf
abca45e152 chore: add comments to exports about server vs. front-end 2024-04-17 10:23:21 -04:00
Alessio Gravili
58ea94f6ac feat: pass through doc preferences to field validate function and improve types 2024-04-17 09:27:04 -04:00
Jessica Chowdhury
49cba92fa1 fix(create-payload-app): uses baseUrl for payload config path in tsconfig (#5888) 2024-04-17 13:53:00 +01:00
Elliot DeNolf
42329fc736 ci: cut down on codeowners noise [skip ci] 2024-04-16 22:07:55 -04:00
Elliot DeNolf
68dee49501 feat(cpa): list plugin template after updating for 3.0 2024-04-16 19:46:27 -04:00
Dan Ribbens
234837ee1d fix: postgres query hasMany in (#5884) 2024-04-16 17:09:43 -04:00
Kendell Joseph
0d3554d70a fix: accepts empty cell data 2024-04-16 12:35:03 -04:00
Paul
7f6c6c4787 fix(next): check for matching passwords when creating the first user (#5869) 2024-04-16 12:41:20 -03:00
Jacob Fletcher
b6c975bfdc Revert "fix(plugin-seo): uses correct key for ukrainian translation"
This reverts commit b9a9dad60a.
2024-04-16 11:36:38 -04:00
Elliot DeNolf
eaf5a86121 ci: enforce node version for all jobs 2024-04-16 11:35:00 -04:00
Jacob Fletcher
b9a9dad60a fix(plugin-seo): uses correct key for ukrainian translation 2024-04-16 11:14:44 -04:00
Alessio Gravili
a2afc38894 fix(richtext-lexical): do not allow empty url field in link drawer 2024-04-16 11:03:12 -04:00
Paul
b80c92ba93 fix(next): issue with password and confirm password fields not being type of password (#5870) 2024-04-16 11:52:56 -03:00
Patrik
6669a2cedb fix(next): adds client-side field validations to login and forgot-password views (#5871) 2024-04-16 10:36:37 -04:00
Bohdan Kucheriavyi
7369da3d8d chore(plugin-seo): adds Ukrainian translations (#5836)
Signed-off-by: Bohdan Kucheriavyi <bohdan.kucheriavyi@zapal.tech>
2024-04-16 09:32:08 -04:00
Jarrod Flesch
697a0f1ecf fix: ensure body limit is respected (#5807)
Co-authored-by: James <james@trbl.design>
2024-04-16 09:22:41 -04:00
Jarrod Flesch
3db0557b07 chore: improve cookie helper functions (#5866) 2024-04-15 22:11:17 -04:00
Elliot DeNolf
8178d57ab9 chore(release): v3.0.0-beta.11 [skip ci] 2024-04-15 16:50:51 -04:00
Ritsu
f1b2f767bb fix(db-postgres): validateExistingBlockIsIdentical localized (#5840) 2024-04-15 16:50:28 -04:00
Dan Ribbens
f21b394d21 chore(create-payload-app): db user and password connection URI (#5853) 2024-04-15 16:40:31 -04:00
Dan Ribbens
4e4ccca02a fix(db-mongodb): version fields indexSortableFields (#5864) 2024-04-15 16:26:26 -04:00
Elliot DeNolf
b6578d6447 test(pcs): add prefix test (#5867) 2024-04-15 16:10:51 -04:00
Elliot DeNolf
abeb94a53d docs: add externalFileHeaderFilter 2024-04-15 15:11:35 -04:00
Ritsu
974a74500b fix(ui): passes cellComponentProps through buildColumnState (#5848) 2024-04-15 15:03:16 -04:00
Elliot DeNolf
61dd17ae5e feat: allow configuration for setting headers on external file fetch (#5862) 2024-04-15 15:02:07 -04:00
Jacob Fletcher
2628249a51 fix(next): removes links to hidden entities (#5861) 2024-04-15 14:58:57 -04:00
Elliot DeNolf
5f57782199 fix(db-postgres): properly pass id type for type gen (#5859) 2024-04-15 13:38:46 -04:00
Ritsu
bceb49ee6c fix(ui): ensures titleField is not empty (#5850) 2024-04-15 13:33:49 -03:00
Alessio Gravili
beeb59f263 ci: add weird tune linux network step which seems to reduce flakes (#5855) 2024-04-15 12:23:48 -04:00
Paul
4150c87be0 chore(plugin-nested-docs): update nested docs plugin exports and moved away from default exports (#5856) 2024-04-15 13:22:01 -03:00
Patrik
a394d8211e fix: passes parent id instead of incoming id to saveVersion (#5854) 2024-04-15 12:02:17 -04:00
Paul
6a162776f2 chore: export react toastify from UI (#5828) 2024-04-15 12:53:12 -03:00
James Mikrut
d41bd7b133 chore: exports getFieldsToSign (#5852) 2024-04-15 10:48:03 -04:00
Patrik
f3409fab29 fix(db-mongodb): failing contains query with special chars (#5776) 2024-04-15 10:24:07 -04:00
James Mikrut
dd75fbfee2 feat: image dimensions rework (#5824) 2024-04-15 10:22:40 -04:00
James
ae2c85f947 chore: exports getFieldsToSign 2024-04-15 10:19:55 -04:00
Oladayo Olufemi Fagbemi
c0b454a5de fix(plugin-seo): add default empty endpoints array (#5844) 2024-04-14 17:44:15 -04:00
Alessio Gravili
27754dd0d7 fix(richtext-lexical): ensure schema maps for complex fields / sub-fields are handled correctly (#5842) 2024-04-14 17:29:48 -04:00
Oladayo Olufemi Fagbemi
18ec830882 fix(plugin-seo): incorrect styling of field labels (#5845)
Co-authored-by: Oladayo Fagbemi <oladayo.fagbemi@acelspringer.com>
Co-authored-by: Alessio Gravili <alessio@gravili.de>
2024-04-14 17:28:53 -04:00
Ritsu
1ffc0f552e fix(payload): remove incorrect payload module import within payload (#5847) 2024-04-14 16:49:06 -04:00
Alessio Gravili
07b676ac81 chore(richtext-lexical): adjust field name inside int tests 2024-04-14 16:45:54 -04:00
Alessio Gravili
da79f09544 fix(payload): ensure that the minimum @swc/core peerdep version used is 1.4.13 (#5841) 2024-04-14 16:43:42 -04:00
Alessio Gravili
993b035285 fix(richtext-lexical): ensure schema maps for complex fields / sub-fields are handled correctly for blocks, link and upload features 2024-04-14 02:19:58 -04:00
Alessio Gravili
3f2df643e7 chore(richtext-lexical): add failing e2e test which ensures sub-richtext blocks work as intended 2024-04-14 02:18:16 -04:00
James
42c7649176 chore: removes tempy 2024-04-13 17:06:25 -04:00
Ritsu
2722d2f5ce fix: passes number type limit arg to find on the list view (#5837)
Co-authored-by: Paul <paul@payloadcms.com>
2024-04-13 17:24:46 -03:00
Elliot DeNolf
f71e61d7d9 chore(templates): remove no longer used editor-import comment 2024-04-13 10:27:39 -04:00
Gabriel Novotny
73c76cab77 fix: postgres turbopack static analysis error (#5832) 2024-04-12 18:53:19 -04:00
Elliot DeNolf
43e8a533b7 fix: ensure file persists through form state changes (#5823) 2024-04-12 15:16:47 -04:00
Elliot DeNolf
ea4203bb32 chore(release): v3.0.0-beta.10 [skip ci] 2024-04-12 14:58:12 -04:00
Elliot DeNolf
568b5073c8 chore(deps): sync pnpm-lock.yaml 2024-04-12 14:56:42 -04:00
Elliot DeNolf
471e1f4827 chore: specify next canary peer dep 2024-04-12 14:56:04 -04:00
Elliot DeNolf
b9185a6fcd chore: fix more publish config exports 2024-04-12 14:48:34 -04:00
Elliot DeNolf
80496aa94c fix: remove all exports null (#5830) 2024-04-12 14:35:36 -04:00
Alessio Gravili
5fd6e3c1a8 fix!: upgrade minimum required node version from 18.17.0 to v18.20.2. Some old node versions have issues with our loader (#5829) 2024-04-12 14:01:33 -04:00
Alessio Gravili
54590c1700 fix(richtext-lexical)!: fix output of internal list HTML converter (#5827)
BREAKING: Changes the classnames of the converted HTML
2024-04-12 12:10:44 -04:00
Elliot DeNolf
b1259be8f2 chore(release): v3.0.0-beta.9 [skip ci] 2024-04-12 12:06:41 -04:00
Elliot DeNolf
cd161e4b16 feat!: remove pointer files (#5826) 2024-04-12 11:58:34 -04:00
Alessio Gravili
cb4214fe6e fix(richtext-lexical)!: fix output of internal list HTML converter
BREAKING: Changes the classnames of the converted HTML
2024-04-12 11:58:05 -04:00
Elliot DeNolf
9d42751a42 feat!: remove more pointer files 2024-04-12 11:39:08 -04:00
Elliot DeNolf
c2c637b359 chore: clean up unused files 2024-04-12 11:35:25 -04:00
Paul
2f446e11d6 chore: bump nextjs dependencies to ^14.2 (#5820)
fix(plugin-seo): overriding existing endpoints
2024-04-12 12:32:45 -03:00
Elliot DeNolf
4f566b088c feat!: remove pointer files 2024-04-12 11:31:35 -04:00
Dan Ribbens
0d40d87b31 fix(db-postgres): relationship query pagination (#5803) 2024-04-12 11:18:40 -04:00
Elliot DeNolf
70fcd6bf40 feat: rework image dimensions, use image-size 2024-04-12 11:13:16 -04:00
Jacob Fletcher
94c0095b3b chore(ui): removes all static font assets (#5821) 2024-04-12 09:57:31 -04:00
Elliot DeNolf
4328060637 feat(pcs): vercel blob storage adapter (#5811) 2024-04-12 09:37:35 -04:00
Elliot DeNolf
d98d0fd5bd chore(pcs): use proper getFilePrefix 2024-04-12 09:30:19 -04:00
Elliot DeNolf
5db2863d08 feat(pcs): export utilities 2024-04-12 09:30:10 -04:00
Jacob Fletcher
ff5e438d6d chore(ui): replaces suisse-intl font with system fallbacks 2024-04-12 09:16:43 -04:00
Paul
bfd5f13ee9 chore: add types for local api find/update operations (#5808) 2024-04-12 10:15:57 -03:00
Elliot DeNolf
8043188f36 chore(pcs): update README 2024-04-12 09:12:34 -04:00
Elliot DeNolf
e3e0998772 chore: ignore vercelBlob pointers, adjust peer deps 2024-04-12 00:34:41 -04:00
Elliot DeNolf
86adc6f282 chore: add vercelBlob to exports 2024-04-11 23:05:22 -04:00
Elliot DeNolf
b51b519d30 feat(plugin-cloud-storage): vercel blob storage adapter 2024-04-11 22:58:55 -04:00
Alessio Gravili
c70dcb6a59 feat(richtext-lexical): add HorizontalRuleFeature, improve block handle positioning (#5806) 2024-04-11 16:51:54 -04:00
Alessio Gravili
2486c7dba0 fix(richtext-lexical): incorrect margin for nested unordered lists 2024-04-11 16:42:35 -04:00
Paul
1456fcdcad chore: type locale from localization config on the payload request (#5801) 2024-04-11 17:38:35 -03:00
Alessio Gravili
a216800c72 chore(richtext-lexical): fix build error 2024-04-11 16:31:40 -04:00
Alessio Gravili
c3d8597c13 feat(richtext-lexical): add HorizontalRuleFeature 2024-04-11 16:24:04 -04:00
Alessio Gravili
844663ce1a fix(richtext-lexical): limit unnecessary floating handle positioning updates 2024-04-11 15:55:55 -04:00
Alessio Gravili
055e6af7b7 feat(richtext-lexical): improve floating handle y-positioning by positioning it in the center for smaller elements. 2024-04-11 15:55:43 -04:00
Alessio Gravili
479e6ecddc fix(richtext-lexical): incorrect floating handle y-position calculation next to certain kinds of HTML elements like HR 2024-04-11 15:55:26 -04:00
Jacob Fletcher
b9456e8244 fix(next): safely handles missing json body in post requests (#5797) 2024-04-11 15:53:50 -04:00
Jacob Fletcher
432dfef435 chore(next): installs merriweather as google font and removes static assets 2024-04-11 15:31:19 -04:00
Elliot DeNolf
429c6f7a48 chore(release): v3.0.0-beta.6 [skip ci] 2024-04-11 15:20:10 -04:00
Elliot DeNolf
6d41f6c56d fix(ui): actual scss paths [skip ci] 2024-04-11 15:17:33 -04:00
Jacob Fletcher
bcb538aee2 fix(next): awaits logout operation in api route handler 2024-04-11 14:57:35 -04:00
Jacob Fletcher
01e8f8c649 Merge branch 'beta' into fix/post-body-parse 2024-04-11 14:15:56 -04:00
Jacob Fletcher
330e4a7724 fix(next): safely handles missing json body in post requests 2024-04-11 13:42:21 -04:00
597 changed files with 11212 additions and 5169 deletions

38
.github/CODEOWNERS vendored
View File

@@ -1,41 +1,33 @@
# Order matters. The last matching pattern takes precedence.
### Core ###
/packages/payload/src/uploads/ @denolfe
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
### Package Exports ###
/**/exports/ @denolfe @jmikrut
### Adapters ###
/packages/db-*/ @denolfe @jmikrut @DanRibbens
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
/packages/richtext-*/ @AlessioGr
### Plugins ###
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens
/packages/plugin-cloud*/ @denolfe
/packages/plugin-form-builder/ @jacobsfletch
/packages/plugin-live-preview*/ @jacobsfletch
/packages/plugin-nested-docs/ @jacobsfletch
/packages/plugin-redirects/ @jacobsfletch
/packages/plugin-search/ @jacobsfletch
/packages/plugin-sentry/ @JessChowdhury
/packages/plugin-seo/ @jacobsfletch
/packages/plugin-stripe/ @jacobsfletch
### Examples ###
/examples/ @jacobsfletch
/examples/testing/ @JarrodMFlesch
/examples/email/ @JessChowdhury
/examples/whitelabel/ @JessChowdhury
### Templates ###
/templates/ @jacobsfletch @denolfe
### Misc ###
/packages/create-payload-app/ @denolfe
/packages/eslint-config-payload/ @denolfe
/packages/payload-admin-bar/ @jacobsfletch
/packages/eslint-*/ @denolfe
### Build Files ###
/**/package.json @denolfe
/tsconfig.json @denolfe
/**/tsconfig*.json @denolfe
/jest.config.js @denolfe
/**/jest.config.js @denolfe
### Root ###
/package.json @denolfe
/scripts/ @denolfe
/.husky/ @denolfe
/.vscode/ @denolfe
/.github/ @denolfe
/.github/CODEOWNERS @denolfe

View File

@@ -10,6 +10,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: 18.20.2
PNPM_VERSION: 8.15.7
jobs:
changes:
runs-on: ubuntu-latest
@@ -19,6 +23,10 @@ jobs:
needs_build: ${{ steps.filter.outputs.needs_build }}
templates: ${{ steps.filter.outputs.templates }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
with:
fetch-depth: 25
@@ -49,15 +57,19 @@ jobs:
with:
fetch-depth: 25
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Get pnpm store directory
@@ -90,15 +102,19 @@ jobs:
needs: build
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
@@ -122,9 +138,9 @@ jobs:
database:
- mongodb
- postgres
# - postgres-custom-schema
# - postgres-uuid
# - supabase
- postgres-custom-schema
- postgres-uuid
- supabase
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -135,15 +151,19 @@ jobs:
AWS_REGION: us-east-1
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
@@ -217,7 +237,6 @@ jobs:
- access-control
- admin
- auth
- email
- field-error-states
- fields-relationship
- fields
@@ -227,6 +246,7 @@ jobs:
- fields__collections__Lexical
- live-preview
- localization
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs
- plugin-seo
@@ -234,15 +254,19 @@ jobs:
- uploads
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
@@ -252,6 +276,10 @@ jobs:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
- name: Install Playwright
run: pnpm exec playwright install --with-deps
@@ -263,6 +291,7 @@ jobs:
with:
name: test-results-${{ matrix.suite }}
path: test/test-results/
if-no-files-found: ignore
retention-days: 1
tests-type-generation:
@@ -271,15 +300,19 @@ jobs:
needs: build
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
@@ -308,11 +341,14 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0

97
.github/workflows/pr-title.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: pr-title
on:
pull_request:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: write
jobs:
main:
name: lint-pr-title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
build
chore
ci
docs
feat
fix
perf
refactor
revert
style
test
types
scopes: |
cpa
db-\*
db-mongodb
db-postgres
email-nodemailer
eslint
graphql
live-preview
live-preview-react
next
payload
plugin-cloud
plugin-cloud-storage
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-search
plugin-sentry
plugin-seo
plugin-stripe
richtext-\*
richtext-lexical
richtext-slate
storage-\*
storage-azure
storage-gcs
storage-vercel-blob
storage-s3
translations
ui
templates
examples
# Disallow uppercase letters at the beginning of the subject
subjectPattern: ^(?![A-Z]).+$
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Pull Request titles must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and have valid scopes.
${{ steps.lint_pr_title.outputs.error_message }}
```
feat(ui): add Button component
^ ^ ^
| | |__ Subject
| |_______ Scope
|____________ Type
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true

View File

@@ -1 +1 @@
v18.19.1
v18.20.2

2
.nvmrc
View File

@@ -1 +1 @@
v18.19.1
v18.20.2

23
.vscode/launch.json vendored
View File

@@ -41,6 +41,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js auth",
"cwd": "${workspaceFolder}",
"name": "Run Dev Auth",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",
@@ -69,36 +76,26 @@
}
},
{
"command": "pnpm run dev versions",
"command": "node --no-deprecation test/dev.js versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Versions",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev localization",
"command": "node --no-deprecation test/dev.js localization",
"cwd": "${workspaceFolder}",
"name": "Run Dev Localization",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev uploads",
"command": "node --no-deprecation test/dev.js uploads",
"cwd": "${workspaceFolder}",
"name": "Run Dev Uploads",
"request": "launch",
"type": "node-terminal"
},
{
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields (Vite)",
"request": "launch",
"type": "node-terminal",
"env": {
"NODE_ENV": "production"
}
},
{
"command": "pnpm run test:int live-preview",
"cwd": "${workspaceFolder}",

View File

@@ -1,9 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -4,7 +4,7 @@ label: JSON
order: 50
desc: The JSON field type will store any string in the Database. Learn how to use JSON fields, see examples and options.
keywords: json, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
keywords: json, jsonSchema, schema, validation, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
<Banner>
@@ -30,6 +30,7 @@ This field uses the `monaco-react` editor syntax highlighting.
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
| **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step)
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
@@ -52,7 +53,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
### Example
`collections/ExampleCollection.ts
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
@@ -68,3 +69,67 @@ export const ExampleCollection: CollectionConfig = {
],
}
```
### JSON Schema Validation
Payload JSON fields fully support the [JSON schema](https://json-schema.org/) standard. By providing a schema in your field config, the editor will be guided in the admin UI, getting typeahead for properties and their formats automatically. When the document is saved, the default validation will prevent saving any invalid data in the field according to the schema in your config.
If you only provide a URL to a schema, Payload will fetch the desired schema if it is publicly available. If not, it is recommended to add the schema directly to your config or import it from another file so that it can be implemented consistently in your project.
#### Local JSON Schema
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'customerJSON', // required
type: 'json', // required
jsonSchema: {
uri: 'a://b/foo.json', // required
fileMatch: ['a://b/foo.json'], // required
schema: {
type: 'object',
properties: {
foo: {
enum: ['bar', 'foobar'],
}
},
},
},
},
],
}
// {"foo": "bar"} or {"foo": "foobar"} - ok
// Attempting to create {"foo": "not-bar"} will throw an error
```
#### Remote JSON Schema
`collections/ExampleCollection.ts`
```ts
import { CollectionConfig } from 'payload/types'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'customerJSON', // required
type: 'json', // required
jsonSchema: {
uri: 'https://example.com/customer.schema.json', // required
fileMatch: ['https://example.com/customer.schema.json'], // required
},
},
],
}
// If 'https://example.com/customer.schema.json' has a JSON schema
// {"foo": "bar"} or {"foo": "foobar"} - ok
// Attempting to create {"foo": "not-bar"} will throw an error
```

View File

@@ -42,11 +42,12 @@ export const PublicUser: CollectionConfig = {
**Payload will automatically open up the following queries:**
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`mePublicUser`** | `me` auth operation |
| Query Name | Operation |
| ------------------ | ------------------- |
| **`PublicUser`** | `findByID` |
| **`PublicUsers`** | `find` |
| **`countPublicUsers`** | `count` |
| **`mePublicUser`** | `me` auth operation |
**And the following mutations:**

View File

@@ -8,7 +8,7 @@ keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, us
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly.
Wiring your front-end into Live Preview is easy. If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides. In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
Wiring your front-end into Live Preview is easy. If your front-end application is built with React, Next.js, Vue or Nuxt.js, use the `useLivePreview` hook that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
By default, all hooks accept the following args:
@@ -36,6 +36,10 @@ And return the following values:
For example, `data?.relatedPosts?.[0]?.title`.
</Banner>
<Banner type="info">
It is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information.
</Banner>
### React
If your front-end application is built with React or Next.js, you can use the `useLivePreview` hook that Payload provides.
@@ -71,11 +75,40 @@ export const PageClient: React.FC<{
}
```
<Banner type="info">
If is important that the `depth` argument matches exactly with the depth of your initial page
request. The depth property is used to populated relationships and uploads beyond their IDs. See
[Depth](../getting-started/concepts#depth) for more information.
</Banner>
### Vue
If your front-end application is built with Vue 3 or Nuxt 3, you can use the `useLivePreview` composable that Payload provides.
First, install the `@payloadcms/live-preview-vue` package:
```bash
npm install @payloadcms/live-preview-vue
```
Then, use the `useLivePreview` hook in your Vue component:
```vue
<script setup lang="ts">
import type { PageData } from '~/types';
import { defineProps } from 'vue';
import { useLivePreview } from '@payloadcms/live-preview-vue';
// Fetch the initial data on the parent component or using async state
const props = defineProps<{ initialData: PageData }>();
// The hook will take over from here and keep the preview in sync with the changes you make.
// The `data` property will contain the live data of the document only when viewed from the Preview view of the Admin UI.
const { data } = useLivePreview<PageData>({
initialData: props.initialData,
serverURL: "<PAYLOAD_SERVER_URL>",
depth: 2,
});
</script>
<template>
<h1>{{ data.title }}</h1>
</template>
```
## Building your own hook

View File

@@ -164,6 +164,22 @@ const result = await payload.findByID({
})
```
#### Count
```js
// Result will be an object with:
// {
// totalDocs: 10, // count of the documents satisfies query
// }
const result = await payload.count({
collection: 'posts', // required
locale: 'en',
where: {}, // pass a `where` query here
user: dummyUser,
overrideAccess: false,
})
```
#### Update by ID
```js

View File

@@ -90,6 +90,19 @@ Note: Collection slugs must be formatted in kebab-case
},
},
},
{
operation: "Count",
method: "GET",
path: "/api/{collection-slug}/count",
description: "Count the documents",
example: {
slug: "count",
req: true,
res: {
totalDocs: 10
},
},
},
{
operation: "Create",
method: "POST",

View File

@@ -138,7 +138,7 @@ import { CallToAction } from '../blocks/CallToAction'
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
| ------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
@@ -157,7 +157,8 @@ Here's an overview of all the included features:
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an <hr> element |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
## Creating your own, custom Feature
@@ -234,6 +235,19 @@ This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`,
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
#### CSS
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
Here is some "base" CSS you can use to ensure that nested lists render correctly:
```css
/* Base CSS for Lexical HTML */
.nestedListItem, .list-check {
list-style-type: none;
}
```
#### Creating your own HTML Converter
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:

View File

@@ -40,21 +40,22 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
### Collection Upload Options
| Option | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| Option | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
_An asterisk denotes that a property above is required._

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.5",
"version": "3.0.0-beta.15",
"private": true,
"type": "module",
"workspaces:": [
@@ -18,6 +18,7 @@
"build:create-payload-app": "turbo build --filter create-payload-app",
"build:db-mongodb": "turbo build --filter db-mongodb",
"build:db-postgres": "turbo build --filter db-postgres",
"build:email-nodemailer": "turbo build --filter email-nodemailer",
"build:eslint-config-payload": "turbo build --filter eslint-config-payload",
"build:graphql": "turbo build --filter graphql",
"build:live-preview": "turbo build --filter live-preview",
@@ -35,7 +36,7 @@
"build:plugin-stripe": "turbo build --filter plugin-stripe",
"build:richtext-lexical": "turbo build --filter richtext-lexical",
"build:richtext-slate": "turbo build --filter richtext-slate",
"build:tests": "pnpm --filter test run typecheck",
"build:tests": "pnpm --filter payload-test-suite run typecheck",
"build:translations": "turbo build --filter translations",
"build:ui": "turbo build --filter ui",
"clean": "turbo clean",
@@ -127,7 +128,7 @@
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "14.2.0-canary.22",
"next": "^14.3.0-canary.7",
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
@@ -163,8 +164,8 @@
"react": "18.2.0"
},
"engines": {
"node": ">=18.17.0",
"pnpm": ">=8"
"node": ">=18.20.2",
"pnpm": "^8.15.7"
},
"lint-staged": {
"*.{md,mdx,yml,json}": "prettier --write",
@@ -175,6 +176,7 @@
},
"dependencies": {
"@sentry/react": "^7.77.0",
"ajv": "^8.12.0",
"passport-strategy": "1.0.0"
},
"pnpm": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.5",
"version": "3.0.0-beta.15",
"license": "MIT",
"type": "module",
"homepage": "https://payloadcms.com",

View File

@@ -1,5 +1,9 @@
import fse from 'fs-extra'
import globby from 'globby'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { DbDetails } from '../types.js'
@@ -15,6 +19,34 @@ export async function configurePayloadConfig(args: {
return
}
// Update package.json
const packageJsonPath =
'projectDir' in args.projectDirOrConfigPath &&
path.resolve(args.projectDirOrConfigPath.projectDir, 'package.json')
if (packageJsonPath && fse.existsSync(packageJsonPath)) {
try {
const packageObj = await fse.readJson(packageJsonPath)
const dbPackage = dbReplacements[args.dbDetails.type]
// Delete all other db adapters
Object.values(dbReplacements).forEach((p) => {
if (p.packageName !== dbPackage.packageName) {
delete packageObj.dependencies[p.packageName]
}
})
// Set version of db adapter to match payload version
packageObj.dependencies[dbPackage.packageName] = packageObj.dependencies['payload']
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning(`Unable to configure Payload in package.json`)
warning(err instanceof Error ? err.message : '')
}
}
try {
let payloadConfigPath: string | undefined
if (!('payloadConfigPath' in args.projectDirOrConfigPath)) {

View File

@@ -117,13 +117,17 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
warning(`Could not find tsconfig.json to add @payload-config path.`)
return
}
const userTsConfigContent = await readFile(tsConfigPath, {
encoding: 'utf8',
})
const userTsConfig = parse(userTsConfigContent) as {
compilerOptions?: CompilerOptions
}
const hasBaseUrl =
userTsConfig?.compilerOptions?.baseUrl && userTsConfig?.compilerOptions?.baseUrl !== '.'
const baseUrl = hasBaseUrl ? userTsConfig?.compilerOptions?.baseUrl : './'
if (!userTsConfig.compilerOptions && !('extends' in userTsConfig)) {
userTsConfig.compilerOptions = {}
}
@@ -134,7 +138,7 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
) {
userTsConfig.compilerOptions.paths = {
...(userTsConfig.compilerOptions.paths || {}),
'@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`],
'@payload-config': [`${baseUrl}${isSrcDir ? 'src/' : ''}payload.config.ts`],
}
await writeFile(tsConfigPath, stringify(userTsConfig, null, 2), { encoding: 'utf8' })
}

View File

@@ -16,7 +16,7 @@ const dbChoiceRecord: Record<DbType, DbChoice> = {
value: 'mongodb',
},
postgres: {
dbConnectionPrefix: 'postgres://127.0.0.1:5432/',
dbConnectionPrefix: 'postgres://postgres:<password>@127.0.0.1:5432/',
title: 'PostgreSQL (beta)',
value: 'postgres',
},

View File

@@ -41,12 +41,12 @@ export function getValidTemplates(): ProjectTemplate[] {
// description: 'E-commerce Template',
// url: 'https://github.com/payloadcms/payload/templates/ecommerce',
// },
// {
// name: 'plugin',
// type: 'plugin',
// description: 'Template for creating a Payload plugin',
// url: 'https://github.com/payloadcms/payload-plugin-template',
// },
{
name: 'plugin',
type: 'plugin',
description: 'Template for creating a Payload plugin',
url: 'https://github.com/payloadcms/payload-plugin-template#beta',
},
// {
// name: 'payload-demo',
// type: 'starter',

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.5",
"version": "3.0.0-beta.15",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -0,0 +1,49 @@
import type { QueryOptions } from 'mongoose'
import type { Count } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { flattenWhereToOperators } from 'payload/database'
import type { MongooseAdapter } from './index.js'
import { withSession } from './withSession.js'
export const count: Count = async function count(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const options: QueryOptions = withSession(this, req.transactionID)
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await Model.buildQuery({
locale,
payload: this.payload,
where,
})
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
options.hint = {
_id: 1,
}
}
const result = await Model.countDocuments(query, options)
return {
totalDocs: result,
}
}

View File

@@ -12,6 +12,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { CollectionModel, GlobalModel } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js'
import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js'
@@ -112,6 +113,7 @@ export function mongooseAdapter({
collections: {},
connectOptions: connectOptions || {},
connection: undefined,
count,
disableIndexHints,
globals: undefined,
mongoMemoryServer,
@@ -119,7 +121,6 @@ export function mongooseAdapter({
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url,
versions: {},
// DatabaseAdapter
beginTransaction: transactionOptions ? beginTransaction : undefined,
commitTransaction,

View File

@@ -28,6 +28,7 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,

View File

@@ -142,7 +142,10 @@ export const sanitizeQueryValue = ({
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains') {
formattedValue = { $options: 'i', $regex: formattedValue }
formattedValue = {
$options: 'i',
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
}
}
}

View File

@@ -6,6 +6,10 @@ export const commitTransaction: CommitTransaction = async function commitTransac
}
await this.sessions[id].commitTransaction()
await this.sessions[id].endSession()
try {
await this.sessions[id].endSession()
} catch (error) {
// ending sessions is only best effort and won't impact anything if it fails since the transaction was committed
}
delete this.sessions[id]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.5",
"version": "3.0.0-beta.15",
"description": "The officially supported Postgres database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -0,0 +1,65 @@
import type { Count } from 'payload/database'
import type { SanitizedCollectionConfig } from 'payload/types'
import { sql } from 'drizzle-orm'
import type { ChainedMethods } from './find/chainMethods.js'
import type { PostgresAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
export const count: Count = async function count(
this: PostgresAdapter,
{ collection, locale, req, where: whereArg },
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
const db = this.sessions[req.transactionID]?.db || this.drizzle
const table = this.tables[tableName]
const { joinAliases, joins, where } = await buildQuery({
adapter: this,
fields: collectionConfig.fields,
locale,
tableName,
where: whereArg,
})
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {
selectCountMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
selectCountMethods.push({
args: [this.tables[joinTable], condition],
method: 'leftJoin',
})
}
})
const countResult = await chainMethods({
methods: selectCountMethods,
query: db
.select({
count: sql<number>`count
(DISTINCT ${this.tables[tableName].id})`,
})
.from(table)
.where(where),
})
return { totalDocs: Number(countResult[0].count) }
}

View File

@@ -2,13 +2,13 @@ import type { Destroy } from 'payload/database'
import type { PostgresAdapter } from './types.js'
import { pushDevSchema } from './utilities/pushDevSchema.js'
// eslint-disable-next-line @typescript-eslint/require-await
export const destroy: Destroy = async function destroy(this: PostgresAdapter) {
if (process.env.NODE_ENV !== 'production') {
await pushDevSchema(this)
} else {
// TODO: this hangs test suite for some reason
// await this.pool.end()
}
this.enums = {}
this.schema = {}
this.tables = {}
this.relations = {}
this.blockTableNames = {}
this.fieldConstraints = {}
this.drizzle = undefined
}

View File

@@ -120,7 +120,7 @@ export const findMany = async function find({
const findPromise = db.query[tableName].findMany(findManyArgs)
if (pagination !== false && (orderedIDs ? orderedIDs?.length >= limit : true)) {
if (pagination !== false && (orderedIDs ? orderedIDs?.length <= limit : true)) {
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {

View File

@@ -8,6 +8,7 @@ import { createDatabaseAdapter } from 'payload/database'
import type { Args, PostgresAdapter } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
import { create } from './create.js'
import { createGlobal } from './createGlobal.js'
import { createGlobalVersion } from './createGlobalVersion.js'
@@ -43,9 +44,11 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
const postgresIDType = args.idType || 'serial'
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(args.migrationDir)
const idType = args.idType || 'serial'
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
@@ -55,7 +58,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
drizzle: undefined,
enums: {},
fieldConstraints: {},
idType,
idType: postgresIDType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
pgSchema: undefined,
@@ -74,15 +77,13 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
beginTransaction,
commitTransaction,
connect,
count,
create,
createGlobal,
createGlobalVersion,
createMigration,
createVersion,
/**
* This represents how a default ID is treated in Payload as were a field type
*/
defaultIDType: idType === 'serial' ? 'number' : 'text',
defaultIDType: payloadIDType,
deleteMany,
deleteOne,
deleteVersions,
@@ -111,7 +112,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
}
return {
defaultIDType: 'number',
defaultIDType: payloadIDType,
init: adapter,
}
}

View File

@@ -225,6 +225,85 @@ export const getTableColumnFromPath = ({
})
}
case 'select': {
if (field.hasMany) {
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName]._locale, locale),
)
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
} else {
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName].parent,
)
}
return {
columnName: 'value',
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'text':
case 'number': {
if (field.hasMany) {
let tableType = 'texts'
let columnName = 'text'
if (field.type === 'number') {
tableType = 'numbers'
columnName = 'number'
}
newTableName = `${tableName}_${tableType}`
const joinConstraints = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
...joinConstraints,
eq(adapter.tables[newTableName]._locale, locale),
)
if (locale !== 'all') {
constraints.push({
columnName: 'locale',
table: adapter.tables[newTableName],
value: locale,
})
}
} else {
joins[newTableName] = and(...joinConstraints)
}
return {
columnName,
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'array': {
newTableName = getTableName({
adapter,
@@ -485,43 +564,41 @@ export const getTableColumnFromPath = ({
value,
})
}
}
default: {
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName]
const parentTable = aliasTable || adapter.tables[tableName]
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
aliasTable = undefined
aliasTable = undefined
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
}
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type {
ForeignKeyBuilder,
IndexBuilder,
PgColumnBuilder,
PgTableWithColumns,
@@ -9,8 +10,17 @@ import type {
import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import {
foreignKey,
index,
integer,
numeric,
serial,
timestamp,
unique,
varchar,
} from 'drizzle-orm/pg-core'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
@@ -19,10 +29,15 @@ import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js'
export type BaseExtraConfig = Record<
string,
(cols: GenericColumns) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>
type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
baseExtraConfig?: BaseExtraConfig
buildNumbers?: boolean
buildRelationships?: boolean
buildTexts?: boolean
@@ -66,13 +81,6 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasLocalizedManyNumberField = false
const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable | PgTableWithColumns<any>
@@ -89,7 +97,7 @@ export const buildTable = ({
const idColType: IDType = setColumnID({ adapter, columns, fields })
;({
const {
hasLocalizedField,
hasLocalizedManyNumberField,
hasLocalizedManyTextField,
@@ -116,7 +124,7 @@ export const buildTable = ({
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
}))
})
if (timestamps) {
columns.createdAt = timestamp('created_at', {
@@ -141,10 +149,12 @@ export const buildTable = ({
return config
}, {})
return Object.entries(indexes).reduce((acc, [colName, func]) => {
const result = Object.entries(indexes).reduce((acc, [colName, func]) => {
acc[colName] = func(cols)
return acc
}, extraConfig)
return result
})
adapter.tables[tableName] = table
@@ -153,9 +163,7 @@ export const buildTable = ({
const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id').notNull()
localesTable = adapter.pgSchema.table(localeTableName, localesColumns, (cols) => {
return Object.entries(localesIndexes).reduce(
@@ -168,6 +176,11 @@ export const buildTable = ({
cols._locale,
cols._parentID,
),
_parentIdFk: foreignKey({
name: `${localeTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [table.id],
}).onDelete('cascade'),
},
)
})
@@ -189,9 +202,7 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
path: varchar('path').notNull(),
text: varchar('text'),
}
@@ -201,19 +212,24 @@ export const buildTable = ({
}
textsTable = adapter.pgSchema.table(textsTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
const config: Record<string, ForeignKeyBuilder | IndexBuilder> = {
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent),
parentFk: foreignKey({
name: `${textsTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
}
if (hasManyTextField === 'index') {
indexes.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
config.text_idx = index(`${textsTableName}_text_idx`).on(cols.text)
}
if (hasLocalizedManyTextField) {
indexes.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
config.localeParent = index(`${textsTableName}_locale_parent`).on(cols.locale, cols.parent)
}
return indexes
return config
})
adapter.tables[textsTableName] = textsTable
@@ -234,9 +250,7 @@ export const buildTable = ({
id: serial('id').primaryKey(),
number: numeric('number'),
order: integer('order').notNull(),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
path: varchar('path').notNull(),
}
@@ -245,22 +259,27 @@ export const buildTable = ({
}
numbersTable = adapter.pgSchema.table(numbersTableName, columns, (cols) => {
const indexes: Record<string, IndexBuilder> = {
const config: Record<string, ForeignKeyBuilder | IndexBuilder> = {
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent),
parentFk: foreignKey({
name: `${numbersTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
}
if (hasManyNumberField === 'index') {
indexes.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
config.numberIdx = index(`${numbersTableName}_number_idx`).on(cols.number)
}
if (hasLocalizedManyNumberField) {
indexes.localeParent = index(`${numbersTableName}_locale_parent`).on(
config.localeParent = index(`${numbersTableName}_locale_parent`).on(
cols.locale,
cols.parent,
)
}
return indexes
return config
})
adapter.tables[numbersTableName] = numbersTable
@@ -280,9 +299,7 @@ export const buildTable = ({
const relationshipColumns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
order: integer('order'),
parent: parentIDColumnMap[idColType]('parent_id')
.references(() => table.id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[idColType]('parent_id').notNull(),
path: varchar('path').notNull(),
}
@@ -290,6 +307,10 @@ export const buildTable = ({
relationshipColumns.locale = adapter.enums.enum__locales('locale')
}
const relationExtraConfig: BaseExtraConfig = {}
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationships.forEach((relationTo) => {
const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = getTableName({
@@ -307,20 +328,38 @@ export const buildTable = ({
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
`${formattedRelationTo}_id`,
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
)
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationExtraConfig[`${relationTo}IdFk`] = (cols) =>
foreignKey({
name: `${relationshipsTableName}_${toSnakeCase(relationTo)}_fk`,
columns: [cols[`${relationTo}ID`]],
foreignColumns: [adapter.tables[formattedRelationTo].id],
}).onDelete('cascade')
})
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
relationshipColumns,
(cols) => {
const result: Record<string, unknown> = {
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
}
const result: Record<string, ForeignKeyBuilder | IndexBuilder> = Object.entries(
relationExtraConfig,
).reduce(
(config, [key, func]) => {
config[key] = func(cols)
return config
},
{
order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
parentFk: foreignKey({
name: `${relationshipsTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [table.id],
}).onDelete('cascade'),
parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
},
)
if (hasLocalizedRelationshipField) {
result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { relations } from 'drizzle-orm'
@@ -9,6 +9,7 @@ import {
PgUUIDBuilder,
PgVarcharBuilder,
boolean,
foreignKey,
index,
integer,
jsonb,
@@ -23,6 +24,7 @@ import { fieldAffectsData, optionIsObject } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import type { BaseExtraConfig } from './build.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js'
@@ -228,7 +230,6 @@ export const traverseFields = ({
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
@@ -249,21 +250,21 @@ export const traverseFields = ({
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id').notNull(),
value: adapter.enums[enumName]('value'),
}
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
const baseExtraConfig: BaseExtraConfig = {
orderIdx: (cols) => index(`${selectTableName}_order_idx`).on(cols.order),
parentFk: (cols) =>
foreignKey({
name: `${selectTableName}_parent_fk`,
columns: [cols.parent],
foreignColumns: [adapter.tables[parentTableName].id],
}),
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
@@ -323,18 +324,20 @@ export const traverseFields = ({
prefix: `${newTableName}_`,
throwValidationError,
})
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
.references(() => adapter.tables[parentTableName].id, { onDelete: 'cascade' })
.notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id').notNull(),
}
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
const baseExtraConfig: BaseExtraConfig = {
_orderIdx: (cols) => index(`${arrayTableName}_order_idx`).on(cols._order),
_parentIDFk: (cols) =>
foreignKey({
name: `${arrayTableName}_parent_id_fk`,
columns: [cols['_parentID']],
foreignColumns: [adapter.tables[parentTableName].id],
}).onDelete('cascade'),
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
}
@@ -412,18 +415,19 @@ export const traverseFields = ({
if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id')
.references(() => adapter.tables[rootTableName].id, { onDelete: 'cascade' })
.notNull(),
_parentID: parentIDColumnMap[rootTableIDColType]('_parent_id').notNull(),
_path: text('_path').notNull(),
}
const baseExtraConfig: Record<
string,
(cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder
> = {
const baseExtraConfig: BaseExtraConfig = {
_orderIdx: (cols) => index(`${blockTableName}_order_idx`).on(cols._order),
_parentIDIdx: (cols) => index(`${blockTableName}_parent_id_idx`).on(cols._parentID),
_parentIdFk: (cols) =>
foreignKey({
name: `${blockTableName}_parent_id_fk`,
columns: [cols._parentID],
foreignColumns: [adapter.tables[rootTableName].id],
}).onDelete('cascade'),
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
}
@@ -493,6 +497,7 @@ export const traverseFields = ({
localized: field.localized,
rootTableName,
table: adapter.tables[blockTableName],
tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
@@ -658,7 +663,7 @@ export const traverseFields = ({
indexes,
localesColumns,
localesIndexes,
newTableName: parentTableName,
newTableName,
parentTableName,
relationsToBuild,
relationships,

View File

@@ -10,9 +10,13 @@ type Args = {
localized: boolean
rootTableName: string
table: GenericTable
tableLocales?: GenericTable
}
const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[] => {
const getFlattenedFieldNames = (
fields: Field[],
prefix: string = '',
): { localized?: boolean; name: string }[] => {
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
@@ -24,7 +28,7 @@ const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[]
}
if (fieldHasSubFields(field)) {
fieldPrefix = 'name' in field ? `${prefix}${field.name}.` : prefix
fieldPrefix = 'name' in field ? `${prefix}${field.name}_` : prefix
return [...fieldsToUse, ...getFlattenedFieldNames(field.fields, fieldPrefix)]
}
@@ -32,7 +36,7 @@ const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[]
return [
...fieldsToUse,
...field.tabs.reduce((tabFields, tab) => {
fieldPrefix = 'name' in tab ? `${prefix}.${tab.name}` : prefix
fieldPrefix = 'name' in tab ? `${prefix}_${tab.name}` : prefix
return [
...tabFields,
...(tabHasName(tab)
@@ -44,7 +48,13 @@ const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[]
}
if (fieldAffectsData(field)) {
return [...fieldsToUse, `${fieldPrefix?.replace('.', '_') || ''}${field.name}`]
return [
...fieldsToUse,
{
name: `${fieldPrefix}${field.name}`,
localized: field.localized,
},
]
}
return fieldsToUse
@@ -56,22 +66,30 @@ export const validateExistingBlockIsIdentical = ({
localized,
rootTableName,
table,
tableLocales,
}: Args): void => {
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField =
// ensure every field from the config is in the matching table
fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) ||
fieldNames.find(({ name, localized }) => {
const fieldTable = localized && tableLocales ? tableLocales : table
return Object.keys(fieldTable).indexOf(name) === -1
}) ||
// ensure every table column is matched for every field from the config
Object.keys(table).find((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
return fieldNames.indexOf(fieldName) === -1
return fieldNames.findIndex((field) => field.name) === -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.`,
`The table ${rootTableName} has multiple blocks with slug ${
block.slug
}, but the schemas do not match. One block includes the field ${
typeof missingField === 'string' ? missingField : missingField.name
}, while the other block does not.`,
)
}

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -0,0 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
}

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "es6"
}
}

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1 @@
# Nodemailer Email Adapter

View File

@@ -0,0 +1,59 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.15",
"description": "Payload Nodemailer Email Adapter",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/email-nodemailer"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"nodemailer": "6.9.10"
},
"peerDependencies": {
"payload": "workspace:*"
},
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"engines": {
"node": ">=18.20.2"
},
"files": [
"dist"
],
"devDependencies": {
"payload": "workspace:*",
"@types/nodemailer": "6.4.14"
}
}

View File

@@ -0,0 +1,123 @@
/* eslint-disable no-console */
import type { Transporter } from 'nodemailer'
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
import type { EmailAdapter } from 'payload/config'
import nodemailer from 'nodemailer'
import { InvalidConfiguration } from 'payload/errors'
export type NodemailerAdapterArgs = {
defaultFromAddress: string
defaultFromName: string
skipVerify?: boolean
transport?: Transporter
transportOptions?: SMTPConnection.Options
}
type NodemailerAdapter = EmailAdapter<unknown>
/**
* Creates an email adapter using nodemailer
*
* If no email configuration is provided, an ethereal email test account is returned
*/
export const nodemailerAdapter = async (
args?: NodemailerAdapterArgs,
): Promise<NodemailerAdapter> => {
const { defaultFromAddress, defaultFromName, transport } = await buildEmail(args)
const adapter: NodemailerAdapter = () => ({
defaultFromAddress,
defaultFromName,
sendEmail: async (message) => {
return await transport.sendMail({
from: `${defaultFromName} <${defaultFromAddress}>`,
...message,
})
},
})
return adapter
}
async function buildEmail(emailConfig?: NodemailerAdapterArgs): Promise<{
defaultFromAddress: string
defaultFromName: string
transport: Transporter
}> {
if (!emailConfig) {
const transport = await createMockAccount(emailConfig)
if (!transport) throw new InvalidConfiguration('Unable to create Nodemailer test account.')
return {
defaultFromAddress: 'info@payloadcms.com',
defaultFromName: 'Payload',
transport,
}
}
// Create or extract transport
let transport: Transporter
if ('transport' in emailConfig && emailConfig.transport) {
;({ transport } = emailConfig)
} else if ('transportOptions' in emailConfig && emailConfig.transportOptions) {
transport = nodemailer.createTransport(emailConfig.transportOptions)
} else {
transport = await createMockAccount(emailConfig)
}
if (emailConfig.skipVerify !== false) {
await verifyTransport(transport)
}
return {
defaultFromAddress: emailConfig.defaultFromAddress,
defaultFromName: emailConfig.defaultFromName,
transport,
}
}
async function verifyTransport(transport: Transporter) {
try {
await transport.verify()
} catch (err: unknown) {
console.error({ err, msg: 'Error verifying Nodemailer transport.' })
}
}
/**
* Use ethereal.email to create a mock email account
*/
async function createMockAccount(emailConfig?: NodemailerAdapterArgs) {
try {
const etherealAccount = await nodemailer.createTestAccount()
const smtpOptions = {
...(emailConfig || {}),
auth: {
pass: etherealAccount.pass,
user: etherealAccount.user,
},
fromAddress: emailConfig?.defaultFromAddress,
fromName: emailConfig?.defaultFromName,
host: 'smtp.ethereal.email',
port: 587,
secure: false,
}
const transport = nodemailer.createTransport(smtpOptions)
const { pass, user, web } = etherealAccount
console.info('E-mail configured with ethereal.email test account. ')
console.info(`Log into mock email provider at ${web}`)
console.info(`Mock email account username: ${user}`)
console.info(`Mock email account password: ${pass}`)
return transport
} catch (err: unknown) {
if (err instanceof Error) {
console.error({ err, msg: 'There was a problem setting up the mock email handler' })
throw new InvalidConfiguration(
`Unable to create Nodemailer test account. Error: ${err.message}`,
)
}
throw new InvalidConfiguration('Unable to create Nodemailer test account.')
}
}

View File

@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true, // Make sure typescript knows that this module depends on their references
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"strict": true,
},
"exclude": [
"dist",
"node_modules",
],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
"references": [
{ "path": "../payload" },
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.5",
"version": "3.0.0-beta.15",
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",
@@ -27,20 +27,17 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/pluralize": "^0.0.33",
"graphql-http": "^1.22.0",
"payload": "workspace:*",
"ts-essentials": "7.0.3"
},
"dependencies": {
"graphql": "16.8.1",
"graphql-http": "^1.22.0",
"graphql-playground-html": "1.6.30",
"graphql-query-complexity": "0.12.0",
"graphql-scalars": "1.22.2",
"graphql-type-json": "0.3.2",
"pluralize": "8.0.0"
},
"peerDependencies": {
"payload": "workspace:*"
"payload": "workspace:*",
"graphql": "^16.8.1"
},
"publishConfig": {
"main": "./dist/index.js",

View File

@@ -4,12 +4,12 @@ import type { GraphQLInfo } from 'payload/config'
import type { SanitizedConfig } from 'payload/types'
import * as GraphQL from 'graphql'
import {
createComplexityRule,
fieldExtensionsEstimator,
simpleEstimator,
} from 'graphql-query-complexity'
} from './packages/graphql-query-complexity/index.js'
import accessResolver from './resolvers/auth/access.js'
import buildFallbackLocaleInputType from './schema/buildFallbackLocaleInputType.js'
import buildLocaleInputType from './schema/buildLocaleInputType.js'
@@ -18,10 +18,10 @@ import initCollections from './schema/initCollections.js'
import initGlobals from './schema/initGlobals.js'
import { wrapCustomFields } from './utilities/wrapCustomResolver.js'
export async function configToSchema(config: SanitizedConfig): Promise<{
export function configToSchema(config: SanitizedConfig): {
schema: GraphQL.GraphQLSchema
validationRules: (args: OperationArgs<any>) => GraphQL.ValidationRule[]
}> {
} {
const collections = config.collections.reduce((acc, collection) => {
acc[collection.slug] = {
config: collection,

View File

@@ -0,0 +1,455 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
/**
* Created by Ivo Meißner on 28.07.17.
*/
import type {
DocumentNode,
FieldNode,
FragmentDefinitionNode,
FragmentSpreadNode,
GraphQLCompositeType,
GraphQLDirective,
GraphQLField,
GraphQLFieldMap,
GraphQLNamedType,
GraphQLSchema,
GraphQLUnionType,
InlineFragmentNode,
OperationDefinitionNode} from 'graphql';
import {
GraphQLError,
GraphQLInterfaceType,
GraphQLObjectType,
Kind,
TypeInfo,
ValidationContext,
getNamedType,
isAbstractType,
isCompositeType,
visit,
visitWithTypeInfo,
} from 'graphql'
import {
getArgumentValues,
getDirectiveValues,
getVariableValues,
} from 'graphql/execution/values.js'
export type ComplexityEstimatorArgs = {
args: { [key: string]: any }
childComplexity: number
context?: Record<string, any>
field: GraphQLField<any, any>
node: FieldNode
type: GraphQLCompositeType
}
export type ComplexityEstimator = (options: ComplexityEstimatorArgs) => number | void
// Complexity can be anything that is supported by the configured estimators
export type Complexity = any
// Map of complexities for possible types (of Union, Interface types)
type ComplexityMap = {
[typeName: string]: number
}
export interface QueryComplexityOptions {
// Pass request context to the estimators via estimationContext
context?: Record<string, any>
// The query variables. This is needed because the variables are not available
// Optional function to create a custom error
createError?: (max: number, actual: number) => GraphQLError
// An array of complexity estimators to use for estimating the complexity
estimators: Array<ComplexityEstimator>
// Optional callback function to retrieve the determined query complexity
// Will be invoked whether the query is rejected or not
// The maximum allowed query complexity, queries above this threshold will be rejected
maximumComplexity: number
// This can be used for logging or to implement rate limiting
onComplete?: (complexity: number) => void
// specify operation name only when pass multi-operation documents
operationName?: string
// in the visitor of the graphql-js library
variables?: Record<string, any>
}
function queryComplexityMessage(max: number, actual: number): string {
return `The query exceeds the maximum complexity of ${max}. ` + `Actual complexity is ${actual}`
}
export function getComplexity(options: {
context?: Record<string, any>
estimators: ComplexityEstimator[]
operationName?: string
query: DocumentNode
schema: GraphQLSchema
variables?: Record<string, any>
}): number {
const typeInfo = new TypeInfo(options.schema)
const errors: GraphQLError[] = []
const context = new ValidationContext(options.schema, options.query, typeInfo, (error) =>
errors.push(error),
)
const visitor = new QueryComplexity(context, {
// Maximum complexity does not matter since we're only interested in the calculated complexity.
context: options.context,
estimators: options.estimators,
maximumComplexity: Infinity,
operationName: options.operationName,
variables: options.variables,
})
visit(options.query, visitWithTypeInfo(typeInfo, visitor))
// Throw first error if any
if (errors.length) {
throw errors.pop()
}
return visitor.complexity
}
export default class QueryComplexity {
OperationDefinition: Record<string, any>
complexity: number
context: ValidationContext
estimators: Array<ComplexityEstimator>
includeDirectiveDef: GraphQLDirective
options: QueryComplexityOptions
requestContext?: Record<string, any>
skipDirectiveDef: GraphQLDirective
variableValues: Record<string, any>
constructor(context: ValidationContext, options: QueryComplexityOptions) {
if (!(typeof options.maximumComplexity === 'number' && options.maximumComplexity > 0)) {
throw new Error('Maximum query complexity must be a positive number')
}
this.context = context
this.complexity = 0
this.options = options
this.includeDirectiveDef = this.context.getSchema().getDirective('include')
this.skipDirectiveDef = this.context.getSchema().getDirective('skip')
this.estimators = options.estimators
this.variableValues = {}
this.requestContext = options.context
this.OperationDefinition = {
enter: this.onOperationDefinitionEnter,
leave: this.onOperationDefinitionLeave,
}
}
createError(): GraphQLError {
if (typeof this.options.createError === 'function') {
return this.options.createError(this.options.maximumComplexity, this.complexity)
}
return new GraphQLError(queryComplexityMessage(this.options.maximumComplexity, this.complexity))
}
nodeComplexity(
node: FieldNode | FragmentDefinitionNode | InlineFragmentNode | OperationDefinitionNode,
typeDef: GraphQLInterfaceType | GraphQLObjectType | GraphQLUnionType,
): number {
if (node.selectionSet) {
let fields: GraphQLFieldMap<any, any> = {}
if (typeDef instanceof GraphQLObjectType || typeDef instanceof GraphQLInterfaceType) {
fields = typeDef.getFields()
}
// Determine all possible types of the current node
let possibleTypeNames: string[]
if (isAbstractType(typeDef)) {
possibleTypeNames = this.context
.getSchema()
.getPossibleTypes(typeDef)
.map((t) => t.name)
} else {
possibleTypeNames = [typeDef.name]
}
// Collect complexities for all possible types individually
const selectionSetComplexities: ComplexityMap = node.selectionSet.selections.reduce(
(
complexities: ComplexityMap,
childNode: FieldNode | FragmentSpreadNode | InlineFragmentNode,
): ComplexityMap => {
// let nodeComplexity = 0;
let innerComplexities = complexities
let includeNode = true
let skipNode = false
for (const directive of childNode.directives ?? []) {
const directiveName = directive.name.value
switch (directiveName) {
case 'include': {
const values = getDirectiveValues(
this.includeDirectiveDef,
childNode,
this.variableValues || {},
)
if (typeof values.if === 'boolean') {
includeNode = values.if
}
break
}
case 'skip': {
const values = getDirectiveValues(
this.skipDirectiveDef,
childNode,
this.variableValues || {},
)
if (typeof values.if === 'boolean') {
skipNode = values.if
}
break
}
}
}
if (!includeNode || skipNode) {
return complexities
}
switch (childNode.kind) {
case Kind.FIELD: {
const field = fields[childNode.name.value]
// Invalid field, should be caught by other validation rules
if (!field) {
break
}
const fieldType = getNamedType(field.type)
// Get arguments
let args: { [key: string]: any }
try {
args = getArgumentValues(field, childNode, this.variableValues || {})
} catch (e) {
this.context.reportError(e)
return complexities
}
// Check if we have child complexity
let childComplexity = 0
if (isCompositeType(fieldType)) {
childComplexity = this.nodeComplexity(childNode, fieldType)
}
// Run estimators one after another and return first valid complexity
// score
const estimatorArgs: ComplexityEstimatorArgs = {
type: typeDef,
args,
childComplexity,
context: this.requestContext,
field,
node: childNode,
}
const validScore = this.estimators.find((estimator) => {
const tmpComplexity = estimator(estimatorArgs)
if (typeof tmpComplexity === 'number' && !isNaN(tmpComplexity)) {
innerComplexities = addComplexities(
tmpComplexity,
complexities,
possibleTypeNames,
)
return true
}
return false
})
if (!validScore) {
this.context.reportError(
new GraphQLError(
`No complexity could be calculated for field ${typeDef.name}.${field.name}. ` +
'At least one complexity estimator has to return a complexity score.',
),
)
return complexities
}
break
}
case Kind.FRAGMENT_SPREAD: {
const fragment = this.context.getFragment(childNode.name.value)
// Unknown fragment, should be caught by other validation rules
if (!fragment) {
break
}
const fragmentType = this.context
.getSchema()
.getType(fragment.typeCondition.name.value)
// Invalid fragment type, ignore. Should be caught by other validation rules
if (!isCompositeType(fragmentType)) {
break
}
const nodeComplexity = this.nodeComplexity(fragment, fragmentType)
if (isAbstractType(fragmentType)) {
// Add fragment complexity for all possible types
innerComplexities = addComplexities(
nodeComplexity,
complexities,
this.context
.getSchema()
.getPossibleTypes(fragmentType)
.map((t) => t.name),
)
} else {
// Add complexity for object type
innerComplexities = addComplexities(nodeComplexity, complexities, [
fragmentType.name,
])
}
break
}
case Kind.INLINE_FRAGMENT: {
let inlineFragmentType: GraphQLNamedType = typeDef
if (childNode.typeCondition && childNode.typeCondition.name) {
inlineFragmentType = this.context
.getSchema()
.getType(childNode.typeCondition.name.value)
if (!isCompositeType(inlineFragmentType)) {
break
}
}
const nodeComplexity = this.nodeComplexity(childNode, inlineFragmentType)
if (isAbstractType(inlineFragmentType)) {
// Add fragment complexity for all possible types
innerComplexities = addComplexities(
nodeComplexity,
complexities,
this.context
.getSchema()
.getPossibleTypes(inlineFragmentType)
.map((t) => t.name),
)
} else {
// Add complexity for object type
innerComplexities = addComplexities(nodeComplexity, complexities, [
inlineFragmentType.name,
])
}
break
}
default: {
innerComplexities = addComplexities(
this.nodeComplexity(childNode, typeDef),
complexities,
possibleTypeNames,
)
break
}
}
return innerComplexities
},
{},
)
// Only return max complexity of all possible types
if (!selectionSetComplexities) {
return NaN
}
return Math.max(...Object.values(selectionSetComplexities), 0)
}
return 0
}
onOperationDefinitionEnter(operation: OperationDefinitionNode): void {
if (
typeof this.options.operationName === 'string' &&
this.options.operationName !== operation.name.value
) {
return
}
// Get variable values from variables that are passed from options, merged
// with default values defined in the operation
const { coerced, errors } = getVariableValues(
this.context.getSchema(),
// We have to create a new array here because input argument is not readonly in graphql ~14.6.0
operation.variableDefinitions ? [...operation.variableDefinitions] : [],
this.options.variables ?? {},
)
if (errors && errors.length) {
// We have input validation errors, report errors and abort
errors.forEach((error) => this.context.reportError(error))
return
}
this.variableValues = coerced
switch (operation.operation) {
case 'query':
this.complexity += this.nodeComplexity(operation, this.context.getSchema().getQueryType())
break
case 'mutation':
this.complexity += this.nodeComplexity(
operation,
this.context.getSchema().getMutationType(),
)
break
case 'subscription':
this.complexity += this.nodeComplexity(
operation,
this.context.getSchema().getSubscriptionType(),
)
break
default:
throw new Error(
`Query complexity could not be calculated for operation of type ${operation.operation}`,
)
}
}
onOperationDefinitionLeave(operation: OperationDefinitionNode): GraphQLError | void {
if (
typeof this.options.operationName === 'string' &&
this.options.operationName !== operation.name.value
) {
return
}
if (this.options.onComplete) {
this.options.onComplete(this.complexity)
}
if (this.complexity > this.options.maximumComplexity) {
return this.context.reportError(this.createError())
}
}
}
/**
* Adds a complexity to the complexity map for all possible types
* @param complexity
* @param complexityMap
* @param possibleTypes
*/
function addComplexities(
complexity: number,
complexityMap: ComplexityMap,
possibleTypes: string[],
): ComplexityMap {
for (const type of possibleTypes) {
if (Object.prototype.hasOwnProperty.call(complexityMap, type)) {
complexityMap[type] += complexity
} else {
complexityMap[type] = complexity
}
}
return complexityMap
}

View File

@@ -0,0 +1,13 @@
import type { ValidationContext } from 'graphql'
import type { QueryComplexityOptions } from './QueryComplexity.js'
import QueryComplexity from './QueryComplexity.js'
export function createComplexityRule(
options: QueryComplexityOptions,
): (context: ValidationContext) => QueryComplexity {
return (context: ValidationContext): QueryComplexity => {
return new QueryComplexity(context, options)
}
}

View File

@@ -0,0 +1,14 @@
import type { ComplexityEstimator, ComplexityEstimatorArgs } from '../../QueryComplexity.js'
export const fieldExtensionsEstimator = (): ComplexityEstimator => {
return (args: ComplexityEstimatorArgs): number | void => {
if (args.field.extensions) {
// Calculate complexity score
if (typeof args.field.extensions.complexity === 'number') {
return args.childComplexity + args.field.extensions.complexity
} else if (typeof args.field.extensions.complexity === 'function') {
return args.field.extensions.complexity(args)
}
}
}
}

View File

@@ -0,0 +1,9 @@
import type { ComplexityEstimator, ComplexityEstimatorArgs } from '../../QueryComplexity.js'
export const simpleEstimator = (options?: { defaultComplexity?: number }): ComplexityEstimator => {
const defaultComplexity =
options && typeof options.defaultComplexity === 'number' ? options.defaultComplexity : 1
return (args: ComplexityEstimatorArgs): number | void => {
return defaultComplexity + args.childComplexity
}
}

View File

@@ -0,0 +1,3 @@
export { createComplexityRule } from './createComplexityRule.js'
export { fieldExtensionsEstimator } from './estimators/fieldExtensions/index.js'
export { simpleEstimator } from './estimators/simple/index.js'

View File

@@ -0,0 +1,73 @@
import { GraphQLScalarType } from 'graphql'
import { Kind, print } from 'graphql/language'
function identity(value) {
return value
}
function ensureObject(value) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new TypeError(`JSONObject cannot represent non-object value: ${value}`)
}
return value
}
function parseObject(typeName, ast, variables) {
const value = Object.create(null)
ast.fields.forEach((field) => {
// eslint-disable-next-line no-use-before-define
value[field.name.value] = parseLiteral(typeName, field.value, variables)
})
return value
}
function parseLiteral(typeName, ast, variables) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value)
case Kind.OBJECT:
return parseObject(typeName, ast, variables)
case Kind.LIST:
return ast.values.map((n) => parseLiteral(typeName, n, variables))
case Kind.NULL:
return null
case Kind.VARIABLE:
return variables ? variables[ast.name.value] : undefined
default:
throw new TypeError(`${typeName} cannot represent value: ${print(ast)}`)
}
}
// This named export is intended for users of CommonJS. Users of ES modules
// should instead use the default export.
export const GraphQLJSON = new GraphQLScalarType({
name: 'JSON',
description:
'The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
parseLiteral: (ast, variables) => parseLiteral('JSON', ast, variables),
parseValue: identity,
serialize: identity,
specifiedByURL: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
})
export const GraphQLJSONObject = new GraphQLScalarType({
name: 'JSONObject',
description:
'The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
parseLiteral: (ast, variables) => {
if (ast.kind !== Kind.OBJECT) {
throw new TypeError(`JSONObject cannot represent non-object value: ${print(ast)}`)
}
return parseObject('JSONObject', ast, variables)
},
parseValue: ensureObject,
serialize: ensureObject,
specifiedByURL: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
})

View File

@@ -0,0 +1,41 @@
import type { PayloadRequest, Where } from 'payload/types'
import type { Collection } from 'payload/types'
import { countOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'
import type { Context } from '../types.js'
export type Resolver = (
_: unknown,
args: {
data: Record<string, unknown>
locale?: string
where?: Where
},
context: {
req: PayloadRequest
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Promise<{ totalDocs: number }>
export default function countResolver(collection: Collection): Resolver {
return async function resolver(_, args, context: Context) {
let { req } = context
const locale = req.locale
const fallbackLocale = req.fallbackLocale
req = isolateObjectProperty(req, 'locale')
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = fallbackLocale
const options = {
collection,
req: isolateObjectProperty(req, 'transactionID'),
where: args.where,
}
const results = await countOperation(options)
return results
}
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-use-before-define */
import type { GraphQLInputFieldConfig, GraphQLScalarType, GraphQLType } from 'graphql'
import type { GraphQLInfo } from 'payload/config'
import type {
@@ -37,11 +36,11 @@ import {
GraphQLNonNull,
GraphQLString,
} from 'graphql'
import { GraphQLJSON } from 'graphql-type-json'
import { fieldAffectsData, optionIsObject, tabHasName } from 'payload/types'
import { toWords } from 'payload/utilities'
import { flattenTopLevelFields } from 'payload/utilities'
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
import combineParentName from '../utilities/combineParentName.js'
import formatName from '../utilities/formatName.js'
import { groupOrTabHasRequiredSubfield } from '../utilities/groupOrTabHasRequiredSubfield.js'

View File

@@ -41,13 +41,12 @@ import {
GraphQLUnionType,
} from 'graphql'
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
/* eslint-disable no-use-before-define */
import { GraphQLJSON } from 'graphql-type-json'
import { tabHasName } from 'payload/types'
import { toWords } from 'payload/utilities'
import type { Context } from '../resolvers/types.js'
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
import combineParentName from '../utilities/combineParentName.js'
import formatName from '../utilities/formatName.js'
import formatOptions from '../utilities/formatOptions.js'
@@ -301,7 +300,7 @@ function buildObjectType({
value: {
type: new GraphQLUnionType({
name: relationshipName,
async resolveType(data, { req }) {
resolveType(data, { req }) {
return graphqlResult.collections[data.collection].graphQL.type.name
},
types,
@@ -470,19 +469,24 @@ function buildObjectType({
// is run here again, with the provided depth.
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
if (editor?.populationPromise) {
await editor?.populationPromise({
if (editor?.populationPromises) {
const fieldPromises = []
const populationPromises = []
editor?.populationPromises({
context,
depth,
field,
fieldPromises,
findMany: false,
flattenLocales: false,
overrideAccess: false,
populationPromises: [],
populationPromises,
req: context.req,
showHiddenFields: false,
siblingDoc: parent,
})
await Promise.all(fieldPromises)
await Promise.all(populationPromises)
}
return parent[field.name]

View File

@@ -8,9 +8,9 @@ import type {
} from 'payload/types'
import { GraphQLBoolean, GraphQLNonNull, GraphQLObjectType } from 'graphql'
import { GraphQLJSONObject } from 'graphql-type-json'
import { toWords } from 'payload/utilities'
import { GraphQLJSONObject } from '../packages/graphql-type-json/index.js'
import formatName from '../utilities/formatName.js'
type OperationType = 'create' | 'delete' | 'read' | 'readVersions' | 'unlock' | 'update'

View File

@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import type { Field, FieldAffectingData } from 'payload/types'
/* eslint-disable no-use-before-define */
import { GraphQLInputObjectType, GraphQLList } from 'graphql'
import { fieldAffectsData, fieldHasSubFields, fieldIsPresentationalOnly } from 'payload/types'
import { flattenTopLevelFields } from 'payload/utilities'

View File

@@ -21,16 +21,13 @@ import type {
} from 'payload/types'
import { GraphQLEnumType, GraphQLInputObjectType } from 'graphql'
import GraphQLJSONImport from 'graphql-type-json'
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
import combineParentName from '../utilities/combineParentName.js'
import formatName from '../utilities/formatName.js'
import recursivelyBuildNestedPaths from './recursivelyBuildNestedPaths.js'
import { withOperators } from './withOperators.js'
const GraphQLJSON = (GraphQLJSONImport ||
GraphQLJSONImport.default) as unknown as typeof GraphQLJSONImport.default
type Args = {
nestedFieldName?: string
parentName: string

View File

@@ -24,6 +24,7 @@ import refresh from '../resolvers/auth/refresh.js'
import resetPassword from '../resolvers/auth/resetPassword.js'
import unlock from '../resolvers/auth/unlock.js'
import verifyEmail from '../resolvers/auth/verifyEmail.js'
import countResolver from '../resolvers/collections/count.js'
import createResolver from '../resolvers/collections/create.js'
import getDeleteResolver from '../resolvers/collections/delete.js'
import { docAccessResolver } from '../resolvers/collections/docAccess.js'
@@ -183,6 +184,25 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
resolve: findResolver(collection),
}
graphqlResult.Query.fields[`count${pluralName}`] = {
type: new GraphQLObjectType({
name: `count${pluralName}`,
fields: {
totalDocs: { type: GraphQLInt },
},
}),
args: {
draft: { type: GraphQLBoolean },
where: { type: collection.graphQL.whereInputType },
...(config.localization
? {
locale: { type: graphqlResult.types.localeInputType },
}
: {}),
},
resolve: countResolver(collection),
}
graphqlResult.Query.fields[`docAccess${singularName}`] = {
type: buildPolicyType({
type: 'collection',

View File

@@ -11,9 +11,9 @@ import {
GraphQLString,
} from 'graphql'
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
import { GraphQLJSON } from 'graphql-type-json'
import { optionIsObject } from 'payload/types'
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
import combineParentName from '../utilities/combineParentName.js'
import formatName from '../utilities/formatName.js'
import operators from './operators.js'

View File

@@ -6,7 +6,6 @@
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"allowImportingTsExtensions": true
},
"exclude": [
"dist",

View File

@@ -39,7 +39,13 @@
}
},
"publishConfig": {
"exports": null,
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -0,0 +1,37 @@
/** @type {import('prettier').Config} */
module.exports = {
extends: ['@payloadcms'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
{
files: ['package.json', 'tsconfig.json'],
rules: {
'perfectionist/sort-array-includes': 'off',
'perfectionist/sort-astro-attributes': 'off',
'perfectionist/sort-classes': 'off',
'perfectionist/sort-enums': 'off',
'perfectionist/sort-exports': 'off',
'perfectionist/sort-imports': 'off',
'perfectionist/sort-interfaces': 'off',
'perfectionist/sort-jsx-props': 'off',
'perfectionist/sort-keys': 'off',
'perfectionist/sort-maps': 'off',
'perfectionist/sort-named-exports': 'off',
'perfectionist/sort-named-imports': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
'perfectionist/sort-svelte-attributes': 'off',
'perfectionist/sort-union-types': 'off',
'perfectionist/sort-vue-attributes': 'off',
},
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
}

View File

@@ -0,0 +1,10 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": "inline",
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "commonjs"
}
}

View File

@@ -0,0 +1,56 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "0.1.0",
"description": "The official live preview Vue SDK for Payload",
"repository": {
"type": "git",
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/live-preview-vue"
},
"license": "MIT",
"homepage": "https://payloadcms.com",
"author": "Payload CMS, Inc.",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"@payloadcms/live-preview": "workspace:^0.x"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"vue": "^3.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
"vue": "^3.0.0"
},
"exports": {
".": {
"default": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"files": [
"dist"
]
}

View File

@@ -0,0 +1,58 @@
import type { Ref } from 'vue'
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview'
import { onMounted, onUnmounted, ref } from 'vue'
/**
* Vue composable to implement Payload CMS Live Preview.
*
* {@link https://payloadcms.com/docs/live-preview/frontend View the documentation}
*/
export const useLivePreview = <T>(props: {
apiRoute?: string
depth?: number
initialData: T
serverURL: string
}): {
data: Ref<T>
isLoading: Ref<boolean>
} => {
const { apiRoute, depth, initialData, serverURL } = props
const data = ref(initialData) as Ref<T>
const isLoading = ref(true)
const hasSentReadyMessage = ref(false)
const onChange = (mergedData: T) => {
data.value = mergedData
isLoading.value = false
}
let subscription: (event: MessageEvent) => void
onMounted(() => {
subscription = subscribe({
apiRoute,
callback: onChange,
depth,
initialData,
serverURL,
})
if (!hasSentReadyMessage.value) {
hasSentReadyMessage.value = true
ready({
serverURL,
})
}
})
onUnmounted(() => {
unsubscribe(subscription)
})
return {
data,
isLoading,
}
}

View File

@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true, // Make sure typescript knows that this module depends on their references
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"jsx": "react"
},
"exclude": [
"dist",
"build",
"tests",
"test",
"node_modules",
".eslintrc.js",
"src/**/*.spec.js",
"src/**/*.spec.jsx",
"src/**/*.spec.ts",
"src/**/*.spec.tsx"
],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
"references": [{ "path": "../payload" }] // db-mongodb depends on payload
}

View File

@@ -32,7 +32,13 @@
}
},
"publishConfig": {
"exports": null,
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.5",
"version": "3.0.0-beta.15",
"main": "./src/index.js",
"types": "./src/index.js",
"type": "module",
@@ -46,6 +46,7 @@
"@types/ws": "^8.5.10",
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"file-type": "16.5.4",
"mini-css-extract-plugin": "1.6.2",
"payload": "workspace:*",
"postcss-loader": "^8.1.1",
@@ -65,20 +66,21 @@
"@types/busboy": "^1.5.3",
"busboy": "^1.6.0",
"deep-equal": "2.2.2",
"graphql": "16.8.1",
"graphql-http": "^1.22.0",
"graphql-playground-html": "1.6.30",
"path-to-regexp": "^6.2.1",
"qs": "6.11.2",
"react-diff-viewer-continued": "3.2.6",
"react-toastify": "8.2.0",
"sass": "^1.71.1",
"graphql-http": "^1.22.0",
"ws": "^8.16.0"
},
"peerDependencies": {
"file-type": "16.5.4",
"http-status": "1.6.2",
"next": "14.2.0-canary.23",
"payload": "workspace:*"
"next": "^14.3.0-canary.7",
"payload": "workspace:*",
"graphql": "^16.8.1"
},
"publishConfig": {
"main": "./dist/index.js",
@@ -102,7 +104,7 @@
"registry": "https://registry.npmjs.org/"
},
"engines": {
"node": ">=18.17.0"
"node": ">=18.20.2"
},
"files": [
"dist"

View File

@@ -3,6 +3,7 @@ export { GRAPHQL_PLAYGROUND_GET, GRAPHQL_POST } from '../routes/graphql/index.js
export {
DELETE as REST_DELETE,
GET as REST_GET,
OPTIONS as REST_OPTIONS,
PATCH as REST_PATCH,
POST as REST_POST,
} from '../routes/rest/index.js'

View File

@@ -1,2 +1,7 @@
export { addDataAndFileToRequest } from '../utilities/addDataAndFileToRequest.js'
export { addLocalesToRequest } from '../utilities/addLocalesToRequest.js'
export { traverseFields } from '../utilities/buildFieldSchemaMap/traverseFields.js'
export { createPayloadRequest } from '../utilities/createPayloadRequest.js'
export { getNextRequestI18n } from '../utilities/getNextRequestI18n.js'
export { getPayloadHMR } from '../utilities/getPayloadHMR.js'
export { getPayloadHMR, reload } from '../utilities/getPayloadHMR.js'
export { headersWithCors } from '../utilities/headersWithCors.js'

View File

@@ -22,6 +22,16 @@ export const metadata = {
title: 'Next.js',
}
import { Merriweather } from 'next/font/google'
const merriweather = Merriweather({
display: 'swap',
style: ['normal', 'italic'],
subsets: ['latin'],
variable: '--font-serif',
weight: ['400', '900'],
})
export const RootLayout = async ({
children,
config: configPromise,
@@ -82,7 +92,7 @@ export const RootLayout = async ({
})
return (
<html dir={dir} lang={languageCode}>
<html className={merriweather.variable} dir={dir} lang={languageCode}>
<body>
<RootProvider
componentMap={componentMap}

View File

@@ -77,7 +77,7 @@ export const tempFileHandler: Handler = (options, fieldname, filename) => {
}
export const memHandler: Handler = (options, fieldname, filename) => {
const buffers = []
const buffers: Buffer[] = []
const hash = crypto.createHash('md5')
let fileSize = 0
let completed = false

View File

@@ -1,4 +1,4 @@
const ACCEPTABLE_CONTENT_TYPE = /^multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i
const ACCEPTABLE_CONTENT_TYPE = /multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i
const UNACCEPTABLE_METHODS = new Set(['GET', 'HEAD', 'DELETE', 'OPTIONS', 'CONNECT', 'TRACE'])
const hasBody = (req: Request): boolean => {

View File

@@ -1,4 +1,5 @@
import Busboy from 'busboy'
import httpStatus from 'http-status'
import { APIError } from 'payload/errors'
import type { NextFileUploadOptions, NextFileUploadResponse } from './index.js'
@@ -17,6 +18,17 @@ type ProcessMultipart = (args: {
}) => Promise<NextFileUploadResponse>
export const processMultipart: ProcessMultipart = async ({ options, request }) => {
let parsingRequest = true
let fileCount = 0
let filesCompleted = 0
let allFilesHaveResolved: (value?: unknown) => void
let failedResolvingFiles: (err: Error) => void
const allFilesComplete = new Promise((res, rej) => {
allFilesHaveResolved = res
failedResolvingFiles = rej
})
const result: NextFileUploadResponse = {
fields: undefined,
files: undefined,
@@ -36,6 +48,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
// Build req.files fields
busboy.on('file', (field, file, info) => {
fileCount += 1
// Parse file name(cutting huge names, decoding, etc..).
const { encoding, filename: name, mimeType: mime } = info
const filename = parseFileName(options, name)
@@ -73,7 +86,9 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
debugLog(options, `Aborting upload because of size limit ${field}->${filename}.`)
cleanup()
parsingRequest = false
throw new APIError(options.responseOnLimit, 413, { size: getFileSize() })
throw new APIError(options.responseOnLimit, httpStatus.REQUEST_ENTITY_TOO_LARGE, {
size: getFileSize(),
})
}
})
@@ -95,6 +110,8 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
return debugLog(options, `Don't add file instance if original name and size are empty`)
}
filesCompleted += 1
result.files = buildFields(
result.files,
field,
@@ -117,19 +134,25 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
request[waitFlushProperty] = []
}
request[waitFlushProperty].push(writePromise)
if (filesCompleted === fileCount) {
allFilesHaveResolved()
}
})
file.on('error', (err) => {
uploadTimer.clear()
debugLog(options, `File Error: ${err.message}`)
cleanup()
failedResolvingFiles(err)
})
// Start upload process.
debugLog(options, `New upload started ${field}->${filename}, bytes:${getFileSize()}`)
uploadTimer.set()
})
busboy.on('finish', () => {
busboy.on('finish', async () => {
debugLog(options, `Busboy finished parsing request.`)
if (options.parseNested) {
result.fields = processNested(result.fields)
@@ -137,20 +160,27 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
}
if (request[waitFlushProperty]) {
Promise.all(request[waitFlushProperty]).then(() => {
delete request[waitFlushProperty]
})
try {
await Promise.all(request[waitFlushProperty]).then(() => {
delete request[waitFlushProperty]
})
} catch (err) {
debugLog(options, `Error waiting for file write promises: ${err}`)
}
}
return result
})
busboy.on('error', (err) => {
debugLog(options, `Busboy error`)
parsingRequest = false
throw new APIError('Busboy error parsing multipart request', 500)
throw new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST)
})
const reader = request.body.getReader()
// Start parsing request
while (parsingRequest) {
const { done, value } = await reader.read()
@@ -163,5 +193,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
}
}
if (fileCount !== 0) await allFilesComplete
return result
}

View File

@@ -19,7 +19,7 @@ let tempCounter = 0
export const debugLog = (options: NextFileUploadOptions, msg: string) => {
const opts = options || {}
if (!opts.debug) return false
console.log(`Express-file-upload: ${msg}`) // eslint-disable-line
console.log(`Next-file-upload: ${msg}`) // eslint-disable-line
return true
}
@@ -287,8 +287,9 @@ export const parseFileName: ParseFileName = (opts, fileName) => {
? opts.safeFileNames
: SAFE_FILE_NAME_REGEX
// Parse file name extension.
let { name, extension } = parseFileNameExtension(opts.preserveExtension, parsedName)
if (extension.length) extension = '.' + extension.replace(nameRegex, '')
const parsedFileName = parseFileNameExtension(opts.preserveExtension, parsedName)
if (parsedFileName.extension.length)
parsedFileName.extension = '.' + parsedFileName.extension.replace(nameRegex, '')
return name.replace(nameRegex, '').concat(extension)
return parsedFileName.name.replace(nameRegex, '').concat(parsedFileName.extension)
}

View File

@@ -1,18 +1,20 @@
import type { GraphQLFormattedError } from 'graphql'
import type { GraphQLError } from 'graphql'
import type { GraphQLError, GraphQLFormattedError } from 'graphql'
import type { CollectionAfterErrorHook, Payload, SanitizedConfig } from 'payload/types'
import { configToSchema } from '@payloadcms/graphql'
import { createHandler } from 'graphql-http/lib/use/fetch'
import httpStatus from 'http-status'
import { addLocalesToRequest } from '../../utilities/addLocalesToRequest.js'
import { createPayloadRequest } from '../../utilities/createPayloadRequest.js'
import { headersWithCors } from '../../utilities/headersWithCors.js'
const handleError = async (
payload: Payload,
err: any,
debug: boolean,
afterErrorHook: CollectionAfterErrorHook,
// eslint-disable-next-line @typescript-eslint/require-await
): Promise<GraphQLFormattedError> => {
const status = err.originalError.status || httpStatus.INTERNAL_SERVER_ERROR
let errorMessage = err.message
@@ -37,7 +39,7 @@ const handleError = async (
}
if (afterErrorHook) {
;({ response } = (await afterErrorHook(err, response, null, null)) || { response })
;({ response } = afterErrorHook(err, response, null, null) || { response })
}
return response
@@ -60,9 +62,10 @@ export const getGraphql = async (config: Promise<SanitizedConfig> | SanitizedCon
}
if (!cached.promise) {
// eslint-disable-next-line no-async-promise-executor
cached.promise = new Promise(async (resolve) => {
const resolvedConfig = await config
const schema = await configToSchema(resolvedConfig)
const schema = configToSchema(resolvedConfig)
resolve(schema)
})
}
@@ -84,6 +87,9 @@ export const POST =
config,
request,
})
addLocalesToRequest({ request: req })
const { schema, validationRules } = await getGraphql(config)
const { payload } = req
@@ -118,13 +124,17 @@ export const POST =
validationRules: (request, args, defaultRules) => defaultRules.concat(validationRules(args)),
})(originalRequest)
const resHeaders = new Headers(apiResponse.headers)
const resHeaders = headersWithCors({
headers: new Headers(apiResponse.headers),
req,
})
for (const key in headers) {
resHeaders.append(key, headers[key])
}
return new Response(apiResponse.body, {
headers: new Headers(resHeaders),
headers: resHeaders,
status: apiResponse.status,
})
}

View File

@@ -2,6 +2,7 @@ import type { SanitizedConfig } from 'payload/types'
import { renderPlaygroundPage } from 'graphql-playground-html'
import { addLocalesToRequest } from '../../utilities/addLocalesToRequest.js'
import { createPayloadRequest } from '../../utilities/createPayloadRequest.js'
export const GET = (config: Promise<SanitizedConfig>) => async (request: Request) => {
@@ -10,6 +11,8 @@ export const GET = (config: Promise<SanitizedConfig>) => async (request: Request
request,
})
addLocalesToRequest({ request: req })
if (
(!req.payload.config.graphQL.disable &&
!req.payload.config.graphQL.disablePlaygroundInProduction &&

View File

@@ -3,12 +3,18 @@ import { accessOperation } from 'payload/operations'
import type { BaseRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const access: BaseRouteHandler = async ({ req }) => {
const results = await accessOperation({
req,
})
return Response.json(results, {
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
})
}

View File

@@ -3,7 +3,10 @@ import { forgotPasswordOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const forgotPassword: CollectionRouteHandler = async ({ collection, req }) => {
const { t } = req
await forgotPasswordOperation({
collection,
data: {
@@ -16,10 +19,13 @@ export const forgotPassword: CollectionRouteHandler = async ({ collection, req }
return Response.json(
{
// TODO(translate)
message: 'Success',
message: t('general:success'),
},
{
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
},
)

View File

@@ -2,11 +2,21 @@ import { initOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const init: CollectionRouteHandler = async ({ collection, req }) => {
const initialized = await initOperation({
collection: collection.config.slug,
req,
})
return Response.json({ initialized })
return Response.json(
{ initialized },
{
headers: headersWithCors({
headers: new Headers(),
req,
}),
},
)
}

View File

@@ -5,8 +5,10 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const login: CollectionRouteHandler = async ({ collection, req }) => {
const { searchParams } = req
const { searchParams, t } = req
const depth = searchParams.get('depth')
const result = await loginOperation({
@@ -31,13 +33,15 @@ export const login: CollectionRouteHandler = async ({ collection, req }) => {
return Response.json(
{
// TODO(translate)
message: 'Auth Passed',
message: t('authentication:passed'),
...result,
},
{
headers: new Headers({
'Set-Cookie': cookie,
headers: headersWithCors({
headers: new Headers({
'Set-Cookie': cookie,
}),
req,
}),
status: httpStatus.OK,
},

View File

@@ -4,18 +4,27 @@ import { logoutOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const logout: CollectionRouteHandler = async ({ collection, req }) => {
const result = logoutOperation({
const { t } = req
const result = await logoutOperation({
collection,
req,
})
const headers = headersWithCors({
headers: new Headers(),
req,
})
if (!result) {
return Response.json(
{
message: 'Logout failed.',
message: t('error:logoutFailed'),
},
{
headers,
status: httpStatus.BAD_REQUEST,
},
)
@@ -26,15 +35,14 @@ export const logout: CollectionRouteHandler = async ({ collection, req }) => {
payload: req.payload,
})
headers.set('Set-Cookie', expiredCookie)
return Response.json(
{
// TODO(translate)
message: 'Logout successful.',
message: t('authentication:logoutSuccessful'),
},
{
headers: new Headers({
'Set-Cookie': expiredCookie,
}),
headers,
status: httpStatus.OK,
},
)

View File

@@ -4,6 +4,8 @@ import { meOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const me: CollectionRouteHandler = async ({ collection, req }) => {
const currentToken = extractJWT(req)
@@ -23,6 +25,10 @@ export const me: CollectionRouteHandler = async ({ collection, req }) => {
message: req.t('authentication:account'),
},
{
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
},
)

View File

@@ -5,16 +5,24 @@ import { refreshOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const refresh: CollectionRouteHandler = async ({ collection, req }) => {
const { t } = req
const token = typeof req.data?.token === 'string' ? req.data.token : extractJWT(req)
const headers = headersWithCors({
headers: new Headers(),
req,
})
if (!token) {
return Response.json(
{
// TODO(translate)
message: 'Token not provided.',
message: t('error:tokenNotProvided'),
},
{
headers,
status: httpStatus.UNAUTHORIZED,
},
)
@@ -36,16 +44,15 @@ export const refresh: CollectionRouteHandler = async ({ collection, req }) => {
delete result.refreshedToken
}
headers.set('Set-Cookie', cookie)
return Response.json(
{
// TODO(translate)
message: 'Token refresh successful',
message: t('authentication:tokenRefreshSuccessful'),
...result,
},
{
headers: new Headers({
'Set-Cookie': cookie,
}),
headers,
status: httpStatus.OK,
},
)

View File

@@ -1,11 +1,23 @@
import httpStatus from 'http-status'
import { generatePayloadCookie } from 'payload/auth'
import { ValidationError } from 'payload/errors'
import { registerFirstUserOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const registerFirstUser: CollectionRouteHandler = async ({ collection, req }) => {
const data = req.data
const { data, t } = req
if (data?.password !== data['confirm-password']) {
throw new ValidationError([
{
field: 'confirm-password',
message: req.t('Password and confirm password fields do not match.'),
},
])
}
const result = await registerFirstUserOperation({
collection,
@@ -26,14 +38,16 @@ export const registerFirstUser: CollectionRouteHandler = async ({ collection, re
return Response.json(
{
exp: result.exp,
// TODO(translate)
message: 'Successfully registered first user.',
message: t('authentication:successfullyRegisteredFirstUser'),
token: result.token,
user: result.user,
},
{
headers: new Headers({
'Set-Cookie': cookie,
headers: headersWithCors({
headers: new Headers({
'Set-Cookie': cookie,
}),
req,
}),
status: httpStatus.OK,
},

View File

@@ -4,8 +4,10 @@ import { resetPasswordOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const resetPassword: CollectionRouteHandler = async ({ collection, req }) => {
const { searchParams } = req
const { searchParams, t } = req
const depth = searchParams.get('depth')
const result = await resetPasswordOperation({
@@ -30,13 +32,15 @@ export const resetPassword: CollectionRouteHandler = async ({ collection, req })
return Response.json(
{
// TODO(translate)
message: 'Password reset successfully.',
message: t('authentication:passwordResetSuccessfully'),
...result,
},
{
headers: new Headers({
'Set-Cookie': cookie,
headers: headersWithCors({
headers: new Headers({
'Set-Cookie': cookie,
}),
req,
}),
status: httpStatus.OK,
},

View File

@@ -3,7 +3,11 @@ import { unlockOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const unlock: CollectionRouteHandler = async ({ collection, req }) => {
const { t } = req
await unlockOperation({
collection,
data: { email: req.data.email as string },
@@ -12,10 +16,13 @@ export const unlock: CollectionRouteHandler = async ({ collection, req }) => {
return Response.json(
{
// TODO(translate)
message: 'Success',
message: t('general:success'),
},
{
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
},
)

View File

@@ -3,7 +3,10 @@ import { verifyEmailOperation } from 'payload/operations'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const verifyEmail: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
const { t } = req
await verifyEmailOperation({
collection,
req,
@@ -12,10 +15,13 @@ export const verifyEmail: CollectionRouteHandlerWithID = async ({ id, collection
return Response.json(
{
// TODO(translate)
message: 'Email verified successfully.',
message: t('authentication:emailVerified'),
},
{
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
},
)

View File

@@ -8,6 +8,7 @@ import httpStatus from 'http-status'
import type { FieldSchemaMap } from '../../utilities/buildFieldSchemaMap/types.js'
import { buildFieldSchemaMap } from '../../utilities/buildFieldSchemaMap/index.js'
import { headersWithCors } from '../../utilities/headersWithCors.js'
let cached = global._payload_fieldSchemaMap
@@ -27,6 +28,11 @@ export const getFieldSchemaMap = (req: PayloadRequest): FieldSchemaMap => {
}
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
const headers = headersWithCors({
headers: new Headers(),
req,
})
try {
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData
@@ -44,17 +50,20 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
if (!canAccessAdmin) {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
} else {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
@@ -84,6 +93,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
message: 'Could not find field schema for given path',
},
{
headers,
status: httpStatus.BAD_REQUEST,
},
)
@@ -187,7 +197,18 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
req,
})
// Maintain form state of file
if (
collectionSlug &&
req.payload.collections[collectionSlug]?.config?.upload &&
formState &&
formState.file
) {
result.file = formState.file
}
return Response.json(result, {
headers,
status: httpStatus.OK,
})
} catch (err) {
@@ -196,6 +217,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
message: 'There was an error building form state',
},
{
headers,
status: httpStatus.BAD_REQUEST,
},
)

View File

@@ -0,0 +1,22 @@
import type { Where } from 'payload/types'
import httpStatus from 'http-status'
import { countOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
export const count: CollectionRouteHandler = async ({ collection, req }) => {
const { where } = req.query as {
where?: Where
}
const result = await countOperation({
collection,
req,
where,
})
return Response.json(result, {
status: httpStatus.OK,
})
}

View File

@@ -5,6 +5,8 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const create: CollectionRouteHandler = async ({ collection, req }) => {
const { searchParams } = req
const autosave = searchParams.get('autosave') === 'true'
@@ -28,6 +30,10 @@ export const create: CollectionRouteHandler = async ({ collection, req }) => {
}),
},
{
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.CREATED,
},
)

View File

@@ -7,6 +7,8 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, where } = req.query as {
depth?: string
@@ -20,6 +22,11 @@ export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) =>
where,
})
const headers = headersWithCors({
headers: new Headers(),
req,
})
if (result.errors.length === 0) {
const message = req.t('general:deletedCountSuccessfully', {
count: result.docs.length,
@@ -35,6 +42,7 @@ export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) =>
message,
},
{
headers,
status: httpStatus.OK,
},
)
@@ -54,6 +62,7 @@ export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) =>
message,
},
{
headers,
status: httpStatus.BAD_REQUEST,
},
)

View File

@@ -4,6 +4,7 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const deleteByID: CollectionRouteHandlerWithID = async ({
@@ -27,12 +28,18 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({
req,
})
const headers = headersWithCors({
headers: new Headers(),
req,
})
if (!doc) {
return Response.json(
{
message: req.t('general:notFound'),
},
{
headers,
status: httpStatus.NOT_FOUND,
},
)
@@ -44,6 +51,7 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({
message: req.t('general:deletedSuccessfully'),
},
{
headers,
status: httpStatus.OK,
},
)

View File

@@ -3,6 +3,8 @@ import { docAccessOperation } from 'payload/operations'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const docAccess: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
const result = await docAccessOperation({
id,
@@ -11,6 +13,10 @@ export const docAccess: CollectionRouteHandlerWithID = async ({ id, collection,
})
return Response.json(result, {
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
})
}

View File

@@ -5,6 +5,7 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const duplicate: CollectionRouteHandlerWithID = async ({
@@ -41,6 +42,10 @@ export const duplicate: CollectionRouteHandlerWithID = async ({
message,
},
{
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
},
)

View File

@@ -6,6 +6,8 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const find: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, draft, limit, page, sort, where } = req.query as {
depth?: string
@@ -28,6 +30,10 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => {
})
return Response.json(result, {
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
})
}

View File

@@ -4,6 +4,7 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const findByID: CollectionRouteHandlerWithID = async ({
@@ -29,6 +30,10 @@ export const findByID: CollectionRouteHandlerWithID = async ({
})
return Response.json(result, {
headers: headersWithCors({
headers: new Headers(),
req,
}),
status: httpStatus.OK,
})
}

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