Compare commits

...

246 Commits

Author SHA1 Message Date
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
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
Elliot DeNolf
20ac2b86cf chore(release): v3.0.0-beta.5 [skip ci] 2024-04-11 15:11:14 -04:00
Elliot DeNolf
b88455166a fix: improve config finding (#5800) 2024-04-11 15:08:04 -04:00
Elliot DeNolf
9b86de1f9d fix(ui): scss paths 2024-04-11 15:06:27 -04:00
Elliot DeNolf
1393c72281 fix: improve config finding 2024-04-11 15:06:09 -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
Elliot DeNolf
1119cf3af9 chore(release): v3.0.0-beta.4 [skip ci] 2024-04-11 14:02:17 -04:00
Elliot DeNolf
216934145c fix: default baseUrl in loader if not set (#5798) 2024-04-11 13:52:35 -04:00
James Mikrut
40f952cac3 chore: type local API auth function and PayloadRequest (#5791) 2024-04-11 13:45:24 -04:00
Jacob Fletcher
330e4a7724 fix(next): safely handles missing json body in post requests 2024-04-11 13:42:21 -04:00
Paul Popus
e676503e02 update further types 2024-04-11 14:12:03 -03:00
James Mikrut
06233fbb2f fix: Generated ids for an array items are the same as global id if it's created (#5795) 2024-04-11 13:06:24 -04:00
Paul Popus
17298695b1 fix: provide request to the previewFunction and fix type 2024-04-11 14:01:22 -03:00
Jarrod Flesch
c1081ccfe2 chore(tests): flakey drawer, tab, navigation tests (#5792) 2024-04-11 12:57:19 -04:00
Ritsu
30da5a8643 fix: generated ids for array items the same as global id 2024-04-11 19:46:14 +03:00
Paul Popus
55bf5436e4 fix type issues 2024-04-11 13:09:39 -03:00
Elliot DeNolf
a4956dc649 fix(cpa): ast parse error handling (#5793) 2024-04-11 12:01:16 -04:00
James Mikrut
512b7bd429 fix: number ids were not sanitized to number in rest api (#5778) 2024-04-11 11:19:22 -04:00
Paul Popus
5119c51439 chore: type request too 2024-04-11 12:19:11 -03:00
James
f3e25f3277 more de-flake 2024-04-11 11:17:59 -04:00
Jacob Fletcher
1275c70187 feat(ui): provides payload as prop to all custom server components (#5775) 2024-04-11 11:16:15 -04:00
James
be69fc448d chore: de-flake 2024-04-11 10:59:26 -04:00
Paul Popus
bcccefe98e chore: add int test for local API login function 2024-04-11 11:56:52 -03:00
Paul Popus
2061f38d9e feat: add new type generation for the auth operation 2024-04-11 11:56:42 -03:00
Elliot DeNolf
d0869d9087 chore: unused file [skip ci] 2024-04-11 10:22:06 -04:00
Elliot DeNolf
1eabf316d6 chore(templates): add generate:types to blank 2024-04-10 22:30:56 -04:00
Elliot DeNolf
a0dd750a52 chore(cpa): add or operator to process.env.DATABASE_URI [skip ci] 2024-04-10 22:17:52 -04:00
Elliot DeNolf
2fc9885abc chore(scripts): add getPackageRegistryVersions script 2024-04-10 21:26:50 -04:00
Elliot DeNolf
14d683fb9a chore(scripts): update release script 2024-04-10 21:26:31 -04:00
Elliot DeNolf
e286519cb1 chore(release): v3.0.0-beta.3 [skip ci] 2024-04-10 20:46:22 -04:00
Elliot DeNolf
b1e78a3562 feat(cpa): improvements (#5783) 2024-04-10 20:35:19 -04:00
Elliot DeNolf
0bc103658a chore(cpa): improve move message 2024-04-10 20:29:57 -04:00
Elliot DeNolf
9037b9b4fa chore(cpa): remove 2.0 templates until updated 2024-04-10 20:25:20 -04:00
Elliot DeNolf
d194493e9a chore(cpa): update help 2024-04-10 20:24:51 -04:00
Elliot DeNolf
aa22344cdb fix(cpa): append to existing .env 2024-04-10 20:16:59 -04:00
Elliot DeNolf
736e7b822e chore(release): v3.0.0-beta.2 [skip ci] 2024-04-10 17:32:32 -04:00
Elliot DeNolf
2ebda95036 fix(next): proper named export of withPayload 2024-04-10 17:30:47 -04:00
Elliot DeNolf
d8783eaad4 chore(release): v3.0.0-beta.1 [skip ci] 2024-04-10 17:16:20 -04:00
Elliot DeNolf
cddb08de1a chore: ignore payload/i18n 2024-04-10 17:11:20 -04:00
James
d4e5d3df54 chore: fixes to unit tests 2024-04-10 17:05:44 -04:00
James
414b03ce74 chore: fix to unit tests 2024-04-10 17:03:37 -04:00
James
96dbab8834 chore: misc fixes 2024-04-10 16:58:08 -04:00
Elliot DeNolf
03a110a750 feat(next)!: cjs support (#5772) 2024-04-10 16:44:20 -04:00
Elliot DeNolf
6accc705be chore(next): cjs build 2024-04-10 16:32:36 -04:00
Elliot DeNolf
312dca003b feat(cpa): CJS next config AST parsing 2024-04-10 16:32:19 -04:00
James
4f9fdb6c14 fix: number ids were not sanitized to number in rest api 2024-04-10 16:01:28 -04:00
Jarrod Flesch
94af06466b chore: re-exports languages in payload (#5771) 2024-04-10 15:55:01 -04:00
Jacob Fletcher
7cf2686097 fix: optionally types req in auth operation (#5769) 2024-04-10 14:08:47 -04:00
Elliot DeNolf
f14883aa11 chore: update blank 3.0 template for withPayload change 2024-04-10 13:41:11 -04:00
Elliot DeNolf
9df8de2386 chore: adjust exports 2024-04-10 13:41:11 -04:00
James
8b2cf4705e chore: withPayload CJS 2024-04-10 13:41:10 -04:00
Jarrod Flesch
364e9832ac fix(next): ensures requested lang header is supported (#5765) 2024-04-10 13:05:56 -04:00
Jarrod Flesch
2deeb61f17 fix: locale switcher flakey test (#5761) 2024-04-10 13:05:30 -04:00
Patrik
14498e8a9c fix(ui): avoids getting and setting doc preferences when creating new (#5758) 2024-04-10 11:42:03 -04:00
Alessio Gravili
eb78022387 fix: undo changing baseBlockFields type to FieldWithRichTextRequiredEditor (#5767) 2024-04-10 11:38:27 -04:00
Elliot DeNolf
3677a59a78 fix(cpa): dependency tag (#5768) 2024-04-10 11:25:54 -04:00
Alessio Gravili
7c1c840a59 chore: increase admin e2e beforeAll timeout, as prebuild sometimes takes longer than the timeout 2024-04-10 11:22:17 -04:00
Alessio Gravili
a73eaf5d37 chore: fix test suite types, add LexicalBlock type 2024-04-10 11:07:01 -04:00
Alessio Gravili
68989a58a8 fix: undo changing baseBlockFields types to FieldWithRichTextRequiredEditor 2024-04-10 10:56:07 -04:00
Alessio Gravili
9841731ae7 feat: properly type withPayload (#5756) 2024-04-10 09:51:46 -04:00
Elliot DeNolf
ba7ac5d439 test: fix unit tests (#5760) 2024-04-09 23:08:20 -04:00
Elliot DeNolf
7c60772b26 ci: alpha -> beta branch push list 2024-04-09 23:07:09 -04:00
Alessio Gravili
1141a5d3af fix(richtext-lexical): do not render uploads extra fields drawer if no extra fields are provided (#5755) 2024-04-09 16:30:43 -04:00
Elliot DeNolf
6f74fd1f98 chore(release): v3.0.0-beta.0 [skip ci] 2024-04-09 15:00:39 -04:00
Alessio Gravili
75873bfcfa fix(richtext-lexical): catch errors that may occur during HTML generation (#5752) 2024-04-09 14:53:17 -04:00
Jarrod Flesch
1faf621f17 fix: persist locale when navigating (#5753) 2024-04-09 14:49:26 -04:00
Elliot DeNolf
1d1c73dfcc chore(release): v3.0.0-alpha.61 [skip ci] 2024-04-09 14:36:04 -04:00
Patrik
d057ce0a85 fix(next): removes global slug from collectionSlug prop in SetStepNav (#5744) 2024-04-09 14:26:09 -04:00
James Mikrut
0ce26d2c08 Feat/config i18n (#5735) 2024-04-09 14:13:17 -04:00
Alessio Gravili
abf285d713 chore: get dev:generate-types to work again (#5750) 2024-04-09 14:12:23 -04:00
James
c2ee8e3999 chore: de-flakes fields/index tests 2024-04-09 13:56:32 -04:00
James
167ba0c68f chore: adds back all tests 2024-04-09 13:50:54 -04:00
James
9ad1cbe920 chore: adjusts test snapshot logic to restore uploads properly 2024-04-09 13:33:42 -04:00
James
313ea52e3d chore: getRequestLanguage now defaults 2024-04-09 13:17:43 -04:00
James
3acfb7a83f chore: corrects imports 2024-04-09 12:43:36 -04:00
James
783dae2bbb Merge branch 'alpha' of github.com:payloadcms/payload into feat/config-i18n 2024-04-09 12:35:57 -04:00
Alessio Gravili
59681b211b fix(richtext-lexical): upload nodes weren't visible (#5746) 2024-04-09 12:32:24 -04:00
James
98438175cf chore: handles server errors 2024-04-09 12:31:25 -04:00
Alessio Gravili
af40302e5f fix(richtext-lexical): get links to work again (#5745) 2024-04-09 12:29:45 -04:00
Alessio Gravili
ec0e0ae449 chore(richtext-lexical): add e2e test to ensure that pre-seeded upload nodes are visible 2024-04-09 12:24:54 -04:00
Alessio Gravili
607ff17033 fix(richtext-lexical): upload nodes weren't visible due to incorrect relationships condition 2024-04-09 12:24:30 -04:00
Alessio Gravili
e73e610669 chore: fields test suite: clearAndSeedEverything instead of seed for dev as well, to ensure same state as test runs (most importantly, this gets rid of leftover uploads) 2024-04-09 12:22:42 -04:00
Jarrod Flesch
30fddde066 chore: corrects type for getLocalI18n 2024-04-09 11:51:40 -04:00
Jarrod Flesch
a56d2842fb chore: converts ua to uk 2024-04-09 11:44:34 -04:00
Jarrod Flesch
35f59a47cc chore: corrects dateFNS keys, stricter types 2024-04-09 11:38:38 -04:00
Jarrod Flesch
817d57bd12 chore: migrate langs 2024-04-09 11:05:35 -04:00
Elliot DeNolf
5826048e7b ci: publish script throttling 2024-04-09 09:50:23 -04:00
Elliot DeNolf
1a975b31cf chore(release): v3.0.0-alpha.60 [skip ci] 2024-04-09 09:36:41 -04:00
James
73298a80f0 chore: adds more de-flake to tabs 2024-04-09 09:34:11 -04:00
James
a5d14ef4c1 chore: de-flakes tabs 2024-04-09 09:34:11 -04:00
James
d0c79b65f8 chore: skips index to see what remains 2024-04-09 09:34:11 -04:00
James
2fc50b1a1f chore: de-flakes array 2024-04-09 09:34:11 -04:00
James
0ddeedb0b3 chore: de-flakes relationship suite 2024-04-09 09:34:11 -04:00
James
09c2fb10f3 chore: bug in ci 2024-04-09 09:34:11 -04:00
James
5bfff5b7ba chore: ensures artifacts work 2024-04-09 09:34:11 -04:00
James
702088375c chore: attempts to de-flake 2024-04-09 09:34:11 -04:00
James
ea507fbcc4 chore: moves lexical tests into collection folder 2024-04-09 09:34:11 -04:00
James
0ff1e6632b chore: splits out relationship 2024-04-09 09:34:11 -04:00
James
996ee47f96 chore: splits out blocks and array into their own suites 2024-04-09 09:34:11 -04:00
James
3e9bd5bb62 chore: uses ci env var to set max retries 2024-04-09 09:34:11 -04:00
James
ee7221c986 chore: sets maxRetries 2024-04-09 09:34:11 -04:00
James
318c126ae3 chore: turns off prebuild for fields 2024-04-09 09:34:11 -04:00
Jarrod Flesch
2154aea89f chore: comment out all other test suites for now 2024-04-09 09:34:11 -04:00
Jarrod Flesch
4d3ad1af35 chore: run fields e2e on ci 2024-04-09 09:34:11 -04:00
Alessio Gravili
75cab7688f chore: fix incorrect next tsconfig paths breaking monorepo setup (#5743) 2024-04-09 09:26:09 -04:00
James
5084d6dd97 chore: attempts to de-flake live preview 2024-04-08 22:34:23 -04:00
James
518f80cbb6 chore: es 2024-04-08 22:29:41 -04:00
James
c9399efa65 chore: fixes access control test 2024-04-08 22:25:35 -04:00
Elliot DeNolf
dd9133659c ci: rework caching, consolidates build (#5737) 2024-04-08 22:19:37 -04:00
James
d3016b7eb5 chore: merge 2024-04-08 22:13:16 -04:00
James
69e884f5b7 chore: dynamically loads date-fns locales 2024-04-08 22:12:05 -04:00
Elliot DeNolf
6d122905f4 chore: ignore new pointer files 2024-04-08 21:19:03 -04:00
Elliot DeNolf
e6e016ac2d chore(cpa): build for es6 (#5736) 2024-04-08 17:11:03 -04:00
James
3e7925e33f chore: removes async nature from a few lexical things 2024-04-08 16:48:56 -04:00
James
7a2ccba63c Merge branch 'feat/config-i18n' of github.com:payloadcms/payload into feat/config-i18n 2024-04-08 16:39:03 -04:00
James
be2134eb69 chore: requires languages to be passed to config 2024-04-08 16:38:12 -04:00
James Mikrut
95e422b0e1 Merge branch 'alpha' into feat/config-i18n 2024-04-08 16:26:38 -04:00
James
53d9c4ca95 chore: dynamically loads translations 2024-04-08 16:25:24 -04:00
James
30948ab545 chore: dynamically loads translations 2024-04-08 16:25:21 -04:00
Jacob Fletcher
906df6b401 fix(next): properly renders document-level unauthorized view (#5734) 2024-04-08 15:40:46 -04:00
Jacob Fletcher
b9c585bab5 fix(ui): uses correct save draft button label (#5730) 2024-04-08 14:34:28 -04:00
Elliot DeNolf
12203140ad chore: unprettified pointer files 2024-04-08 13:59:03 -04:00
Patrik
0704152e38 chore(next): removes unnecessary apostrophe from payload-lng cookie (#5729) 2024-04-08 13:42:44 -04:00
Elliot DeNolf
f582efe98d chore(release): v3.0.0-alpha.59 [skip ci] 2024-04-08 13:32:21 -04:00
James Mikrut
540579f520 chore: corrects invalid export (#5728) 2024-04-08 13:26:37 -04:00
James
24c348dc49 chore: corrects invalid export 2024-04-08 13:26:04 -04:00
Jacob Fletcher
4b4c245507 fix(ui): properly initializes collapsible context (#5725) 2024-04-08 12:48:44 -04:00
Dan Ribbens
400f68d1aa chore: add fetch to dev.js to trigger admin (#5724) 2024-04-08 12:26:33 -04:00
Jacob Fletcher
5bb27ed9cd fix(ui): renders searchable fields in list controls (#5723) 2024-04-08 11:49:02 -04:00
Jacob Fletcher
833498c269 chore(deps): regenerates frozen lockfile 2024-04-08 11:30:12 -04:00
Jacob Fletcher
80507d487b Merge branch 'alpha' into fix/list-searchable-fields 2024-04-08 11:23:24 -04:00
Jacob Fletcher
08f4ebaaf8 fix(ui): adds css specificity to select all 2024-04-08 11:22:05 -04:00
Jacob Fletcher
4c418525eb fix(ui): renders searchable fields in list controls 2024-04-08 11:01:47 -04:00
Elliot DeNolf
4962f6c926 chore(release): v3.0.0-alpha.58 [skip ci] 2024-04-08 10:52:04 -04:00
James Mikrut
50f0e9298c chore: adds upload export back (#5722) 2024-04-08 10:47:10 -04:00
James
89efcc5db1 chore: adds upload export back 2024-04-08 10:46:41 -04:00
Elliot DeNolf
c3119a5632 chore(release): v3.0.0-alpha.57 [skip ci] 2024-04-08 10:29:26 -04:00
James Mikrut
069bbd92b0 Feat/bulletproof loader (#5721) 2024-04-08 10:18:38 -04:00
James
c4422a2593 chore: improves logic of loader 2024-04-08 10:17:18 -04:00
James
3aab9d368e chore: working loader 2024-04-08 10:07:55 -04:00
Elliot DeNolf
b6afab63b2 chore(cpa): remove test from prepublishOnly 2024-04-08 09:34:52 -04:00
Alessio Gravili
b93f5e9c44 feat!: pass in req to access.admin, to match 2.0 behavior, and strongly type it (#5712) 2024-04-07 22:22:48 -04:00
James
630082035f chore: sets up test environment for loader 2024-04-07 19:48:42 -04:00
Elliot DeNolf
c74e41fc76 chore(release): v3.0.0-alpha.56 [skip ci] 2024-04-07 13:53:49 -04:00
Alessio Gravili
08ce7c58b5 chore: upgrade playwright & TS, and hide deprecation warnings (#5708) 2024-04-06 17:15:02 -04:00
Alessio Gravili
1853fde379 chore: upgrade typescript and "@types/"-prefixed packages 2024-04-06 15:35:09 -04:00
Alessio Gravili
f29d22ca95 chore: ensure node deprecation warnings stay hidden during e2e test runs 2024-04-06 15:06:04 -04:00
Alessio Gravili
5ea5f928ab chore: upgrade playwright from 1.42.1 to 1.43.0 and update patches 2024-04-06 15:05:27 -04:00
James Mikrut
efd6d35eb3 Fix/live preview flake (#5707) 2024-04-06 14:31:19 -04:00
James
2b2538f13a chore: removes old bin 2024-04-06 14:31:02 -04:00
James
9375dae179 Merge branch 'alpha' of github.com:payloadcms/payload into fix/live-preview-flake 2024-04-06 14:22:50 -04:00
James
674bb3758d chore: reduces parallel creates in live preview seed 2024-04-06 14:22:43 -04:00
James Mikrut
cedf9a2eb8 chore: pre-builds in CI (#5690) 2024-04-06 14:10:25 -04:00
James
7d2dc5b6c6 chore: adds more tests to loader int suite 2024-04-06 14:07:17 -04:00
James
9d2aad7bf9 chore: lockfile 2024-04-06 14:01:16 -04:00
James
f085d7609b chore: cleanup 2024-04-06 13:59:29 -04:00
James
a49243a42a chore: cleanup 2024-04-06 13:58:05 -04:00
James
e79f431f14 chore: cleans up tsconfigs 2024-04-06 13:40:30 -04:00
James
38e5b6e8e3 chore: temp disables fields 2024-04-06 13:38:14 -04:00
James
35bdb785c4 Merge branch 'chore/pre-build-e2e' of github.com:payloadcms/payload into chore/pre-build-e2e 2024-04-06 13:35:35 -04:00
James
25cb146fde feat: moduleResolution: bundler in config loaders 2024-04-06 13:35:09 -04:00
Jarrod Flesch
c10f0f4a9e chore: fix sort header route replace 2024-04-05 22:34:04 -04:00
Elliot DeNolf
3a3a7f6e16 ci(scripts): remove p-map 2024-04-05 17:48:21 -04:00
Elliot DeNolf
1d3b500962 chore(release): v3.0.0-alpha.55 [skip ci] 2024-04-05 17:40:07 -04:00
Dan Ribbens
f39f95af6d feat: custom db naming for postgres and mongodb (#5697) 2024-04-05 17:09:08 -04:00
Dan Ribbens
6fec2bbe1c chore: refactor auth from next into new payload local operation (#5641) 2024-04-05 17:08:51 -04:00
Elliot DeNolf
2412134073 chore: importConfig and importWithoutClientFiles (#5701) 2024-04-05 16:49:20 -04:00
Dan Ribbens
136545d1fd feat: move disableDuplicate out of admin and update APIs (#5620) 2024-04-05 16:44:41 -04:00
Jarrod Flesch
684c4d2113 chore: fix admin sort flake, fix console warning for test package.json 2024-04-05 16:39:13 -04:00
Elliot DeNolf
6624fd0401 ci: single job per branch (#5702) 2024-04-05 16:24:53 -04:00
James
31502d2da3 chore: attempts to de-flake admin 2024-04-05 16:12:49 -04:00
James
3ee39ecca3 chore: increases timeout for fields prebuild 2024-04-05 16:06:00 -04:00
Paul
89e7c305e7 chore: emails e2e suite (#5698)
fix: move 'email' configuration to server only
2024-04-05 16:50:47 -03:00
James
60dd71c59e chore: sets longer timeout for prebuild in admin and fields 2024-04-05 15:49:12 -04:00
James
0936f77930 Merge branch 'alpha' of github.com:payloadcms/payload into chore/pre-build-e2e 2024-04-05 15:25:41 -04:00
James
3c09b95a8c chore: builds in child process for e2e 2024-04-05 15:23:07 -04:00
Jacob Fletcher
780f26f135 fix(next): incorrect access of select options in versions view (#5687) 2024-04-05 15:03:49 -04:00
Jacob Fletcher
77618674a0 fix(next): live preview url (#5692) 2024-04-05 15:00:58 -04:00
Alessio Gravili
c9c89a6005 fix(richtext-lexical): do not allow omitting editor prop for sub-richtext fields within lexical defined in the payload config (#5699) 2024-04-05 14:42:15 -04:00
Jacob Fletcher
d1276c4299 feat(next): threads payload through live preview url 2024-04-05 14:36:14 -04:00
Jacob Fletcher
69730b6c95 Merge branch 'alpha' into fix/lp-url 2024-04-05 14:25:17 -04:00
James
9147d30152 Merge branch 'chore/green-ci' of github.com:payloadcms/payload into chore/pre-build-e2e 2024-04-05 14:13:28 -04:00
James
6217c70fb5 chore: prebuilds fields and admin in ci 2024-04-05 14:13:14 -04:00
Jarrod Flesch
17352c9a56 chore: change pw buttons, field readOnly prop, admin panel thumbnailURL fallback (#5694) 2024-04-05 13:51:17 -04:00
Jacob Fletcher
1b6026304f fix(next): adjusts args sent through live preview url 2024-04-05 13:37:12 -04:00
Jacob Fletcher
91f9973c6c fix(next): properly types locale in live preview url 2024-04-05 13:37:07 -04:00
Elliot DeNolf
ca2acee38a chore(release): create-payload-app/v3.0.0-alpha.54 [skip ci] 2024-04-05 11:53:19 -04:00
James
3bd455ced2 chore: pre-builds in CI 2024-04-05 11:46:02 -04:00
Patrik
2c25abd143 test(versions): replaces outdated CustomPublishButtonProps type (#5688) 2024-04-05 11:11:16 -04:00
PatrikKozak
08a9fb8cd7 Merge branch 'alpha' of https://github.com/payloadcms/payload into fix/alpha/versions-select-options 2024-04-05 10:38:23 -04:00
PatrikKozak
d8a38b4e35 fix: incorrect access of select options in versions 2024-04-05 10:38:09 -04:00
Paul
e64660f2d8 fix(db-postgres): querying by localised relations postgres (#5686) 2024-04-05 10:37:58 -04:00
Elliot DeNolf
1a11466e69 chore(release): v3.0.0-alpha.54 [skip ci] 2024-04-04 21:05:18 -04:00
Elliot DeNolf
78ab2fbe09 chore: adjust translations and next publishConfig 2024-04-04 21:03:36 -04:00
James
0c45d5773a chore: brings back admin and fields 2024-04-04 21:02:31 -04:00
James
f48335444b chore: flake 2024-04-04 21:01:38 -04:00
Elliot DeNolf
81b33cee5c chore(release): v3.0.0-alpha.53 [skip ci] 2024-04-04 20:32:31 -04:00
James Mikrut
020dcaad75 chore: exports sql from pg adapter (#5678) 2024-04-04 20:31:14 -04:00
James
9bc56bcfc7 chore: exports sql from pg adapter 2024-04-04 20:30:38 -04:00
Elliot DeNolf
8325fadeb3 chore(release): v3.0.0-alpha.52 [skip ci] 2024-04-04 20:15:31 -04:00
Elliot DeNolf
d54275b3bd chore: unpushed version bump 2024-04-04 20:11:16 -04:00
James Mikrut
29d20423a3 chore: adds retryWrites, fixes a few flakes (#5674) 2024-04-04 20:09:51 -04:00
James
e539816253 chore: temp remove fields 2024-04-04 20:09:33 -04:00
James
922ce9ef5f chore: re-enables database adapter types 2024-04-04 20:07:59 -04:00
James
b73ec6ae94 chore: flake 2024-04-04 19:39:41 -04:00
Elliot DeNolf
4a11bf956d fix: db migrations esm part 2 (#5677) 2024-04-04 19:36:59 -04:00
Elliot DeNolf
3b3bb6c80a fix: db migrations esm (#5675) 2024-04-04 19:33:04 -04:00
James
0f323ff2e3 chore: re-adds fields 2024-04-04 19:17:41 -04:00
James
3305c65ae6 chore: adds retryWrites, fixes a few flakes 2024-04-04 19:14:01 -04:00
735 changed files with 22479 additions and 32635 deletions

View File

@@ -4,7 +4,11 @@ on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches: ['main', 'alpha']
branches: ['main', 'beta']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
@@ -35,7 +39,7 @@ jobs:
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
echo "templates: ${{ steps.filter.outputs.templates }}"
core-build:
build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
@@ -61,66 +65,29 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
- name: Setup pnpm cache
uses: actions/cache@v4
timeout-minutes: 720
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
pnpm-store-
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- run: pnpm run build:core
- run: pnpm run build:all
- name: Cache build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
plugins-build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- run: pnpm run build:plugins
tests-unit:
runs-on: ubuntu-latest
needs: core-build
if: false # Disable until tests are updated for 3.0
needs: build
steps:
- name: Use Node.js 18
@@ -136,6 +103,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -147,7 +115,7 @@ jobs:
tests-int:
runs-on: ubuntu-latest
needs: core-build
needs: build
strategy:
fail-fast: false
matrix:
@@ -180,6 +148,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -238,7 +207,7 @@ jobs:
tests-e2e:
runs-on: ubuntu-latest
needs: core-build
needs: build
strategy:
fail-fast: false
matrix:
@@ -246,12 +215,16 @@ jobs:
suite:
- _community
- access-control
# - admin
- admin
- auth
- email
- field-error-states
- fields-relationship
# - fields
- fields/lexical
- fields
- fields__collections__Blocks
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__Lexical
- live-preview
- localization
- plugin-form-builder
@@ -274,6 +247,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -294,7 +268,7 @@ jobs:
tests-type-generation:
if: false # This should be replaced with gen on a real Payload project
runs-on: ubuntu-latest
needs: core-build
needs: build
steps:
- name: Use Node.js 18
@@ -310,6 +284,7 @@ jobs:
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}

View File

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

2
.nvmrc
View File

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

View File

@@ -10,3 +10,5 @@
**/temp
**/docs/**
tsconfig.json
packages/payload/*.js
packages/payload/*.d.ts

11
.vscode/launch.json vendored
View File

@@ -23,6 +23,17 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/loader/init.js",
"cwd": "${workspaceFolder}",
"name": "Run Loader",
"request": "launch",
"type": "node-terminal",
"env": {
"LOADER_TEST_FILE_PATH": "./dependency-test.js"
// "LOADER_TEST_FILE_PATH": "../fields/config.ts"
}
},
{
"command": "node --no-deprecation test/dev.js admin",
"cwd": "${workspaceFolder}",

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views/NotFound/index.js'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views/Root/index.js'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {

View File

@@ -1,7 +1,7 @@
/* 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/index.js'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)

View File

@@ -1,6 +1,6 @@
/* 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 { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -1,6 +1,6 @@
/* 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 { GRAPHQL_POST } from '@payloadcms/next/routes/index.js'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts/Root/index.js'
import { RootLayout } from '@payloadcms/next/layouts'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'

View File

@@ -1,35 +0,0 @@
import QueryString from 'qs'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export const fetchDoc = async <T>(args: {
collection: string
depth?: number
id?: string
slug?: string
}): Promise<T> => {
const { id, slug, collection, depth = 2 } = args || {}
const queryString = QueryString.stringify(
{
...(slug ? { 'where[slug][equals]': slug } : {}),
...(depth ? { depth } : {}),
},
{ addQueryPrefix: true },
)
const doc: T = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}${queryString}`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')
return res?.docs?.[0]
})
return doc
}

View File

@@ -1,19 +0,0 @@
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export const fetchDocs = async <T>(collection: string): Promise<T[]> => {
const docs: T[] = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}?depth=0&limit=100`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => res.json())
?.then((res) => {
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs')
return res?.docs
})
return docs
}

View File

@@ -1,25 +0,0 @@
import type { Footer } from '../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export async function fetchFooter(): Promise<Footer> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const footer = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/footer`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching footer')
return res
})
return footer
}

View File

@@ -1,25 +0,0 @@
import type { Header } from '../../../test/live-preview/payload-types.js'
import { PAYLOAD_SERVER_URL } from './serverURL.js'
export async function fetchHeader(): Promise<Header> {
if (!PAYLOAD_SERVER_URL) throw new Error('PAYLOAD_SERVER_URL not found')
const header = await fetch(`${PAYLOAD_SERVER_URL}/api/globals/header`, {
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})
?.then((res) => {
if (!res.ok) throw new Error('Error fetching doc')
return res.json()
})
?.then((res) => {
if (res?.errors) throw new Error(res?.errors[0]?.message || 'Error fetching header')
return res
})
return header
}

View File

@@ -639,12 +639,12 @@ export const CustomArrayManager = () => {
The `useCollapsible` hook allows you to control parent collapsibles:
| Property | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | --- |
| **`collapsed`** | State of the collapsible. `true` if open, `false` if collapsed |
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise |
| **`toggle`** | Toggles the state of the nearest collapsible |
| **`withinCollapsible`** | Determine when you are within another collaspible | |
| Property | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------ | --- |
| **`isCollapsed`** | State of the collapsible. `true` if open, `false` if collapsed |
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise |
| **`toggle`** | Toggles the state of the nearest collapsible |
| **`isWithinCollapsible`** | Determine when you are within another collaspible | |
**Example:**
@@ -654,10 +654,11 @@ import React from 'react'
import { useCollapsible } from 'payload/components/utilities'
const CustomComponent: React.FC = () => {
const { collapsed, toggle } = useCollapsible()
const { isCollapsed, toggle } = useCollapsible()
return (
<div>
<p className="field-type">I am {collapsed ? 'closed' : 'open'}</p>
<p className="field-type">I am {isCollapsed ? 'closed' : 'open'}</p>
<button onClick={toggle} type="button">
Toggle
</button>

View File

@@ -13,23 +13,25 @@ It's often best practice to write your Collections in separate files and then im
## Options
| Option | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
| **`endpoints`** | Add custom routes to the REST API. Set to `false` to disable routes. [More](/docs/rest-api/overview#custom-endpoints) |
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`disableDuplicate`** | When true, do not show the "Duplicate" button while editing documents within this collection and prevent `duplicate` from all APIs. |
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
| **`dbName`** | Custom table or collection name depending on the database adapter. Auto-generated from slug if not defined.
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
_\* An asterisk denotes that a property is required._
@@ -75,7 +77,6 @@ property on a collection's config.
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
| `enableRichTextLink` | The [Rich Text](/docs/fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](/docs/fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |

View File

@@ -26,6 +26,7 @@ As with Collection configs, it's often best practice to write your Globals in se
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`dbName`** | Custom table or collection name for this global depending on the database adapter. Auto-generated from slug if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -38,12 +38,18 @@ export default buildConfig({
### Options
| Option | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pool` | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| Option | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
| `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. |
| `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
### Access to Drizzle

View File

@@ -45,6 +45,7 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`dbName`** | Custom table name for the field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
_\* An asterisk denotes that a property is required._

View File

@@ -80,6 +80,7 @@ Blocks are defined as separate configs of their own.
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. |
| **`dbName`** | Custom table name for this block type when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined.
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
#### Auto-generated data per block

View File

@@ -20,27 +20,27 @@ keywords: number, fields, config, configuration, documentation, Content Manageme
### Config
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`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) |
| **`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) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Option | Description |
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
| **`min`** | Minimum value accepted. Used in the default `validation` function. |
| **`max`** | Maximum value accepted. Used in the default `validation` function. |
| **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. |
| **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. |
| **`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) |
| **`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) |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
_\* An asterisk denotes that a property is required._

View File

@@ -36,6 +36,7 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined.
_\* An asterisk denotes that a property is required._

View File

@@ -38,6 +38,8 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL database adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
_\* An asterisk denotes that a property is required._

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

@@ -1 +0,0 @@
export default {}

View File

@@ -10,6 +10,12 @@ const withBundleAnalyzer = bundleAnalyzer({
export default withBundleAnalyzer(
withPayload({
reactStrictMode: false,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
async redirects() {
return [
{

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-alpha.50",
"version": "3.0.0-beta.10",
"private": true,
"type": "module",
"workspaces:": [
@@ -76,7 +76,7 @@
"@octokit/core": "^5.1.0",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
"@playwright/test": "^1.42.1",
"@playwright/test": "1.43.0",
"@swc/cli": "^0.1.62",
"@swc/jest": "0.2.36",
"@testing-library/jest-dom": "6.4.2",
@@ -88,13 +88,13 @@
"@types/conventional-changelog-writer": "^4.0.10",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
"@types/minimist": "1.2.2",
"@types/node": "20.11.28",
"@types/minimist": "1.2.5",
"@types/node": "20.12.5",
"@types/prompts": "^2.4.5",
"@types/qs": "6.9.7",
"@types/react": "18.2.15",
"@types/qs": "6.9.14",
"@types/react": "18.2.74",
"@types/semver": "^7.5.3",
"@types/shelljs": "0.8.12",
"@types/shelljs": "0.8.15",
"add-stream": "^1.0.0",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -127,15 +127,15 @@
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "14.2.0-canary.22",
"next": "^14.2.0-canary.23",
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
"p-map": "^7.0.2",
"p-limit": "^5.0.0",
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"playwright": "^1.42.1",
"playwright-core": "^1.42.1",
"playwright": "1.43.0",
"playwright-core": "1.43.0",
"prettier": "^3.0.3",
"prompts": "2.4.2",
"qs": "6.11.2",
@@ -155,7 +155,7 @@
"ts-node": "10.9.1",
"tsx": "^4.7.1",
"turbo": "^1.13.2",
"typescript": "5.4.2",
"typescript": "5.4.4",
"uuid": "^9.0.1",
"yocto-queue": "^1.0.0"
},
@@ -163,7 +163,7 @@
"react": "18.2.0"
},
"engines": {
"node": ">=18.17.0",
"node": ">=18.20.2",
"pnpm": ">=8"
},
"lint-staged": {
@@ -195,8 +195,7 @@
"domexception": "4"
},
"patchedDependencies": {
"playwright@1.42.1": "patches/playwright@1.42.1.patch"
"playwright@1.43.0": "patches/playwright@1.43.0.patch"
}
},
"packageManager": "pnpm@8.15.4+sha256.cea6d0bdf2de3a0549582da3983c70c92ffc577ff4410cbf190817ddc35137c2"
}
}

View File

@@ -10,6 +10,6 @@
}
},
"module": {
"type": "commonjs"
"type": "es6"
}
}

View File

@@ -1,2 +1,4 @@
#!/usr/bin/env node
require('../dist/index.js')
import { main } from '../dist/index.js'
main()

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-alpha.50",
"version": "3.0.0-beta.10",
"license": "MIT",
"type": "module",
"homepage": "https://payloadcms.com",
@@ -19,7 +19,7 @@
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"clean": "rimraf {dist,*.tsbuildinfo}",
"test": "jest",
"prepublishOnly": "pnpm test && pnpm clean && pnpm build"
"prepublishOnly": "pnpm clean && pnpm build"
},
"files": [
"package.json",
@@ -35,7 +35,7 @@
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"detect-package-manager": "^3.0.1",
"esprima": "^4.0.1",
"esprima-next": "^6.0.3",
"execa": "^5.0.0",
"figures": "^6.1.0",
"fs-extra": "^9.0.1",
@@ -48,7 +48,7 @@
"@types/esprima": "^4.0.6",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^27.0.3",
"@types/node": "^16.6.2"
"@types/node": "20.12.5"
},
"exports": {
"./commands": {

View File

@@ -1,8 +1,12 @@
import { Main } from './main.js'
import { error } from './utils/log.js'
async function main(): Promise<void> {
await new Main().init()
export async function main(): Promise<void> {
try {
await new Main().init()
} catch (e) {
if (e instanceof Error) {
error(e.message)
}
}
}
main().catch((e) => error(`An error has occurred: ${e instanceof Error ? e.message : e}`))

View File

@@ -4,6 +4,7 @@ import * as p from '@clack/prompts'
import { parse, stringify } from 'comment-json'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'
import { promisify } from 'util'
@@ -31,6 +32,8 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
useDistFiles?: boolean
}
type NextConfigType = 'cjs' | 'esm'
type InitNextResult =
| {
isSrcDir: boolean
@@ -45,11 +48,22 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const nextAppDetails = args.nextAppDetails || (await getNextAppDetails(projectDir))
const { hasTopLevelLayout, isSrcDir, nextAppDir } =
nextAppDetails || (await getNextAppDetails(projectDir))
if (!nextAppDetails.nextAppDir) {
warning(`Could not find app directory in ${projectDir}, creating...`)
const createdAppDir = path.resolve(projectDir, nextAppDetails.isSrcDir ? 'src/app' : 'app')
fse.mkdirSync(createdAppDir, { recursive: true })
nextAppDetails.nextAppDir = createdAppDir
}
if (!nextAppDir) {
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
const { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigType } = nextAppDetails
if (!nextConfigType) {
return {
isSrcDir,
nextAppDir,
reason: `Could not determine Next Config type in ${projectDir}. Possibly try renaming next.config.js to next.config.cjs or next.config.mjs.`,
success: false,
}
}
if (hasTopLevelLayout) {
@@ -69,6 +83,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const configurationResult = installAndConfigurePayload({
...args,
nextAppDetails,
nextConfigType,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
})
@@ -96,6 +111,13 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean) {
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
// Check if tsconfig.json exists
if (!fs.existsSync(tsConfigPath)) {
warning(`Could not find tsconfig.json to add @payload-config path.`)
return
}
const userTsConfigContent = await readFile(tsConfigPath, {
encoding: 'utf8',
})
@@ -119,13 +141,18 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
}
function installAndConfigurePayload(
args: InitNextArgs & { nextAppDetails: NextAppDetails; useDistFiles?: boolean },
args: InitNextArgs & {
nextAppDetails: NextAppDetails
nextConfigType: NextConfigType
useDistFiles?: boolean
},
):
| { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } {
const {
'--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
nextConfigType,
projectDir,
useDistFiles,
} = args
@@ -172,6 +199,7 @@ function installAndConfigurePayload(
logDebug(`nextAppDir: ${nextAppDir}`)
logDebug(`projectDir: ${projectDir}`)
logDebug(`nextConfigPath: ${nextConfigPath}`)
logDebug(`payloadConfigPath: ${path.resolve(projectDir, 'payload.config.ts')}`)
logDebug(
`isSrcDir: ${isSrcDir}. source: ${templateSrcDir}. dest: ${path.dirname(nextConfigPath)}`,
@@ -181,7 +209,7 @@ function installAndConfigurePayload(
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
// Wrap next.config.js with withPayload
wrapNextConfig({ nextConfigPath })
wrapNextConfig({ nextConfigPath, nextConfigType })
return {
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
@@ -191,10 +219,10 @@ function installAndConfigurePayload(
async function installDeps(projectDir: string, packageManager: PackageManager, dbType: DbType) {
const packagesToInstall = ['payload', '@payloadcms/next', '@payloadcms/richtext-lexical'].map(
(pkg) => `${pkg}@alpha`,
(pkg) => `${pkg}@beta`,
)
packagesToInstall.push(`@payloadcms/db-${dbType}@alpha`)
packagesToInstall.push(`@payloadcms/db-${dbType}@beta`)
let exitCode = 0
switch (packageManager) {
@@ -226,6 +254,7 @@ type NextAppDetails = {
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
}
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
@@ -246,6 +275,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
ignore: ['**/node_modules/**'],
onlyDirectories: true,
})
)?.[0]
@@ -254,9 +284,31 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
nextAppDir = undefined
}
const configType = await getProjectType(projectDir, nextConfigPath)
const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
: false
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath }
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath, nextConfigType: configType }
}
async function getProjectType(projectDir: string, nextConfigPath: string): Promise<'cjs' | 'esm'> {
if (nextConfigPath.endsWith('.mjs')) {
return 'esm'
}
if (nextConfigPath.endsWith('.cjs')) {
return 'cjs'
}
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
const packageJsonType = packageObj.type
if (packageJsonType === 'module') {
return 'esm'
}
if (packageJsonType === 'commonjs') {
return 'cjs'
}
return 'cjs'
}

View File

@@ -10,14 +10,18 @@ const mongodbReplacement: DbAdapterReplacement = {
importReplacement: "import { mongooseAdapter } from '@payloadcms/db-mongodb'",
packageName: '@payloadcms/db-mongodb',
// Replacement between `// database-adapter-config-start` and `// database-adapter-config-end`
configReplacement: [' db: mongooseAdapter({', ' url: process.env.DATABASE_URI,', ' }),'],
configReplacement: [
' db: mongooseAdapter({',
" url: process.env.DATABASE_URI || '',",
' }),',
],
}
const postgresReplacement: DbAdapterReplacement = {
configReplacement: [
' db: postgresAdapter({',
' pool: {',
' connectionString: process.env.DATABASE_URI,',
" connectionString: process.env.DATABASE_URI || '',",
' },',
' }),',
],

View File

@@ -18,43 +18,46 @@ export function getValidTemplates(): ProjectTemplate[] {
name: 'blank-3.0',
type: 'starter',
description: 'Blank 3.0 Template',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0',
},
{
name: 'blank',
type: 'starter',
description: 'Blank Template',
url: 'https://github.com/payloadcms/payload/templates/blank',
},
{
name: 'website',
type: 'starter',
description: 'Website Template',
url: 'https://github.com/payloadcms/payload/templates/website',
},
{
name: 'ecommerce',
type: 'starter',
description: 'E-commerce Template',
url: 'https://github.com/payloadcms/payload/templates/ecommerce',
},
{
name: 'plugin',
type: 'plugin',
description: 'Template for creating a Payload plugin',
url: 'https://github.com/payloadcms/payload-plugin-template',
},
{
name: 'payload-demo',
type: 'starter',
description: 'Payload demo site at https://demo.payloadcms.com',
url: 'https://github.com/payloadcms/public-demo',
},
{
name: 'payload-website',
type: 'starter',
description: 'Payload website CMS at https://payloadcms.com',
url: 'https://github.com/payloadcms/website-cms',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0#beta',
},
// Remove these until they have been updated for 3.0
// {
// name: 'blank',
// type: 'starter',
// description: 'Blank Template',
// url: 'https://github.com/payloadcms/payload/templates/blank',
// },
// {
// name: 'website',
// type: 'starter',
// description: 'Website Template',
// url: 'https://github.com/payloadcms/payload/templates/website',
// },
// {
// name: 'ecommerce',
// type: 'starter',
// description: 'E-commerce Template',
// url: 'https://github.com/payloadcms/payload/templates/ecommerce',
// },
// {
// name: 'plugin',
// type: 'plugin',
// description: 'Template for creating a Payload plugin',
// url: 'https://github.com/payloadcms/payload-plugin-template',
// },
// {
// name: 'payload-demo',
// type: 'starter',
// description: 'Payload demo site at https://demo.payloadcms.com',
// url: 'https://github.com/payloadcms/public-demo',
// },
// {
// name: 'payload-website',
// type: 'starter',
// description: 'Payload website CMS at https://payloadcms.com',
// url: 'https://github.com/payloadcms/website-cms',
// },
]
}

View File

@@ -1,61 +1,159 @@
import { parseAndModifyConfigContent, withPayloadImportStatement } from './wrap-next-config.js'
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
import * as p from '@clack/prompts'
const defaultNextConfig = `/** @type {import('next').NextConfig} */
const esmConfigs = {
defaultNextConfig: `/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
`
const nextConfigWithFunc = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(nextConfig)
`
const nextConfigWithFuncMultiline = `const nextConfig = {
// Your Next.js config here
}
`,
nextConfigWithFunc: `const nextConfig = {};
export default someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};;
export default someFunc(
nextConfig
)
`
const nextConfigExportNamedDefault = `const nextConfig = {
// Your Next.js config here
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
export { wrapped as default };
`,
nextConfigWithSpread: `const nextConfig = {
...someConfig,
};
export default nextConfig;
`,
}
const cjsConfigs = {
defaultNextConfig: `
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;
`,
anonConfig: `module.exports = {};`,
nextConfigWithFunc: `const nextConfig = {};
module.exports = someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};
module.exports = someFunc(
nextConfig
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
module.exports = wrapped;
`,
nextConfigWithSpread: `const nextConfig = { ...someConfig };
module.exports = nextConfig;
`,
}
const wrapped = someFunc(asdf)
export { wrapped as default }
`
describe('parseAndInsertWithPayload', () => {
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(defaultNextConfig)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFunc)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
describe('esm', () => {
const configType = 'esm'
const importStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
esmConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Could not automatically wrap'),
)
})
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFuncMultiline)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
describe('cjs', () => {
const configType = 'cjs'
const requireStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse anonymous default config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.anonConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload({})')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a named export as default', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(wrapped)')
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
nextConfigExportNamedDefault,
)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(expect.stringContaining('Could not automatically wrap'))
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
})
})

View File

@@ -1,16 +1,29 @@
import type { Program } from 'esprima-next'
import chalk from 'chalk'
import { parseModule } from 'esprima'
import { Syntax, parseModule } from 'esprima-next'
import fs from 'fs'
import { warning } from '../utils/log.js'
import { log } from '../utils/log.js'
export const withPayloadImportStatement = `import { withPayload } from '@payloadcms/next'\n`
export const withPayloadStatement = {
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`,
esm: `import { withPayload } from '@payloadcms/next/withPayload'\n`,
}
export const wrapNextConfig = (args: { nextConfigPath: string }) => {
const { nextConfigPath } = args
type NextConfigType = 'cjs' | 'esm'
export const wrapNextConfig = (args: {
nextConfigPath: string
nextConfigType: NextConfigType
}) => {
const { nextConfigPath, nextConfigType: configType } = args
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(configContent)
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(
configContent,
configType,
)
if (!success) {
return
@@ -22,72 +35,121 @@ export const wrapNextConfig = (args: { nextConfigPath: string }) => {
/**
* Parses config content with AST and wraps it with withPayload function
*/
export function parseAndModifyConfigContent(content: string): {
modifiedConfigContent: string
success: boolean
} {
content = withPayloadImportStatement + content
const ast = parseModule(content, { loc: true })
const exportDefaultDeclaration = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
| Directive
| undefined
export function parseAndModifyConfigContent(
content: string,
configType: NextConfigType,
): { modifiedConfigContent: string; success: boolean } {
content = withPayloadStatement[configType] + content
const exportNamedDeclaration = ast.body.find((p) => p.type === 'ExportNamedDeclaration') as
| ExportNamedDeclaration
| undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
}
let ast: Program | undefined
try {
ast = parseModule(content, { loc: true })
} catch (error: unknown) {
if (error instanceof Error) {
warning(`Unable to parse Next config. Error: ${error.message} `)
warnUserWrapNotSuccessful(configType)
}
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful()
if (configType === 'esm') {
const exportDefaultDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportDefaultDeclaration,
) as Directive | undefined
const exportNamedDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportNamedDeclaration,
) as ExportNamedDeclaration | undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
} else if (configType === 'cjs') {
// Find `module.exports = X`
const moduleExports = ast.body.find(
(p) =>
p.type === Syntax.ExpressionStatement &&
p.expression?.type === Syntax.AssignmentExpression &&
p.expression.left?.type === Syntax.MemberExpression &&
p.expression.left.object?.type === Syntax.Identifier &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === Syntax.Identifier &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
moduleExports.expression.right.loc,
)
return { modifiedConfigContent, success: true }
}
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
function warnUserWrapNotSuccessful() {
function warnUserWrapNotSuccessful(configType: NextConfigType) {
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)}
import withPayload from '@payloadcms/next/withPayload'
${withPayloadStatement[configType]}
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)
${configType === 'esm' ? 'export default withPayload(nextConfig)' : 'module.exports = withPayload(nextConfig)'}
`

View File

@@ -20,32 +20,41 @@ export async function writeEnvFile(args: {
return
}
const envOutputPath = path.join(projectDir, '.env')
try {
if (template?.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
.split('\n')
.filter((e) => e)
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) return line
if (fs.existsSync(envOutputPath)) {
if (template?.type === 'starter') {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
.split('\n')
.filter((e) => e)
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) return line
const split = line.split('=')
const key = split[0]
let value = split[1]
const split = line.split('=')
const key = split[0]
let value = split[1]
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
value = databaseUri
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
value = payloadSecret
}
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
value = databaseUri
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
value = payloadSecret
}
return `${key}=${value}`
})
return `${key}=${value}`
})
// Write new .env file
await fs.writeFile(path.join(projectDir, '.env'), envWithValues.join('\n'))
// Write new .env file
await fs.writeFile(envOutputPath, envWithValues.join('\n'))
} else {
const existingEnv = await fs.readFile(envOutputPath, 'utf8')
const newEnv =
existingEnv + `\nDATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}\n`
await fs.writeFile(envOutputPath, newEnv)
}
} else {
const content = `DATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
await fs.outputFile(`${projectDir}/.env`, content)

View File

@@ -21,6 +21,12 @@ export function helpMessage(): void {
console.log(chalk`
{bold USAGE}
{dim Inside of an existing Next.js project}
{dim $} {bold npx create-payload-app}
{dim Create a new project from scratch}
{dim $} {bold npx create-payload-app}
{dim $} {bold npx create-payload-app} my-project
{dim $} {bold npx create-payload-app} -n my-project -t template-name
@@ -80,7 +86,7 @@ export function successfulNextInit(): string {
}
export function moveMessage(args: { nextAppDir: string; projectDir: string }): string {
const relativePath = path.relative(process.cwd(), args.nextAppDir)
const relativeAppDir = path.relative(process.cwd(), args.nextAppDir)
return `
${header('Next Steps:')}
@@ -88,7 +94,10 @@ Payload does not support a top-level layout.tsx file in the app directory.
${chalk.bold('To continue:')}
Move all files from ./${relativePath} to a named directory such as ./${relativePath}/${chalk.bold('(app)')}
- Create a new directory in ./${relativeAppDir} such as ./${relativeAppDir}/${chalk.bold('(app)')}
- Move all files from ./${relativeAppDir} into that directory
It is recommended to do this from your IDE if your app has existing file references.
Once moved, rerun the create-payload-app command again.
`

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-alpha.50",
"version": "3.0.0-beta.10",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",
@@ -30,7 +30,6 @@
"get-port": "5.1.1",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"uuid": "9.0.0"

View File

@@ -45,7 +45,9 @@ export const createMigration: CreateMigration = async function createMigration({
// Check if predefined migration exists
if (fs.existsSync(cleanPath)) {
let migration = await eval(`${require ? 'require' : 'import'}(${cleanPath})`)
let migration = await eval(
`${typeof require === 'function' ? 'require' : 'import'}(${cleanPath})`,
)
if ('default' in migration) migration = migration.default
const { down, up } = migration

View File

@@ -5,11 +5,7 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import mongoose from 'mongoose'
import paginate from 'mongoose-paginate-v2'
import {
buildVersionCollectionFields,
buildVersionGlobalFields,
getVersionsModelName,
} from 'payload/versions'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import type { MongooseAdapter } from './index.js'
import type { CollectionModel } from './types.js'
@@ -18,13 +14,14 @@ import buildCollectionSchema from './models/buildCollectionSchema.js'
import { buildGlobalModel } from './models/buildGlobalModel.js'
import buildSchema from './models/buildSchema.js'
import getBuildQueryPlugin from './queries/buildQuery.js'
import { getDBName } from './utilities/getDBName.js'
export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const schema = buildCollectionSchema(collection, this.payload.config)
if (collection.versions) {
const versionModelName = getVersionsModelName(collection)
const versionModelName = getDBName({ config: collection, versions: true })
const versionCollectionFields = buildVersionCollectionFields(collection)
@@ -54,17 +51,11 @@ export const init: Init = function init(this: MongooseAdapter) {
}
const model = mongoose.model(
collection.slug,
getDBName({ config: collection }),
schema,
this.autoPluralization === true ? undefined : collection.slug,
) as CollectionModel
this.collections[collection.slug] = model
// TS expect error only needed until we launch 2.0.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
this.payload.collections[collection.slug] = {
config: collection,
}
})
const model = buildGlobalModel(this.payload.config)
@@ -72,7 +63,7 @@ export const init: Init = function init(this: MongooseAdapter) {
this.payload.config.globals.forEach((global) => {
if (global.versions) {
const versionModelName = getVersionsModelName(global)
const versionModelName = getDBName({ config: global, versions: true })
const versionGlobalFields = buildVersionGlobalFields(global)

View File

@@ -363,7 +363,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
}
if (field.localized && config.localization) {
config.localization.locales.forEach((locale) => {
schema.index({ [`${field.name}.${locale}`]: '2dsphere' }, indexOptions)
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
})
} else {
schema.index({ [field.name]: '2dsphere' }, indexOptions)

View File

@@ -59,17 +59,12 @@ export async function buildSearchParam({
let hasCustomID = false
if (sanitizedPath === '_id') {
const customIDfield = payload.collections[collectionSlug]?.config.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
const customIDFieldType = payload.collections[collectionSlug]?.customIDType
let idFieldType: 'number' | 'text' = 'text'
if (customIDfield) {
if (customIDfield?.type === 'text' || customIDfield?.type === 'number') {
idFieldType = customIDfield.type
}
if (customIDFieldType) {
idFieldType = customIDFieldType
hasCustomID = true
}
@@ -213,18 +208,11 @@ export async function buildSearchParam({
} else {
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(
(relationTo) => {
const isRelatedToCustomNumberID = payload.collections[
relationTo
]?.config?.fields.find((relatedField) => {
return (
fieldAffectsData(relatedField) &&
relatedField.name === 'id' &&
relatedField.type === 'number'
)
})
const isRelatedToCustomNumberID =
payload.collections[relationTo]?.customIDType === 'number'
if (isRelatedToCustomNumberID) {
if (isRelatedToCustomNumberID.type === 'number') hasNumberIDRelation = true
hasNumberIDRelation = true
}
},
)

View File

@@ -1,18 +1,20 @@
import { sanitizeConfig } from 'payload/config'
import { SanitizedConfig, sanitizeConfig } from 'payload/config'
import { Config } from 'payload/config'
import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'
const config = {
const config = sanitizeConfig({
localization: {
locales: ['en', 'es'],
defaultLocale: 'en',
fallback: true,
},
} as Config
} as Config) as SanitizedConfig
describe('get localized sort property', () => {
it('passes through a non-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -28,7 +30,7 @@ describe('get localized sort property', () => {
it('properly localizes an un-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -45,7 +47,7 @@ describe('get localized sort property', () => {
it('keeps specifically asked-for localized sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['title', 'es'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -62,7 +64,7 @@ describe('get localized sort property', () => {
it('properly localizes nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'group',
@@ -85,7 +87,7 @@ describe('get localized sort property', () => {
it('keeps requested locale with nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title', 'es'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'group',
@@ -108,7 +110,7 @@ describe('get localized sort property', () => {
it('properly localizes field within row', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'row',
@@ -130,7 +132,7 @@ describe('get localized sort property', () => {
it('properly localizes field within named tab', () => {
const result = getLocalizedSortProperty({
segments: ['tab', 'title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'tabs',
@@ -157,7 +159,7 @@ describe('get localized sort property', () => {
it('properly localizes field within unnamed tab', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'tabs',

View File

@@ -0,0 +1,41 @@
import type { DBIdentifierName } from 'payload/database'
type Args = {
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
locales?: boolean
target?: 'dbName' | 'enumName'
versions?: boolean
}
/**
* Used to name database enums and collections
* Returns the collection or enum name for a given entity
*/
export const getDBName = ({
config: { name, slug },
config,
target = 'dbName',
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({}) : custom
} else {
result = name ?? slug
}
if (versions) result = `_${result}_versions`
return result
}

View File

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

View File

@@ -1,9 +1,8 @@
import type { Create } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const create: Create = async function create(
@@ -20,7 +19,10 @@ export const create: Create = async function create(
fields: collection.fields,
operation: 'create',
req,
tableName: toSnakeCase(collectionSlug),
tableName: getTableName({
adapter: this,
config: collection,
}),
})
return result

View File

@@ -1,10 +1,9 @@
import type { CreateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobal<T extends TypeWithID>(
@@ -21,7 +20,10 @@ export async function createGlobal<T extends TypeWithID>(
fields: globalConfig.fields,
operation: 'create',
req,
tableName: toSnakeCase(slug),
tableName: getTableName({
adapter: this,
config: globalConfig,
}),
})
return result

View File

@@ -4,10 +4,10 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { type CreateGlobalVersionArgs } from 'payload/database'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createGlobalVersion<T extends TypeWithID>(
@@ -16,8 +16,11 @@ export async function createGlobalVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const globalTableName = toSnakeCase(globalSlug)
const tableName = `_${globalTableName}_v`
const tableName = getTableName({
adapter: this,
config: global,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,

View File

@@ -3,15 +3,17 @@ import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { CreateMigration } from 'payload/database'
import fs from 'fs'
import { createRequire } from 'module'
import prompts from 'prompts'
import type { PostgresAdapter } from './types.js'
const require = createRequire(import.meta.url)
const migrationTemplate = (
upSQL?: string,
downSQL?: string,
) => `import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-postgres'
import { sql } from 'drizzle-orm'
) => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ payload }: MigrateUpArgs): Promise<void> {
${
@@ -60,9 +62,7 @@ export const createMigration: CreateMigration = async function createMigration(
fs.mkdirSync(dir)
}
const { generateDrizzleJson, generateMigration } = require
? require('drizzle-kit/payload')
: await import('drizzle-kit/payload')
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload')
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')

View File

@@ -3,10 +3,10 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
import { sql } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function createVersion<T extends TypeWithID>(
@@ -21,8 +21,11 @@ export async function createVersion<T extends TypeWithID>(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const collectionTableName = toSnakeCase(collectionSlug)
const tableName = `_${collectionTableName}_v`
const tableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const result = await upsertRow<TypeWithVersion<T>>({
adapter: this,
@@ -40,7 +43,15 @@ export async function createVersion<T extends TypeWithID>(
})
const table = this.tables[tableName]
const relationshipsTable = this.tables[`${tableName}_rels`]
const relationshipsTable =
this.tables[
getTableName({
adapter: this,
config: collection,
relationships: true,
versions: true,
})
]
if (collection.versions.drafts) {
await db.execute(sql`

View File

@@ -2,11 +2,11 @@ import type { DeleteMany } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { inArray } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteMany: DeleteMany = async function deleteMany(
this: PostgresAdapter,
@@ -14,7 +14,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = toSnakeCase(collection)
const tableName = getTableName({ adapter: this, config: collectionConfig })
const result = await findMany({
adapter: this,

View File

@@ -2,13 +2,13 @@ import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne(
@@ -17,7 +17,10 @@ export const deleteOne: DeleteOne = async function deleteOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
let docToDelete: Record<string, unknown>
const { joinAliases, joins, selectFields, where } = await buildQuery({

View File

@@ -3,11 +3,11 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { inArray } from 'drizzle-orm'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const deleteVersions: DeleteVersions = async function deleteVersion(
this: PostgresAdapter,
@@ -16,7 +16,11 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { docs } = await findMany({

View File

@@ -1,38 +1,41 @@
import type { Find } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const find: Find = async function find(
this: PostgresAdapter,
{
collection,
limit: limitArg,
limit,
locale,
page = 1,
pagination,
req = {} as PayloadRequest,
sort: sortArg,
where: whereArg,
where,
},
) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
return findMany({
adapter: this,
fields: collectionConfig.fields,
limit: limitArg,
limit,
locale,
page,
pagination,
req,
sort,
tableName: toSnakeCase(collection),
where: whereArg,
tableName,
where,
})
}

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

@@ -2,11 +2,12 @@
import type { Field } from 'payload/types'
import { fieldAffectsData, tabHasName } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js'
import type { Result } from './buildFindManyArgs.js'
import { getTableName } from '../schema/getTableName.js'
type TraverseFieldArgs = {
_locales: Record<string, unknown>
adapter: PostgresAdapter
@@ -78,9 +79,22 @@ export const traverseFields = ({
with: {},
}
const arrayTableName = `${currentTableName}_${path}${toSnakeCase(field.name)}`
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[`${arrayTableName}_locales`]) withArray.with._locales = _locales
const arrayTableNameWithLocales = getTableName({
adapter,
config: field,
locales: true,
parentTableName: currentTableName,
prefix: `${currentTableName}_${path}`,
})
if (adapter.tables[arrayTableNameWithLocales]) withArray.with._locales = _locales
currentArgs.with[`${path}${field.name}`] = withArray
traverseFields({
@@ -128,9 +142,16 @@ export const traverseFields = ({
with: {},
}
const tableName = `${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`
const tableName = getTableName({
adapter,
config: block,
parentTableName: topLevelTableName,
prefix: `${topLevelTableName}_blocks_`,
})
if (adapter.tables[`${tableName}_locales`]) withBlock.with._locales = _locales
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
withBlock.with._locales = _locales
}
topLevelArgs.with[blockKey] = withBlock
traverseFields({

View File

@@ -1,17 +1,19 @@
import type { FindGlobal } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobal: FindGlobal = async function findGlobal(
this: PostgresAdapter,
{ slug, locale, req, where },
) {
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = toSnakeCase(slug)
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const {
docs: [doc],

View File

@@ -2,11 +2,11 @@ import type { FindGlobalVersions } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: PostgresAdapter,
@@ -27,7 +27,11 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
)
const sort = typeof sortArg === 'string' ? sortArg : '-createdAt'
const tableName = `_${toSnakeCase(global)}_v`
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
return findMany({

View File

@@ -1,17 +1,20 @@
import type { FindOneArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export async function findOne<T extends TypeWithID>(
this: PostgresAdapter,
{ collection, locale, req = {} as PayloadRequest, where: incomingWhere }: FindOneArgs,
{ collection, locale, req = {} as PayloadRequest, where }: FindOneArgs,
): Promise<T> {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = getTableName({
adapter: this,
config: collectionConfig,
})
const { docs } = await findMany({
adapter: this,
@@ -22,8 +25,8 @@ export async function findOne<T extends TypeWithID>(
pagination: false,
req,
sort: undefined,
tableName: toSnakeCase(collection),
where: incomingWhere,
tableName,
where,
})
return docs?.[0] || null

View File

@@ -2,11 +2,11 @@ import type { FindVersions } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const findVersions: FindVersions = async function findVersions(
this: PostgresAdapter,
@@ -25,7 +25,11 @@ export const findVersions: FindVersions = async function findVersions(
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const sort = typeof sortArg === 'string' ? sortArg : collectionConfig.defaultSort
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
return findMany({

View File

@@ -40,6 +40,8 @@ import { updateVersion } from './updateVersion.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(args.migrationDir)
@@ -49,20 +51,24 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
name: 'postgres',
// Postgres-specific
blockTableNames: {},
drizzle: undefined,
enums: {},
fieldConstraints: {},
idType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
pgSchema: undefined,
pool: undefined,
poolOptions: args.pool,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',
schema: {},
schemaName: args.schemaName,
sessions: {},
tables: {},
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter
beginTransaction,

View File

@@ -4,11 +4,11 @@ import type { SanitizedCollectionConfig } from 'payload/types'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildTable } from './schema/build.js'
import { getTableName } from './schema/getTableName.js'
export const init: Init = function init(this: PostgresAdapter) {
if (this.schemaName) {
@@ -25,7 +25,10 @@ export const init: Init = function init(this: PostgresAdapter) {
}
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = toSnakeCase(collection.slug)
const tableName = getTableName({
adapter: this,
config: collection,
})
buildTable({
adapter: this,
@@ -37,10 +40,15 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: collection.fields,
tableName,
timestamps: collection.timestamps,
versions: false,
})
if (collection.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({
adapter: this,
config: collection,
versions: true,
})
const versionFields = buildVersionCollectionFields(collection)
buildTable({
@@ -53,12 +61,13 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})
this.payload.config.globals.forEach((global) => {
const tableName = toSnakeCase(global.slug)
const tableName = getTableName({ adapter: this, config: global })
buildTable({
adapter: this,
@@ -70,10 +79,11 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: global.fields,
tableName,
timestamps: false,
versions: false,
})
if (global.versions) {
const versionsTableName = `_${tableName}_v`
const versionsTableName = getTableName({ adapter: this, config: global, versions: true })
const versionFields = buildVersionGlobalFields(global)
buildTable({
@@ -86,6 +96,7 @@ export const init: Init = function init(this: PostgresAdapter) {
fields: versionFields,
tableName: versionsTableName,
timestamps: true,
versions: true,
})
}
})

View File

@@ -3,6 +3,7 @@ import type { Payload } from 'payload'
import type { Migration } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { createRequire } from 'module'
import {
commitTransaction,
initTransaction,
@@ -17,6 +18,8 @@ import { createMigrationTable } from './utilities/createMigrationTable.js'
import { migrationTableExists } from './utilities/migrationTableExists.js'
import { parseError } from './utilities/parseError.js'
const require = createRequire(import.meta.url)
export async function migrate(this: PostgresAdapter): Promise<void> {
const { payload } = this
const migrationFiles = await readMigrationFiles({ payload })
@@ -82,16 +85,14 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
}
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
const { generateDrizzleJson } = require
? require('drizzle-kit/payload')
: await import('drizzle-kit/payload')
const { generateDrizzleJson } = require('drizzle-kit/payload')
const start = Date.now()
const req = { payload } as PayloadRequest
payload.logger.info({ msg: `Migrating: ${migration.name}` })
const pgAdapter = payload.db as PostgresAdapter
const pgAdapter = payload.db
const drizzleJSON = generateDrizzleJson(pgAdapter.schema)
try {

View File

@@ -14,6 +14,8 @@ import { v4 as uuid } from 'uuid'
import type { GenericColumn, GenericTable, PostgresAdapter } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery.js'
import { getTableName } from '../schema/getTableName.js'
type Constraint = {
columnName: string
table: GenericTable | PgTableWithColumns<any>
@@ -183,7 +185,13 @@ export const getTableColumnFromPath = ({
case 'group': {
if (locale && field.localized && adapter.payload.config.localization) {
newTableName = `${tableName}_locales`
newTableName = getTableName({
adapter,
config: field,
locales: true,
parentTableName: tableName,
prefix: `${tableName}_`,
})
joins[tableName] = eq(
adapter.tables[tableName].id,
@@ -218,7 +226,12 @@ export const getTableColumnFromPath = ({
}
case 'array': {
newTableName = `${tableName}_${tableNameSuffix}${toSnakeCase(field.name)}`
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
constraintPath = `${constraintPath}${field.name}.%.`
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
@@ -265,7 +278,12 @@ export const getTableColumnFromPath = ({
const blockTypes = Array.isArray(value) ? value : [value]
blockTypes.forEach((blockType) => {
const block = field.blocks.find((block) => block.slug === blockType)
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}`
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName]._parentID,
@@ -285,7 +303,12 @@ export const getTableColumnFromPath = ({
}
const hasBlockField = field.blocks.some((block) => {
newTableName = `${tableName}_blocks_${toSnakeCase(block.slug)}`
newTableName = getTableName({
adapter,
config: block,
parentTableName: tableName,
prefix: `${tableName}_blocks_`,
})
constraintPath = `${constraintPath}${field.name}.%.`
let result
const blockConstraints = []
@@ -351,7 +374,7 @@ export const getTableColumnFromPath = ({
case 'relationship':
case 'upload': {
let relationshipFields
const relationTableName = `${rootTableName}_rels`
const relationTableName = `${rootTableName}${adapter.relationshipsSuffix}`
const newCollectionPath = pathSegments.slice(1).join('.')
const aliasRelationshipTableName = uuid()
const aliasRelationshipTable = alias(
@@ -360,22 +383,45 @@ export const getTableColumnFromPath = ({
)
// Join in the relationships table
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
if (locale && field.localized && adapter.payload.config.localization) {
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
eq(aliasRelationshipTable.locale, locale),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
if (locale !== 'all') {
constraints.push({
columnName: 'locale',
table: aliasRelationshipTable,
value: locale,
})
}
} else {
// Join in the relationships table
joinAliases.push({
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
}
selectFields[`${relationTableName}.path`] = aliasRelationshipTable.path
let newAliasTable
if (typeof field.relationTo === 'string') {
newTableName = `${toSnakeCase(field.relationTo)}`
const relationshipConfig = adapter.payload.collections[field.relationTo].config
newTableName = getTableName({
adapter,
config: relationshipConfig,
})
// parent to relationship join table
relationshipFields = adapter.payload.collections[field.relationTo].config.fields
relationshipFields = relationshipConfig.fields
newAliasTable = alias(adapter.tables[newTableName], toSnakeCase(uuid()))
@@ -394,7 +440,11 @@ export const getTableColumnFromPath = ({
}
} else if (newCollectionPath === 'value') {
const tableColumnsNames = field.relationTo.map(
(relationTo) => `"${aliasRelationshipTableName}"."${toSnakeCase(relationTo)}_id"`,
(relationTo) =>
`"${aliasRelationshipTableName}"."${getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
})}_id"`,
)
return {
constraints,
@@ -441,7 +491,7 @@ export const getTableColumnFromPath = ({
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}_locales`
newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName]

View File

@@ -2,9 +2,9 @@ import type { PayloadRequest, SanitizedCollectionConfig } from 'payload/types'
import { type QueryDrafts, combineQueries } from 'payload/database'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import { findMany } from './find/findMany.js'
import { getTableName } from './schema/getTableName.js'
export const queryDrafts: QueryDrafts = async function queryDrafts({
collection,
@@ -17,7 +17,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts({
where,
}) {
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const combinedWhere = combineQueries({ latest: { equals: true } }, where)

View File

@@ -11,10 +11,10 @@ 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 toSnakeCase from 'to-snake-case'
import type { GenericColumns, GenericTable, IDType, PostgresAdapter } from '../types.js'
import { getTableName } from './getTableName.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { setColumnID } from './setColumnID.js'
import { traverseFields } from './traverseFields.js'
@@ -35,6 +35,7 @@ type Args = {
rootTableName?: string
tableName: string
timestamps?: boolean
versions: boolean
}
type Result = {
@@ -59,6 +60,7 @@ export const buildTable = ({
rootTableName: incomingRootTableName,
tableName,
timestamps,
versions,
}: Args): Result => {
const rootTableName = incomingRootTableName || tableName
const columns: Record<string, PgColumnBuilder> = baseColumns
@@ -113,6 +115,7 @@ export const buildTable = ({
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
}))
if (timestamps) {
@@ -147,7 +150,7 @@ export const buildTable = ({
adapter.tables[tableName] = table
if (hasLocalizedField) {
const localeTableName = `${tableName}_locales`
const localeTableName = `${tableName}${adapter.localesSuffix}`
localesColumns.id = serial('id').primaryKey()
localesColumns._locale = adapter.enums.enum__locales('_locale').notNull()
localesColumns._parentID = parentIDColumnMap[idColType]('_parent_id')
@@ -288,20 +291,26 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const formattedRelationTo = toSnakeCase(relationTo)
const relationshipConfig = adapter.payload.collections[relationTo].config
const formattedRelationTo = getTableName({
adapter,
config: relationshipConfig,
throwValidationError: true,
})
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
const relatedCollectionCustomID = adapter.payload.collections[
relationTo
].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id')
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
const relatedCollectionCustomIDType =
adapter.payload.collections[relationshipConfig.slug]?.customIDType
if (relatedCollectionCustomIDType === 'number') colType = 'numeric'
if (relatedCollectionCustomIDType === 'text') colType = 'varchar'
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
`${formattedRelationTo}_id`,
).references(() => adapter.tables[formattedRelationTo].id, { onDelete: 'cascade' })
})
const relationshipsTableName = `${tableName}_rels`
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
@@ -333,7 +342,11 @@ export const buildTable = ({
}
relationships.forEach((relationTo) => {
const relatedTableName = toSnakeCase(relationTo)
const relatedTableName = getTableName({
adapter,
config: adapter.payload.collections[relationTo].config,
throwValidationError: true,
})
const idColumnName = `${relationTo}ID`
result[idColumnName] = one(adapter.tables[relatedTableName], {
fields: [relationshipsTable[idColumnName]],

View File

@@ -0,0 +1,75 @@
import type { DBIdentifierName } from 'payload/database'
import { APIError } from 'payload/errors'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../types.js'
type Args = {
adapter: PostgresAdapter
/** The collection, global or field config **/
config: {
dbName?: DBIdentifierName
enumName?: DBIdentifierName
name?: string
slug?: string
}
/** Localized tables need to be given the locales suffix */
locales?: boolean
/** For nested tables passed for the user custom dbName functions to handle their own iterations */
parentTableName?: string
/** For sub tables (array for example) this needs to include the parentTableName */
prefix?: string
/** Adds the relationships suffix */
relationships?: boolean
/** For tables based on fields that could have both enumName and dbName (ie: select with hasMany), default: 'dbName' */
target?: 'dbName' | 'enumName'
throwValidationError?: boolean
/** Adds the versions suffix, should only be used on the base collection to duplicate suffixing */
versions?: boolean
}
/**
* Used to name database enums and tables
* Returns the table or enum name for a given entity
*/
export const getTableName = ({
adapter,
config: { name, slug },
config,
locales = false,
parentTableName,
prefix = '',
relationships = false,
target = 'dbName',
throwValidationError = false,
versions = false,
}: Args): string => {
let result: string
let custom = config[target]
if (!custom && target === 'enumName') {
custom = config['dbName']
}
if (custom) {
result = typeof custom === 'function' ? custom({ tableName: parentTableName }) : custom
} else {
result = `${prefix}${toSnakeCase(name ?? slug)}`
}
if (locales) result = `${result}${adapter.localesSuffix}`
if (versions) result = `_${result}${adapter.versionsSuffix}`
if (relationships) result = `${result}${adapter.relationshipsSuffix}`
if (!throwValidationError) {
return result
}
if (result.length > 63) {
throw new APIError(
`Exceeded max identifier length for table or enum name of 63 characters. Invalid name: ${result}`,
)
}
return result
}

View File

@@ -27,6 +27,7 @@ import type { GenericColumns, IDType, PostgresAdapter } from '../types.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { getTableName } from './getTableName.js'
import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical.js'
@@ -53,6 +54,7 @@ type Args = {
rootRelationsToBuild?: Map<string, string>
rootTableIDColType: string
rootTableName: string
versions: boolean
}
type Result = {
@@ -86,7 +88,9 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
}: Args): Result => {
const throwValidationError = true
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
@@ -217,7 +221,15 @@ export const traverseFields = ({
case 'radio':
case 'select': {
const enumName = `enum_${newTableName}_${toSnakeCase(field.name)}`
const enumName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `enum_${newTableName}_`,
target: 'enumName',
throwValidationError,
versions,
})
adapter.enums[enumName] = pgEnum(
enumName,
@@ -231,7 +243,14 @@ export const traverseFields = ({
)
if (field.type === 'select' && field.hasMany) {
const selectTableName = `${newTableName}_${toSnakeCase(field.name)}`
const selectTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
versions,
})
const baseColumns: Record<string, PgColumnBuilder> = {
order: integer('order').notNull(),
parent: parentIDColumnMap[parentIDColType]('parent_id')
@@ -266,6 +285,7 @@ export const traverseFields = ({
disableUnique,
fields: [],
tableName: selectTableName,
versions,
})
relationsToBuild.set(fieldName, selectTableName)
@@ -296,7 +316,13 @@ export const traverseFields = ({
case 'array': {
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`
const arrayTableName = getTableName({
adapter,
config: field,
parentTableName: newTableName,
prefix: `${newTableName}_`,
throwValidationError,
})
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
_parentID: parentIDColumnMap[parentIDColType]('_parent_id')
@@ -334,6 +360,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
versions,
})
if (subHasManyTextField) {
@@ -356,7 +383,7 @@ export const traverseFields = ({
}
if (hasLocalesTable(field.fields)) {
result._locales = many(adapter.tables[`${arrayTableName}_locales`])
result._locales = many(adapter.tables[`${arrayTableName}${adapter.localesSuffix}`])
}
subRelationsToBuild.forEach((val, key) => {
@@ -375,7 +402,13 @@ export const traverseFields = ({
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
field.blocks.forEach((block) => {
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}`
const blockTableName = getTableName({
adapter,
config: block,
parentTableName: rootTableName,
prefix: `${rootTableName}_blocks_`,
throwValidationError,
})
if (!adapter.tables[blockTableName]) {
const baseColumns: Record<string, PgColumnBuilder> = {
_order: integer('_order').notNull(),
@@ -416,6 +449,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
tableName: blockTableName,
versions,
})
if (subHasManyTextField) {
@@ -439,7 +473,9 @@ export const traverseFields = ({
}
if (hasLocalesTable(block.fields)) {
result._locales = many(adapter.tables[`${blockTableName}_locales`])
result._locales = many(
adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
)
}
subRelationsToBuild.forEach((val, key) => {
@@ -451,7 +487,7 @@ export const traverseFields = ({
)
adapter.relations[`relations_${blockTableName}`] = blockTableRelations
} else if (process.env.NODE_ENV !== 'production') {
} else if (process.env.NODE_ENV !== 'production' && !versions) {
validateExistingBlockIsIdentical({
block,
localized: field.localized,
@@ -459,7 +495,7 @@ export const traverseFields = ({
table: adapter.tables[blockTableName],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName
rootRelationsToBuild.set(`_blocks_${block.slug}`, blockTableName)
})
@@ -498,6 +534,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -540,6 +577,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -583,6 +621,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (tabHasLocalizedField) hasLocalizedField = true
@@ -626,6 +665,7 @@ export const traverseFields = ({
rootRelationsToBuild,
rootTableIDColType,
rootTableName,
versions,
})
if (rowHasLocalizedField) hasLocalizedField = true

View File

@@ -24,11 +24,14 @@ export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = {
idType?: 'serial' | 'uuid'
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
push?: boolean
relationshipsSuffix?: string
schemaName?: string
versionsSuffix?: string
}
export type GenericColumn = PgColumn<
@@ -58,6 +61,10 @@ export type DrizzleTransaction = PgTransaction<
>
export type PostgresAdapter = BaseDatabaseAdapter & {
/**
* Used internally to map the block name to the table name
*/
blockTableNames: Record<string, string>
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
/**
@@ -66,12 +73,14 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
*/
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
localesSuffix?: string
logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
@@ -82,6 +91,7 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
}
}
tables: Record<string, GenericTable | PgTableWithColumns<any>>
versionsSuffix?: string
}
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
@@ -98,9 +108,11 @@ declare module 'payload' {
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>>
localeSuffix?: string
pool: Pool
push: boolean
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
sessions: {
[id: string]: {
@@ -110,5 +122,6 @@ declare module 'payload' {
}
}
tables: Record<string, GenericTable>
versionsSuffix?: string
}
}

View File

@@ -1,11 +1,10 @@
import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -14,7 +13,10 @@ export const updateOne: UpdateOne = async function updateOne(
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const tableName = getTableName({
adapter: this,
config: collection,
})
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id
@@ -49,7 +51,7 @@ export const updateOne: UpdateOne = async function updateOne(
fields: collection.fields,
operation: 'update',
req,
tableName: toSnakeCase(collectionSlug),
tableName,
})
return result

View File

@@ -1,10 +1,9 @@
import type { UpdateGlobalArgs } from 'payload/database'
import type { PayloadRequest, TypeWithID } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobal<T extends TypeWithID>(
@@ -13,7 +12,10 @@ export async function updateGlobal<T extends TypeWithID>(
): Promise<T> {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = toSnakeCase(slug)
const tableName = getTableName({
adapter: this,
config: globalConfig,
})
const existingGlobal = await db.query[tableName].findFirst({})

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateGlobalVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedGlobalConfig, TypeWithID } from 'payload/types'
import { buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateGlobalVersion<T extends TypeWithID>(
@@ -25,7 +25,11 @@ export async function updateGlobalVersion<T extends TypeWithID>(
({ slug }) => slug === global,
)
const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${toSnakeCase(global)}_v`
const tableName = getTableName({
adapter: this,
config: globalConfig,
versions: true,
})
const fields = buildVersionGlobalFields(globalConfig)
const { where } = await buildQuery({

View File

@@ -2,11 +2,11 @@ import type { TypeWithVersion, UpdateVersionArgs } from 'payload/database'
import type { PayloadRequest, SanitizedCollectionConfig, TypeWithID } from 'payload/types'
import { buildVersionCollectionFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { getTableName } from './schema/getTableName.js'
import { upsertRow } from './upsertRow/index.js'
export async function updateVersion<T extends TypeWithID>(
@@ -23,7 +23,11 @@ export async function updateVersion<T extends TypeWithID>(
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } }
const tableName = `_${toSnakeCase(collection)}_v`
const tableName = getTableName({
adapter: this,
config: collectionConfig,
versions: true,
})
const fields = buildVersionCollectionFields(collectionConfig)
const { where } = await buildQuery({

View File

@@ -1,9 +1,12 @@
import { eq } from 'drizzle-orm'
import { numeric, timestamp, varchar } from 'drizzle-orm/pg-core'
import { createRequire } from 'module'
import prompts from 'prompts'
import type { PostgresAdapter } from '../types.js'
const require = createRequire(import.meta.url)
/**
* Pushes the development schema to the database using Drizzle.
*
@@ -11,9 +14,7 @@ import type { PostgresAdapter } from '../types.js'
* @returns {Promise<void>} - A promise that resolves once the schema push is complete.
*/
export const pushDevSchema = async (db: PostgresAdapter) => {
const { pushSchema } = require
? require('drizzle-kit/payload')
: await import('drizzle-kit/payload')
const { pushSchema } = require('drizzle-kit/payload')
// This will prompt if clarifications are needed for Drizzle to push new schema
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-alpha.50",
"version": "3.0.0-beta.10",
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",

View File

@@ -238,12 +238,14 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
resolve: getDeleteResolver(collection),
}
graphqlResult.Mutation.fields[`duplicate${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: duplicateResolver(collection),
if (collectionConfig.disableDuplicate !== true) {
graphqlResult.Mutation.fields[`duplicate${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: duplicateResolver(collection),
}
}
if (collectionConfig.versions) {

View File

@@ -26,7 +26,7 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "^18.2.0",
"@types/react": "18.2.74",
"payload": "workspace:*"
},
"peerDependencies": {
@@ -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

@@ -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"

15
packages/next/.swcrc-cjs Normal file
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": "commonjs"
}
}

View File

@@ -1,54 +0,0 @@
import path from 'path'
import fs from 'fs'
import { copyRecursiveSync } from './utilities/copyRecursiveSync'
import { copyFile } from './utilities/copyFile'
const install = () => {
const useSrc = fs.existsSync(path.resolve(process.cwd(), './src'))
const hasAppFolder = fs.existsSync(path.resolve(process.cwd(), `./${useSrc ? 'src/' : 'app'}`))
if (!hasAppFolder) {
console.error(
`You need to have a ${
useSrc ? 'src/' : 'app/'
} folder in your project before running this command.`,
)
process.exit(1)
}
const basePath = useSrc ? './src' : '.'
// Copy handlers into /api
copyRecursiveSync(
path.resolve(__dirname, './templates/pages/api'),
path.resolve(process.cwd(), `${basePath}/pages/api`),
)
// Copy admin into /app
copyRecursiveSync(
path.resolve(__dirname, './templates/app'),
path.resolve(process.cwd(), `${basePath}/app`),
)
const payloadConfigPath = path.resolve(process.cwd(), `${basePath}/payload`)
if (!fs.existsSync(payloadConfigPath)) {
fs.mkdirSync(payloadConfigPath)
}
// Copy payload initialization
copyFile(
path.resolve(__dirname, './templates/payloadClient.ts'),
path.resolve(process.cwd(), `${basePath}/payload/payloadClient.ts`),
)
// Copy base payload config
copyFile(
path.resolve(__dirname, './templates/payload.config.ts'),
path.resolve(process.cwd(), `${basePath}/payload/payload.config.ts`),
)
process.exit(0)
}
export default install()

View File

@@ -1,7 +0,0 @@
import fs from 'fs'
export const copyFile = (source, target) => {
if (!fs.existsSync(target)) {
fs.writeFileSync(target, fs.readFileSync(source))
}
}

View File

@@ -1,16 +0,0 @@
import fs from 'fs'
import path from 'path'
export function copyRecursiveSync(src, dest) {
var exists = fs.existsSync(src)
var stats = exists && fs.statSync(src)
var isDirectory = exists && stats && stats.isDirectory()
if (isDirectory) {
fs.mkdirSync(dest, { recursive: true })
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
})
} else {
fs.copyFileSync(src, dest)
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-alpha.50",
"version": "3.0.0-beta.10",
"main": "./src/index.js",
"types": "./src/index.js",
"type": "module",
@@ -10,11 +10,9 @@
"url": "https://github.com/payloadcms/payload.git",
"directory": "packages/next"
},
"bin": {
"@payloadcms/next": "./dist/bin/index.js"
},
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:webpack && rm dist/prod/index.js",
"build:cjs": "swc ./src/withPayload.js -o ./dist/cjs/withPayload.cjs --config-file .swcrc-cjs",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:cjs && pnpm build:types && pnpm build:webpack && rm dist/prod/index.js",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:webpack": "webpack --config webpack.config.js",
@@ -30,6 +28,10 @@
"require": "./src/index.js",
"types": "./src/index.js"
},
"./withPayload": {
"import": "./src/withPayload.js",
"require": "./src/withPayload.js"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
@@ -39,8 +41,8 @@
"devDependencies": {
"@next/eslint-plugin-next": "^14.1.0",
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
"@types/react": "18.2.74",
"@types/react-dom": "18.2.24",
"@types/ws": "^8.5.10",
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
@@ -75,21 +77,21 @@
},
"peerDependencies": {
"http-status": "1.6.2",
"next": "14.2.0-canary.23",
"next": "^14.2.0-canary.23",
"payload": "workspace:*"
},
"publishConfig": {
"main": "./dist/index.js",
"types": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./css": {
"import": "./dist/prod/styles.css",
"require": "./dist/prod/styles.css",
"default": "./dist/prod/styles.css"
},
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
"./withPayload": {
"import": "./dist/withPayload.js",
"require": "./dist/cjs/withPayload.cjs"
},
"./*": {
"import": "./dist/exports/*.js",
@@ -100,7 +102,7 @@
"registry": "https://registry.npmjs.org/"
},
"engines": {
"node": ">=18.17.0"
"node": ">=18.20.2"
},
"files": [
"dist"

View File

@@ -1 +1,2 @@
export { getNextRequestI18n } from '../utilities/getNextRequestI18n.js'
export { getPayloadHMR } from '../utilities/getPayloadHMR.js'

View File

@@ -1,16 +1,18 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { translations } from '@payloadcms/translations/client'
import { rtlLanguages } from '@payloadcms/translations'
import { initI18n } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui/providers/Root'
import '@payloadcms/ui/scss/app.scss'
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { parseCookies } from 'payload/auth'
import { createClientConfig } from 'payload/config'
import { deepMerge } from 'payload/utilities'
import React from 'react'
import 'react-toastify/dist/ReactToastify.css'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
import { DefaultEditView } from '../../views/Edit/Default/index.js'
import { DefaultListView } from '../../views/List/Default/index.js'
@@ -20,7 +22,15 @@ export const metadata = {
title: 'Next.js',
}
const rtlLanguages = ['ar', 'fa', 'ha', 'ku', 'ur', 'ps', 'dv', 'ks', 'khw', 'he', 'yi']
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,
@@ -30,32 +40,43 @@ export const RootLayout = async ({
config: Promise<SanitizedConfig>
}) => {
const config = await configPromise
const clientConfig = await createClientConfig(config)
const headers = getHeaders()
const cookies = parseCookies(headers)
const lang =
getRequestLanguage({
config,
cookies,
headers,
}) ?? clientConfig.i18n.fallbackLanguage
const languageCode = getRequestLanguage({
config,
cookies,
headers,
})
const dir = rtlLanguages.includes(lang) ? 'RTL' : 'LTR'
const payload = await getPayloadHMR({ config })
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
const clientConfig = await createClientConfig({ config, t: i18n.t })
const mergedTranslations = deepMerge(translations, clientConfig.i18n.translations)
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
: 'LTR'
const languageOptions = Object.entries(translations || {}).map(([language, translations]) => ({
label: translations.general.thisLanguage,
value: language,
}))
const languageOptions = Object.entries(config.i18n.supportedLanguages || {}).reduce(
(acc, [language, languageConfig]) => {
if (Object.keys(config.i18n.supportedLanguages).includes(language)) {
acc.push({
label: languageConfig.translations.general.thisLanguage,
value: language,
})
}
return acc
},
[],
)
// eslint-disable-next-line @typescript-eslint/require-await
async function switchLanguageServerAction(lang: string): Promise<void> {
'use server'
nextCookies().set({
name: `${config.cookiePrefix || 'payload'}-lng'`,
name: `${config.cookiePrefix || 'payload'}-lng`,
path: '/',
value: lang,
})
@@ -66,20 +87,23 @@ export const RootLayout = async ({
DefaultListView,
children,
config,
i18n,
payload,
})
return (
<html dir={dir} lang={lang}>
<html className={merriweather.variable} dir={dir} lang={languageCode}>
<body>
<RootProvider
componentMap={componentMap}
config={clientConfig}
dateFNSKey={i18n.dateFNSKey}
fallbackLang={clientConfig.i18n.fallbackLanguage}
lang={lang}
languageCode={languageCode}
languageOptions={languageOptions}
// eslint-disable-next-line react/jsx-no-bind
switchLanguageServerAction={switchLanguageServerAction}
translations={mergedTranslations[lang]}
translations={i18n.translations}
>
{wrappedChildren}
</RootProvider>

View File

@@ -5,7 +5,7 @@ import { logoutOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
export const logout: CollectionRouteHandler = async ({ collection, req }) => {
const result = logoutOperation({
const result = await logoutOperation({
collection,
req,
})

View File

@@ -1,11 +1,5 @@
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
import type {
DocumentPreferences,
Field,
PayloadRequest,
SanitizedConfig,
TypeWithID,
} from 'payload/types'
import type { DocumentPreferences, Field, PayloadRequest, TypeWithID } from 'payload/types'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
@@ -22,12 +16,12 @@ if (!cached) {
cached = global._payload_fieldSchemaMap = null
}
export const getFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
export const getFieldSchemaMap = (req: PayloadRequest): FieldSchemaMap => {
if (cached && process.env.NODE_ENV !== 'development') {
return cached
}
cached = buildFieldSchemaMap(config)
cached = buildFieldSchemaMap(req)
return cached
}
@@ -46,7 +40,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction(req)
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
return Response.json(null, {
@@ -65,7 +59,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
})
}
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
const fieldSchemaMap = getFieldSchemaMap(req)
const id = collectionSlug ? reqData.id : undefined
const schemaPathSegments = schemaPath.split('.')
@@ -162,7 +156,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
})
}
if (globalSlug) {
if (globalSlug && schemaPath === globalSlug) {
resolvedData = await req.payload.findGlobal({
slug: globalSlug,
depth: 0,

View File

@@ -4,9 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const deleteByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const deleteByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const doc = await deleteByIDOperation({
id,
collection,

View File

@@ -5,12 +5,24 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const duplicate: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const duplicate: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
// draft defaults to true, unless explicitly set requested as false to prevent the newly duplicated document from being published
const draft = searchParams.get('draft') !== 'false'
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const doc = await duplicateOperation({
id,
collection,

View File

@@ -4,10 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const findByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const findByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const result = await findByIDOperation({
id,
collection,

View File

@@ -4,10 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const findVersionByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const findVersionByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const result = await findVersionByIDOperation({
id,
collection,

View File

@@ -1,4 +1,5 @@
import httpStatus from 'http-status'
import { extractJWT } from 'payload/auth'
import { findByIDOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
@@ -24,11 +25,14 @@ export const preview: CollectionRouteHandlerWithID = async ({ id, collection, re
(config) => config.slug === collection.config.slug,
)?.admin?.preview
const token = extractJWT(req)
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
req,
token,
})
} catch (err) {
routeError({

View File

@@ -4,10 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const restoreVersion: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const restoreVersion: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const result = await restoreVersionOperation({
id,
collection,

View File

@@ -4,12 +4,24 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const updateByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const updateByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const autosave = searchParams.get('autosave') === 'true'
const draft = searchParams.get('draft') === 'true'
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const doc = await updateByIDOperation({
id,
autosave,

View File

@@ -1,4 +1,5 @@
import httpStatus from 'http-status'
import { extractJWT } from 'payload/auth'
import { findOneOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
@@ -24,11 +25,14 @@ export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
(config) => config.slug === globalConfig.slug,
)?.admin?.preview
const token = extractJWT(req)
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
req,
token,
})
} catch (err) {
routeError({

View File

@@ -378,7 +378,7 @@ export const POST =
res = await (
endpoints.collection.POST[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
)({ id: slug3, collection, req })
} else if (slug3 === 'duplicate') {
} else if (slug3 === 'duplicate' && collection.config.disableDuplicate !== true) {
// /:collection/:id/duplicate
res = await endpoints.collection.POST.duplicate({ id: slug2, collection, req })
}

View File

@@ -1,7 +1,7 @@
import type { Collection, PayloadRequest } from 'payload/types'
import httpStatus from 'http-status'
import { APIError, ValidationError } from 'payload/errors'
import { APIError } from 'payload/errors'
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }

View File

@@ -0,0 +1,23 @@
import type { Payload } from 'payload/types'
type Args = {
collectionSlug: string
id: string
payload: Payload
}
export const sanitizeCollectionID = ({ id, collectionSlug, payload }: Args): number | string => {
let sanitizedID: number | string = id
const collection = payload.collections[collectionSlug]
// If default db ID type is a number, we should sanitize
let shouldSanitize = Boolean(payload.db.defaultIDType === 'number')
// UNLESS the customIDType for this collection is text.... then we leave it
if (shouldSanitize && collection.customIDType === 'text') shouldSanitize = false
// If we still should sanitize, parse float
if (shouldSanitize) sanitizedID = parseFloat(sanitizedID)
return sanitizedID
}

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