Compare commits

..

55 Commits

Author SHA1 Message Date
Elliot DeNolf
18ac47c1fc chore: rebase, fix up 2025-01-15 12:21:04 -05:00
Elliot DeNolf
e6f98d1ba9 chore: add writeDefaultAdapter flag, jest args were interfering 2025-01-15 12:19:28 -05:00
Elliot DeNolf
fb148fff0f chore: some strictNullChecks mitigation 2025-01-15 12:19:27 -05:00
Elliot DeNolf
aa0b6ebf61 chore(tests): uncomment more suites to build 2025-01-15 12:19:27 -05:00
Elliot DeNolf
549b8c8e87 chore(tests): add prebuild to ensure database adapter exists 2025-01-15 12:19:22 -05:00
Elliot DeNolf
f5ea7b47cf build: partial test suite build, runs with build:all 2025-01-15 12:19:12 -05:00
Dan Ribbens
05b9d94cd2 fix: delete scheduled publish jobs when deleting documents (#10584)
### What?

When a document gets deleted we are not cleaning up jobs that would fail
if the document doesn't exist. This change makes an extra call to the DB
to delete any incomplete jobs for the document.

### Why?

The jobs queue will error and retry needlessly unless these are purged.

### How?

Adds a call to delete jobs from the delete operation.
2025-01-15 16:22:21 +00:00
Germán Jabloñski
d4039f2f9e chore: enable noUncheckedIndexedAccess in all packages except richtext-lexical (#10592)
richtext-lexical throws a lot of errors so it will need a separate PR
2025-01-15 16:09:10 +00:00
DracoBlue
7a392ddbff fix: UpsertArgs is not exported in payload (#9347)
### What?

While working on a custom database adapter (I know I am crazy for this)
I noticed that UpsertArgs is not exported when doing:

```
import {
  type UpsertArgs
} from 'payload'

```

it results in:

```
Error: src/index.ts(21,8): error TS2614: Module '"payload"' has no exported member 'UpsertArgs'. Did you mean to use 'import UpsertArgs from "payload"' instead?
```

### Why?

Because index.ts in packages/payload/src/index.ts includes Upsert but
not UpsertArgs in export.

### How?

Add the export from UpsertArgs back.
2025-01-15 10:56:54 -05:00
Germán Jabloñski
d55b6a3db9 chore: enable noImplicitOverride in all packages (#10588) 2025-01-15 10:06:40 +00:00
Sasha
9043b10792 fix(db-mongodb): incorrect errors logging due to invalid logic in handleError (#10575)
Previously, every error from MongoDB was logged as "Value must be
unique", as well the response code should not be `BAD_REQUEST` but
`INTERNAL_SERVER_ERROR`. `throw error` preserves the original error so
it can be traced.
2025-01-15 11:02:09 +02:00
Alessio Gravili
ecf05725e6 fix(richtext-slate): link and upload extra field drawers did not render fields if collection has unrelated access control set (#10583)
Fixes https://github.com/payloadcms/payload/issues/9695
2025-01-15 08:11:24 +00:00
Dan Ribbens
918bd72335 chore: update mongodb-memory-server v9 -> v10 (#10556)
Updated version of mongodb-memory-server to 10.
2025-01-14 22:38:31 -05:00
Elliot DeNolf
4629784c99 ci(scripts): publish-canary script always bump minor, more realistic [skip ci][skip lint] 2025-01-14 21:16:11 -05:00
Germán Jabloñski
a304dc4b01 chore: make TypeScript strict in test folder. Simplify tsconfig (#10582)
This PR makes the "test" folder strict in typescript.

`pnpm build:test` before: Found 3275 errors in 174 files.
`pnpm build:test` after: Found 4912 errors in 268 files.

At some point we should bring that number to 0 and make it a requirement
in the CI. Currently `pnpm build:test` is not run anywhere in the CI.

Additionally, I took the opportunity to combine the duplicate
configurations from `tsconfig.json` and `tsconfig.typecheck.json` using
"extend".

declaration, declarationMap and sourceMap have been removed as they have
no reason to exist in noEmit.

The settings I left in `tsconfig.typecheck.json` are ones that I'm not
sure why they are there. Perhaps the file could be removed or at least
reduced further.
2025-01-14 20:00:00 -03:00
Alessio Gravili
8ab05b0c22 fix(richtext-lexical): setting hideInsertParagraphAtEnd to true did not hide insert paragraph button (#10581) 2025-01-14 22:16:39 +00:00
Germán Jabloñski
085c1d0cac chore: make TypeScript strict by default in packages and 7 packages stricter (#10579)
This PR modifies `tsconfig.base.json` by setting the following
strictness properties to true: `strict`, `noUncheckedIndexedAccess` and
`noImplicitOverride`.

In packages where compilation errors were observed, these settings were
opted out, and TODO comments were added to make it easier to track the
roadmap for converting everything to strict mode.

The following packages now have increased strictness, which prevents new
errors from being accidentally introduced:

- storage-vercel-blob
- storage-s3*
- storage-gcs
- plugin-sentry
- payload-cloud*
- email-resend*
- email-nodemailer*

*These packages already had `strict: true`, but now have
`noUncheckedIndexedAccess` and `noImplicitOverride`.

Note that this only affects the `/packages` folder, but not
`/templates`, `/test` or `/examples` which have a different `tsconfig`.
2025-01-14 21:39:40 +00:00
Alessio Gravili
61117ee5cb fix(richtext-lexical): inline blocks did not store nested fields correctly (#10578)
Fixes https://github.com/payloadcms/payload/issues/10555

Form state with nested fields was not unflattened before saving field
data to the node
2025-01-14 21:17:25 +00:00
Jacob Fletcher
05b03b2dcd fix: form state read access control args (#10576)
The `access.read` function executed within form state was missing the
`id` arg, and was also incorrectly setting `data` as `doc`. When
building form state, there is no concept of a "doc" because it is
possible to build form state using only a subset of fields. There is
"data", however, which represents the schema path at the entry point of
the function. Similarly, when building form state on within an
`onChange` function, for example, we do not send the original doc
through the request, which is what "doc" would represent. Instead, we
send either `data` or `formState`, both of which could represent a
_modified_ doc. This particular invocation of read access does not
effect the visibility of fields themselves, but rather their return
values from the form state endpoint. Field visibility is determined at
the request level.
2025-01-14 16:05:38 -05:00
Sasha
120735c55c fix: missing find collection versions REST endpoint (#10573)
The `/api/:collection/versions` endpoint was missing, added a test to
prevent regressions like this.
2025-01-14 20:35:32 +02:00
Germán Jabloñski
16ad7a671f fix(payload-cloud): add ts strict mode and fix a couple of wrong runtime behaviors (#10570) 2025-01-14 16:14:37 +00:00
Jacob Fletcher
31ae27b67d perf: significantly reduce form state response size by up to 3x (#9388)
This significantly optimizes the form state, reducing its size by up to
more than 3x and improving overall response times. This change also has
rolling effects on initial page size as well, where the initial state
for the entire form is sent through the request. To achieve this, we do
the following:
- Remove `$undefined` strings that are potentially attached to
properties like `value`, `initialValue`, `fieldSchema`, etc.
- Remove unnecessary properties like empty `errorPaths` arrays and empty
`customComponents` objects, which only need to exist if used
- Remove unnecessary properties like `valid`, `passesCondition`, etc.
which only need to be returned if explicitly `false`
- Remove unused properties like `isSidebar`, which simply don't need to
exist at all, as they can be easily calculated during render

## Results

The following results were gathered by booting up each test suite listed
below using the existing seed data, navigating to a document in the
relevant collection, then typing a single letter into the noted field in
order to invoke new form-state. The result is then saved to the file
system for comparison.

| Test Suite | Collection | Field | Before | After | Percentage Change |
|------|------|---------|--------|--------|--------|
| `field-perf` | `blocks-collection` | `layout.0.field1` | 227kB | 110
kB | ~52% smaller |
| `fields` | `array-fields` | `items.0.text` | 14 kB | 4 kB | ~72%
smaller |
| `fields` | `block-fields` | `blocks.0.richText` | 25 kB | 14 kB | ~44%
smaller |
2025-01-14 10:45:54 -05:00
Jessica Chowdhury
8217842bb3 docs: add section on localized access control (#10567) 2025-01-14 08:36:46 -05:00
Alessio Gravili
2e09da8a8c feat(richtext-lexical): add jsx and html converters for tab nodes (#10565) 2025-01-14 07:55:51 +00:00
Alessio Gravili
5d6c29f3df perf(richtext-lexical): ensure internal link nodes do not store url field, and vice versa (#10564)
Previously, the url field of a link was stored and outputted despite the
link being an internal link. This PR ensures that either the link url,
or the link doc is stored and outputted - never both.
2025-01-14 07:46:56 +00:00
Alessio Gravili
df4af70fb9 fix(richtext-lexical): ensure jsx and html converters do not output linebreak if editor is empty (#10563)
If you add text to the editor, then delete it using ctrl+a + delete, one
empty paragraph that cannot be removed remains in the editor state.

In order to account for this, we have a `hasText()` function - this,
however, was not used in our JSX and HTML converters. This caused the
converters to incorrectly output a linebreak if said empty editor state
was passed in.
2025-01-14 06:31:34 +00:00
Suphon T.
90e1843795 fix: basePath was not passed through if method was overriden (#10562)
Original issue: #10534 
The original issue was partially fixed by #10535, but it missed a case
that overrides a post method with get.
This PR passes the `basePath` to the overridden call.
2025-01-14 06:24:43 +00:00
Elliot DeNolf
5ee36fced3 ci: access sha in dispatch event 2025-01-13 21:12:30 -05:00
Elliot DeNolf
f306785eb2 ci: dispatch event 2025-01-13 21:02:26 -05:00
Alessio Gravili
6a6ef8f786 chore(richtext-lexical): add unit test that ensures lexical dependency checker is updated (#10561) 2025-01-14 01:50:28 +00:00
Elliot DeNolf
a865a902d5 chore(release): v3.17.1 [skip ci] 2025-01-13 19:57:13 -05:00
Alessio Gravili
878763b36d fix(richtext-lexical): incorrect lexical version in dependency checker (#10559) 2025-01-13 19:56:30 -05:00
Elliot DeNolf
3c29015887 chore(release): v3.17.0 [skip ci] 2025-01-13 16:24:41 -05:00
Alessio Gravili
9631060383 fix(richtext-lexical): error when deleting links (#10557)
When pressing the delete button in the floating link popup, the link was
not deleted and a console error was thrown
2025-01-13 21:11:18 +00:00
Elliot DeNolf
5cfb1daaae fix: respect res header immutability (#10554)
Properly respect and merge res headers.
2025-01-13 15:41:46 -05:00
Elliot DeNolf
9278eec2b6 fix: better messaging when no arg passed to payload cli (#10550)
Better error message when no argument is passed to `pnpm payload`.

Before:
```
Unknown script: "".
```

After:
```
Please provide a command to run
Available commands:
  - command-1
  - command-2
  - etc.
```
2025-01-13 20:13:54 +00:00
Germán Jabloñski
a3ef5eee7b fix(ui): reset pagination when typing in WhereBuilder select menu (#10551)
After working on this I found a more accurate way to reproduce the bug:

- in the issue repro, type a letter in the select menu.
- delete the letter and wait for the debounce (300ms)
- type another letter.
- in devtools, you should see that the query increases the pagination by
+1. With this change, the pagination is reset when the input changes.

Fixes #10496

I'd like to do integration testing. But since there is no isolated `/ui`
test yet, this requires some planning. I have it pending.
2025-01-13 16:43:04 -03:00
Dan Ribbens
f95d6ba94a feat: delete scheduled published events (#10504)
### What?

Allows a user to delete a scheduled publish event after it has been
added:

![image](https://github.com/user-attachments/assets/79b1a206-c8a7-4ffa-a9bf-d0f84f86b8f9)

### Why?

Previously a user had no control over making changes once scheduled.

### How?

Extends the `scheduledPublishHandler` server action to accept a
`deleteID` for the event that should be removed and exposes this to the
user via the admin UI in a new column in the Upcoming Events table.
2025-01-13 19:41:38 +00:00
Alessio Gravili
6ada450531 fix(richtext-lexical): insert paragraph at end button overlaps floating link toolbar (#10552) 2025-01-13 19:41:14 +00:00
Elliot DeNolf
9004205b84 fix(cpa): proper debug logging (#10549)
Debug logs were improperly running without debug flag being passed.
2025-01-13 14:14:46 -05:00
Alessio Gravili
6757f7d459 feat(richtext-lexical): add new paragraph button below the editor (#10530)
Fixes https://github.com/payloadcms/payload/issues/10448


https://github.com/user-attachments/assets/dfcd4ab6-8e41-4a1b-b642-876a0737d9ae
2025-01-13 19:08:00 +00:00
Anthony Mifsud
2ae26d33e3 chore(cpa): fixes typo in messages.ts (#10342) 2025-01-13 19:07:11 +00:00
Ben Löffel
5043a8a43f docs: improves grammar in vercel postgres usage note (#10365)
Refined the grammar and structure of the usage note for
`vercelPostgresAdapter`. Replaced the ambiguous phrase "If when using"
with "If you are using" for better readability and clarity.
2025-01-13 13:36:39 -05:00
Jacob Fletcher
6848cf43ed fix(ui): passes serverProps to custom label components within table columns (#10547)
Continuation of #10540. Passes server props to custom label components
rendered within table columns, such as the list view. This way custom
server components can have access to `payload`, `i18n`, etc. as
expected.
2025-01-13 18:23:52 +00:00
Tristan
2e0595b170 fix(translations): update etTranslations type to DefaultTranslationsObject (#10358)
After merging this PR: https://github.com/payloadcms/payload/pull/10169
the estonian language pack has been published, but since the translation
type was not correct, it meant en wasn't used as a fallback lanugage,
which resulted the whole app to crash:
In the Browser the following error is shown, if Estonian language is
picked.
```
Error: Cannot read properties of undefined (reading 'default')
    at resolveErrorDev (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0_sass@1.77.4/node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:1792:63)
    at processFullStringRow (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0_sass@1.77.4/node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2071:17)
    at processFullBinaryRow (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0_sass@1.77.4/node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2059:7)
    at progress (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0_sass@1.77.4/node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2262:17)
```

Fixes #

This is now fixed by adding the correct type to the translation object.
2025-01-13 13:10:11 -05:00
Jarrod Flesch
43b40f0b00 docs: updates docs to reflect correct array hook usage (#10546)
### What?
The documentation for `addFieldRow` and `replaceFieldRow` was not
updated during the v2 -> v3 update.

### How?
Updates the documentation for `addFieldRow` and `replaceFieldRow`.

Fixes #9244
2025-01-13 13:00:40 -05:00
Paul
c9584a932a fix(ui): scheduled publish not showing related events in postgres (#10481)
Since postgres uses number IDs by default, when we were storing the
relationship field value with postgres we weren't able to query it

This fixes that problem by casting the ID to always a string making it
safe for querying inside the JSON field
2025-01-13 12:35:27 -05:00
Simon Vreman
69fac593ca fix(richtext-lexical): remove alteration of lexical text format constant (#10415)
In PR https://github.com/payloadcms/payload/pull/9507, which aims to
enable only used formats to be enabled in lexical, the
`TEXT_TYPE_TO_FORMAT` constant in the lexical library was altered. This
means it becomes impossible to create a feature relying on the
`highlight` format. I am of the opinion that this should not be the
case; and have used this for a lexical feature in one of my projects.
The type of `enabledFormats` of the `createClientFeature` function
should also be updated to reflect the availability of the format.

This PR aims to:
- Remove the alteration to the library constant
- Update type of `enabledFormats`
2025-01-13 14:14:03 -03:00
Amelia
415fbf1341 fix(ui): table custom label missing client field props (#10540)
Fixes #9663. The `field` prop was not passed to custom label components
within the list view table.

<img width="1366" alt="Screenshot 2025-01-13 at 16 05 34"
src="https://github.com/user-attachments/assets/efae9f92-ffad-46dd-aec8-e1f968f9f278"
/>

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-01-13 16:58:46 +00:00
Jarrod Flesch
cc13ae77fb fix: aligns first render for hydration of dates in list view (#10541)
### What?
The list view was throwing a hydration error for date fields.

### Why?
The issue really stems from the fact that cells are client rendered. We
dynamically load the dateFNS Locale object at runtime to keep the bundle
size small — which makes sense. But on the first render that means we do
not have the Locale object from the known locale so the server/client
determines what to render it as. This causes a mismatch when hydrating.

In the future I think cells could be server rendered and that would
solve the need for this fix which adds "Loading..." while the dateFNS
Locale is loaded.

I think server rendering the cells would allow us to import the dateFNS
Locale inline (blocking) and then pass the rendered string down to the
list view. This should work because we **know** the locale on the
server.

### How?
In this PR, it adds a "Loading..." fallback for the date cell if the
dateFNS Locale has not loaded yet.

Fixes #10044
2025-01-13 11:45:05 -05:00
Jacob Fletcher
afcc970e36 fix(next): ensures req.locale is populated before running access control (#10533)
Fixes #10529. The `req.locale` property within collection and global
access control functions does not reflect the current locale. This was
because we were attaching the locale to the req only _after_ running
`payload.auth`, which attempts to get access control without a
fully-formed req. The fix is to first authenticate the user using the
`executeAuthStrategies` operation directly, then determine the request
locale with that user, and finally get access results with the proper
locale.
2025-01-13 10:33:27 -05:00
Paul
6b051bd59e feat: add ability to disable cache tags for admin thumbnails (#10319)
This PR adds `cacheTags: boolean` (default `true`) to allow users to
disable the appended document updatedAt value in the case of hosting
with third party CDNs which may not allow additional search params and
throw an error.

It also fixes how we append this value to consider the case where the
URL already contains parameters and appends it with `&` instead.

In the future `cacheTags` can be made an object to allow granularity for
disabling `eTag` headers used for caching as well.

The cache tag control should help with these two issues:
- Fixes https://github.com/payloadcms/payload/issues/9880
- Fixes https://github.com/payloadcms/payload/issues/9993

The appending of the value correctly addresses this:
- Fixes https://github.com/payloadcms/payload/issues/10139
2025-01-13 15:26:47 +00:00
Paul
082c4f0d71 fix(ui): fixed issue with updatedAt timestamps not updating in the UI when drafts are updated (#10503)
Fixes https://github.com/payloadcms/payload/issues/10436

Fixes an issue where drafts' updatedAt timestamp is not being updated.
We weren't updating the `versionData` to have the right timestamp in the
saveVersion operation when drafts were being updated.

Added e2e tests to make sure 'Last Modified' is always different in both
autosave and non-autosave drafts.
2025-01-13 09:01:34 -06:00
Germán Jabloñski
0252681313 fix(richtext-lexical): combine 2 normalizeMarkdown implementations and fix code block regex (#10470)
This should fix it https://github.com/payloadcms/payload/issues/10387

I don't know why we had 2 different copies of normalizeMarkdown.

Also, the most up-to-date one still had a bug where lines were
considered as if they were inside codeblocks when they weren't.

How I tested that it works:

1. I copied the `normalizeMarkdown` implementation from this PR into the
website repo, and made sure it is called before the conversion to
editorState.
2. In the admin panel, sync docs.
3. In the admin panel, refresh mdx to lexical (new button, below sync
docs).
4. Look for the examples from bug #10387 and verify that they have been
resolved.

An extra pair of eyes would be nice to make sure I'm not getting
confused with the imports.
2025-01-13 14:51:26 +00:00
Jarrod Flesch
690e99f2f9 feat: consolidates logic in update and updateByID operations (#9998)
### What?
Consolidates logic in update and updateByID. These two operations used a
lot of the same core functionality. This is a QOL improvement for future
features/fixes on each operation. You will not need to make changes to
both operations now.

### Why?
Recently we released a feature for `publishSpecificLocale` and that was
only implemented on the updateByID operation. I think future
enhancements and fixes may suffer the same treatment.

### How?
Moves shared logic into a new file with a function called
`updateDocument`.
2025-01-13 09:28:25 -05:00
260 changed files with 3050 additions and 2490 deletions

24
.github/workflows/dispatch-event.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: dispatch-event
on:
workflow_dispatch:
env:
PAYLOAD_PUSH_MAIN_EVENT: payload-push-main-event
jobs:
repository-dispatch:
name: Repository dispatch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dispatch event
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAYLOAD_REPOSITORY_DISPATCH }}
repository: ${{ secrets.REMOTE_REPOSITORY }}
event-type: ${{ env.PAYLOAD_PUSH_MAIN_EVENT }}
client-payload: '{"event": {"head_commit": {"id": "${{ env.GITHUB_SHA }}"}}}' # mocked for testing
# client-payload: '{"event": ${{ toJson(github.event) }}}'

1
.gitignore vendored
View File

@@ -317,3 +317,4 @@ test/databaseAdapter.js
/filename-compound-index
/media-with-relation-preview
/media-without-relation-preview
/media-without-cache-tags

View File

@@ -59,16 +59,17 @@ To accomplish this, Payload exposes the [Access Operation](../authentication/ope
If you use `id` or `data` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your Access Control is being executed via the Access Operation to determine solely what the user can do within the Admin Panel.
## Locale Specific Access Control
To implement locale-specific access control, you can use the `locale` argument in your access control functions. This argument allows you to evaluate the current locale of the request and determine access permissions accordingly.
To implement locale-specific access control, you can use the `req.locale` argument in your access control functions. This argument allows you to evaluate the current locale of the request and determine access permissions accordingly.
Here is an example of how you can implement localized access control:
Here is an example:
```ts
const access = ({ locale }) => {
const access = ({ req }) => {
// Grant access if the locale is 'en'
if (locale === 'en') {
if (req.locale === 'en') {
return true;
}

View File

@@ -397,12 +397,17 @@ export const CustomArrayManager = () => {
onClick={() => {
addFieldRow({
path: "arrayField",
rowIndex: 0,
data: {
textField: "text",
// blockType: "yourBlockSlug",
// ^ if managing a block array, you need to specify the block type
schemaPath: "arrayField",
rowIndex: 0, // optionally specify the index to add the row at
subFieldState: {
textField: {
initialValue: 'New row text',
valid: true,
value: 'New row text',
},
},
// blockType: "yourBlockSlug",
// ^ if managing a block array, you need to specify the block type
})
}}
>
@@ -595,12 +600,17 @@ export const CustomArrayManager = () => {
onClick={() => {
replaceFieldRow({
path: "arrayField",
rowIndex: 0,
data: {
textField: "updated text",
// blockType: "yourBlockSlug",
// ^ if managing a block array, you need to specify the block type
schemaPath: "arrayField",
rowIndex: 0, // optionally specify the index to add the row at
subFieldState: {
textField: {
initialValue: 'Updated text',
valid: true,
value: 'Upddated text',
},
},
// blockType: "yourBlockSlug",
// ^ if managing a block array, you need to specify the block type
})
}}
>

View File

@@ -52,7 +52,7 @@ export default buildConfig({
<Banner type="info">
**Note:**
If when using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to adapter's 'args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
If you're using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to adapter's 'args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
</Banner>
## Options

View File

@@ -305,6 +305,8 @@ The Rich Text Field editor configuration has an `admin` property with the follow
| --- | --- |
| **`placeholder`** | Set this property to define a placeholder string for the field. |
| **`hideGutter`** | Set this property to `true` to hide this field's gutter within the Admin Panel. |
| **`hideInsertParagraphAtEnd`** | Set this property to `true` to hide the "+" button that appears at the end of the editor |
### Disable the gutter
@@ -336,4 +338,4 @@ You can customize the placeholder (the text that appears in the editor when it's
},
}),
}
```
```

View File

@@ -92,6 +92,7 @@ _An asterisk denotes that an option is required._
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.16.0",
"version": "3.17.1",
"private": true,
"type": "module",
"scripts": {
@@ -46,7 +46,7 @@
"build:storage-s3": "turbo build --filter \"@payloadcms/storage-s3\"",
"build:storage-uploadthing": "turbo build --filter \"@payloadcms/storage-uploadthing\"",
"build:storage-vercel-blob": "turbo build --filter \"@payloadcms/storage-vercel-blob\"",
"build:tests": "pnpm --filter payload-test-suite run typecheck",
"build:tests": "pnpm --filter payload-test-suite run build",
"build:translations": "turbo build --filter \"@payloadcms/translations\"",
"build:ui": "turbo build --filter \"@payloadcms/ui\"",
"clean": "turbo clean",
@@ -150,7 +150,7 @@
"jest-environment-jsdom": "29.7.0",
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"mongodb-memory-server": "^10",
"next": "15.1.3",
"open": "^10.1.0",
"p-limit": "^5.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.16.0",
"version": "3.17.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -78,6 +78,8 @@ export async function manageEnvFiles(args: {
}): Promise<void> {
const { cliArgs, databaseType, databaseUri, payloadSecret, projectDir, template } = args
const debugFlag = cliArgs['--debug']
if (cliArgs['--dry-run']) {
debug(`DRY RUN: Environment files managed`)
return
@@ -100,7 +102,10 @@ export async function manageEnvFiles(args: {
updatedExampleContents = updateEnvExampleVariables(envExampleContents, databaseType)
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
debug(`.env.example file successfully updated`)
if (debugFlag) {
debug(`.env.example file successfully updated`)
}
} else {
updatedExampleContents = `# Added by Payload\nDATABASE_URI=your-connection-string-here\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
@@ -116,7 +121,9 @@ export async function manageEnvFiles(args: {
)
await fs.writeFile(envPath, `# Added by Payload\n${envContent.trimEnd()}\n`)
debug(`.env file successfully created or updated`)
if (debugFlag) {
debug(`.env file successfully created or updated`)
}
} catch (err: unknown) {
error('Unable to manage environment files')
if (err instanceof Error) {

View File

@@ -34,7 +34,7 @@ export function helpMessage(): void {
-n {underline my-payload-app} Set project name
-t {underline template_name} Choose specific template
-e {underline example_name} Choose specific exmaple
-e {underline example_name} Choose specific example
{dim Available templates: ${formatTemplates(validTemplates)}}

View File

@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true
/* TODO: remove the following lines */
"noUncheckedIndexedAccess": false,
},
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.16.0",
"version": "3.17.1",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -57,7 +57,7 @@
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.12",
"mongodb": "6.10.0",
"mongodb-memory-server": "^9",
"mongodb-memory-server": "^10",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -1,7 +1,6 @@
import type { PayloadRequest } from 'payload'
import httpStatus from 'http-status'
import { APIError, ValidationError } from 'payload'
import { ValidationError } from 'payload'
export const handleError = ({
collection,
@@ -10,7 +9,7 @@ export const handleError = ({
req,
}: {
collection?: string
error: unknown
error: Error
global?: string
req?: Partial<PayloadRequest>
}) => {
@@ -18,10 +17,9 @@ export const handleError = ({
throw error
}
const message = req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique'
// Handle uniqueness error from MongoDB
if ('code' in error && error.code === 11000 && 'keyValue' in error && error.keyValue) {
const message = req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique'
throw new ValidationError(
{
collection,
@@ -37,5 +35,5 @@ export const handleError = ({
)
}
throw new APIError(message, httpStatus.BAD_REQUEST)
throw error
}

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }, { "path": "../translations" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.16.0",
"version": "3.17.1",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,5 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [
{
"path": "../payload"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.16.0",
"version": "3.17.1",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,5 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [
{
"path": "../payload"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.16.0",
"version": "3.17.1",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,5 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [
{
"path": "../payload"

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.16.0",
"version": "3.17.1",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }, { "path": "../translations" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.16.0",
"version": "3.17.1",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,7 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.16.0",
"version": "3.17.1",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,7 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.16.0",
"version": "3.17.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.16.0",
"version": "3.17.1",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.16.0",
"version": "3.17.1",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }] // db-mongodb depends on payload
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.16.0",
"version": "3.17.1",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.16.0",
"version": "3.17.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -51,7 +51,7 @@ export function getRouteInfo({
globalConfig = config.globals.find((global) => global.slug === globalSlug)
}
// If the collection is using a custom ID, we need to determine it's type
// If the collection is using a custom ID, we need to determine its type
if (collectionConfig && payload) {
if (payload.collections?.[collectionSlug]?.customIDType) {
idType = payload.collections?.[collectionSlug].customIDType

View File

@@ -7,7 +7,6 @@ import * as qs from 'qs-esm'
import type { Args } from './types.js'
import { getRequestLocale } from '../getRequestLocale.js'
import { initReq } from '../initReq.js'
import { getRouteInfo } from './handleAdminPage.js'
import { handleAuthRedirect } from './handleAuthRedirect.js'
@@ -32,7 +31,7 @@ export const initPage = async ({
const cookies = parseCookies(headers)
const { permissions, req } = await initReq(payload.config, {
const { locale, permissions, req } = await initReq(payload.config, {
fallbackLocale: false,
req: {
headers,
@@ -58,12 +57,6 @@ export const initPage = async ({
[],
)
const locale = await getRequestLocale({
req,
})
req.locale = locale?.code
const visibleEntities: VisibleEntities = {
collections: collections
.map(({ slug, admin: { hidden } }) =>

View File

@@ -1,12 +1,22 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
import type { Locale, PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
import { initI18n } from '@payloadcms/translations'
import { headers as getHeaders } from 'next/headers.js'
import { createLocalReq, getPayload, getRequestLanguage, parseCookies } from 'payload'
import {
createLocalReq,
executeAuthStrategies,
getAccessResults,
getPayload,
getRequestLanguage,
parseCookies,
} from 'payload'
import { cache } from 'react'
import { getRequestLocale } from './getRequestLocale.js'
type Result = {
locale?: Locale
permissions: SanitizedPermissions
req: PayloadRequest
}
@@ -33,7 +43,14 @@ export const initReq = cache(async function (
language: languageCode,
})
const { permissions, user } = await payload.auth({ headers })
/**
* Cannot simply call `payload.auth` here, as we need the user to get the locale, and we need the locale to get the access results
* I.e. the `payload.auth` function would call `getAccessResults` without a fully-formed `req` object
*/
const { responseHeaders, user } = await executeAuthStrategies({
headers,
payload,
})
const { req: reqOverrides, ...optionsOverrides } = overrides || {}
@@ -43,6 +60,7 @@ export const initReq = cache(async function (
headers,
host: headers.get('host'),
i18n: i18n as I18n,
responseHeaders,
url: `${payload.config.serverURL}`,
user,
...(reqOverrides || {}),
@@ -52,7 +70,18 @@ export const initReq = cache(async function (
payload,
)
const locale = await getRequestLocale({
req,
})
req.locale = locale?.code
const permissions = await getAccessResults({
req,
})
return {
locale,
permissions,
req,
}

View File

@@ -1,5 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [
{ "path": "../payload" },
{ "path": "../ui" },

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.16.0",
"version": "3.17.1",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -7,7 +7,7 @@ type NodemailerAdapter = ReturnType<typeof nodemailerAdapter>
export const payloadCloudEmail = async (
args: PayloadCloudEmailOptions,
): Promise<NodemailerAdapter> | undefined => {
): Promise<NodemailerAdapter | undefined> => {
if (process.env.PAYLOAD_CLOUD !== 'true' || !args) {
return undefined
}

View File

@@ -17,8 +17,10 @@ export const getAfterDeleteHook = ({
const { identityID, storageClient } = await getStorageClient()
const filesToDelete: string[] = [
doc.filename,
...Object.values(doc?.sizes || []).map((resizedFileData) => resizedFileData?.filename),
doc.filename || '',
...Object.values(doc?.sizes || [])
.map((resizedFileData) => resizedFileData.filename)
.filter((filename): filename is string => filename !== null),
]
const promises = filesToDelete.map(async (filename) => {

View File

@@ -128,7 +128,7 @@ export const payloadCloudPlugin =
return config
}
const oldAutoRunCopy = config.jobs.autoRun
const oldAutoRunCopy = config.jobs.autoRun ?? []
const newAutoRun = async (payload: Payload) => {
const instance = generateRandomString()

View File

@@ -11,7 +11,7 @@ interface Args {
}
// Convert a stream into a promise that resolves with a Buffer
const streamToBuffer = async (readableStream) => {
const streamToBuffer = async (readableStream: any) => {
const chunks = []
for await (const chunk of readableStream) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
@@ -52,7 +52,7 @@ export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHa
Key: key,
})
if (!object.Body) {
if (!object.Body || !object.ContentType || !object.ETag) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}

View File

@@ -13,6 +13,13 @@ export const authAsCognitoUser = async (
return sessionAndToken
}
if (!process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID) {
throw new Error('PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID is required')
}
if (!process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID) {
throw new Error('PAYLOAD_CLOUD_COGNITO_USER_POOL_ID is required')
}
const userPool = new CognitoUserPool({
ClientId: process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID,
UserPoolId: process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID,

View File

@@ -23,6 +23,16 @@ export const getStorageClient: GetStorageClient = async () => {
}
}
if (!process.env.PAYLOAD_CLOUD_PROJECT_ID) {
throw new Error('PAYLOAD_CLOUD_PROJECT_ID is required')
}
if (!process.env.PAYLOAD_CLOUD_COGNITO_PASSWORD) {
throw new Error('PAYLOAD_CLOUD_COGNITO_PASSWORD is required')
}
if (!process.env.PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID) {
throw new Error('PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID is required')
}
session = await authAsCognitoUser(
process.env.PAYLOAD_CLOUD_PROJECT_ID,
process.env.PAYLOAD_CLOUD_COGNITO_PASSWORD,

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.16.0",
"version": "3.17.1",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -18,7 +18,7 @@ import type {
export type ClientFieldWithOptionalType = MarkOptional<ClientField, 'type'>
export type ClientComponentProps = {
customComponents: FormField['customComponents']
customComponents?: FormField['customComponents']
field: ClientBlock | ClientField | ClientTab
forceRender?: boolean
permissions?: SanitizedFieldPermissions

View File

@@ -45,14 +45,13 @@ export type FieldState = {
*/
fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue: unknown
isSidebar?: boolean
initialValue?: unknown
passesCondition?: boolean
requiresRender?: boolean
rows?: Row[]
valid: boolean
valid?: boolean
validate?: Validate
value: unknown
value?: unknown
}
export type FieldStateWithoutComponents = Omit<FieldState, 'customComponents'>

View File

@@ -453,6 +453,12 @@ export type RenderedField = {
Field: React.ReactNode
indexPath?: string
initialSchemaPath?: string
/**
* @deprecated
* This is a legacy property that will be removed in v4.
* Please use `fieldIsSidebar(field)` from `payload` instead.
* Or check `field.admin.position === 'sidebar'` directly.
*/
isSidebar: boolean
path: string
schemaPath: string

View File

@@ -13,7 +13,6 @@ export const accessHandler: PayloadHandler = async (req) => {
try {
const results = await accessOperation({
locale: req.locale,
req,
})

View File

@@ -8,11 +8,10 @@ type OperationArgs = {
disableErrors?: boolean
id?: number | string
isReadingStaticFile?: boolean
locale?: string
req: PayloadRequest
}
const executeAccess = async (
{ id, data, disableErrors, isReadingStaticFile = false, locale, req }: OperationArgs,
{ id, data, disableErrors, isReadingStaticFile = false, req }: OperationArgs,
access: Access,
): Promise<AccessResult> => {
if (access) {
@@ -20,7 +19,6 @@ const executeAccess = async (
id,
data,
isReadingStaticFile,
locale,
req,
})

View File

@@ -5,11 +5,9 @@ import { getEntityPolicies } from '../utilities/getEntityPolicies.js'
import { sanitizePermissions } from '../utilities/sanitizePermissions.js'
type GetAccessResultsArgs = {
locale?: string
req: PayloadRequest
}
export async function getAccessResults({
locale,
req,
}: GetAccessResultsArgs): Promise<SanitizedPermissions> {
const results = {} as Permissions
@@ -48,7 +46,6 @@ export async function getAccessResults({
const collectionPolicy = await getEntityPolicies({
type: 'collection',
entity: collection,
locale,
operations: collectionOperations,
req,
})
@@ -70,7 +67,6 @@ export async function getAccessResults({
const globalPolicy = await getEntityPolicies({
type: 'global',
entity: global,
locale,
operations: globalOperations,
req,
})

View File

@@ -6,17 +6,16 @@ import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/event
import { getAccessResults } from '../getAccessResults.js'
type Arguments = {
locale?: string
req: PayloadRequest
}
export const accessOperation = async (args: Arguments): Promise<SanitizedPermissions> => {
const { locale, req } = args
const { req } = args
adminInitTelemetry(req)
try {
return getAccessResults({ locale, req })
return getAccessResults({ req })
} catch (e: unknown) {
await killTransaction(req)
throw e

View File

@@ -63,7 +63,7 @@ export const unlockOperation = async <TSlug extends CollectionSlug>(
// /////////////////////////////////////
if (!overrideAccess) {
await executeAccess({ locale, req }, collectionConfig.access.unlock)
await executeAccess({ req }, collectionConfig.access.unlock)
}
// /////////////////////////////////////

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { Cron } from 'croner'
import minimist from 'minimist'
import { pathToFileURL } from 'node:url'
@@ -11,7 +12,18 @@ import { generateImportMap } from './generateImportMap/index.js'
import { generateTypes } from './generateTypes.js'
import { info } from './info.js'
import { loadEnv } from './loadEnv.js'
import { migrate } from './migrate.js'
import { migrate, availableCommands as migrateCommands } from './migrate.js'
// Note: this does not account for any user bin scripts
const availableScripts = [
'generate:db-schema',
'generate:importmap',
'generate:types',
'info',
'jobs:run',
'run',
...migrateCommands,
] as const
export const bin = async () => {
loadEnv()
@@ -137,6 +149,8 @@ export const bin = async () => {
process.exit(0)
}
console.error(`Unknown script: "${script}".`)
console.error(script ? `Unknown command: "${script}"` : 'Please provide a command to run')
console.log(`\nAvailable commands:\n${availableScripts.map((c) => ` - ${c}`).join('\n')}`)
process.exit(1)
}

View File

@@ -14,7 +14,7 @@ const prettySyncLogger = {
loggerOptions: {},
}
const availableCommands = [
export const availableCommands = [
'migrate',
'migrate:create',
'migrate:down',

View File

@@ -152,6 +152,7 @@ export const sanitizeCollection = async (
// sanitize fields for reserved names
sanitizeUploadFields(sanitized.fields, sanitized)
sanitized.upload.cacheTags = sanitized.upload?.cacheTags ?? true
sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
sanitized.admin.useAsTitle =

View File

@@ -11,6 +11,7 @@ import { duplicateHandler } from './duplicate.js'
import { findHandler } from './find.js'
import { findByIDHandler } from './findByID.js'
import { findVersionByIDHandler } from './findVersionByID.js'
import { findVersionsHandler } from './findVersions.js'
import { getFileHandler } from './getFile.js'
import { previewHandler } from './preview.js'
import { restoreVersionHandler } from './restoreVersion.js'
@@ -45,6 +46,11 @@ export const defaultCollectionEndpoints: Endpoint[] = [
method: 'post',
path: '/access/:id?',
},
{
handler: findVersionsHandler,
method: 'get',
path: '/versions',
},
{
handler: duplicateHandler,
method: 'post',

View File

@@ -141,7 +141,7 @@ export const createOperation = async <
// /////////////////////////////////////
if (!overrideAccess) {
await executeAccess({ data, locale, req }, collectionConfig.access.create)
await executeAccess({ data, req }, collectionConfig.access.create)
}
// /////////////////////////////////////

View File

@@ -23,6 +23,7 @@ import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions.js'
import { deleteScheduledPublishJobs } from '../../versions/deleteScheduledPublishJobs.js'
import { buildAfterOperation } from './utils.js'
export type Arguments = {
@@ -97,7 +98,7 @@ export const deleteOperation = async <
let accessResult: AccessResult
if (!overrideAccess) {
accessResult = await executeAccess({ locale, req }, collectionConfig.access.delete)
accessResult = await executeAccess({ req }, collectionConfig.access.delete)
}
await validateQueryPaths({
@@ -177,6 +178,18 @@ export const deleteOperation = async <
})
}
// /////////////////////////////////////
// Delete scheduled posts
// /////////////////////////////////////
if (collectionConfig.versions?.drafts && collectionConfig.versions.drafts.schedulePublish) {
await deleteScheduledPublishJobs({
id,
slug: collectionConfig.slug,
payload,
req,
})
}
// /////////////////////////////////////
// Delete document
// /////////////////////////////////////

View File

@@ -19,6 +19,7 @@ import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions.js'
import { deleteScheduledPublishJobs } from '../../versions/deleteScheduledPublishJobs.js'
import { buildAfterOperation } from './utils.js'
export type Arguments = {
@@ -85,7 +86,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
// /////////////////////////////////////
const accessResults = !overrideAccess
? await executeAccess({ id, locale, req }, collectionConfig.access.delete)
? await executeAccess({ id, req }, collectionConfig.access.delete)
: true
const hasWhereAccess = hasWhereAccessResult(accessResults)
@@ -155,6 +156,18 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
})
}
// /////////////////////////////////////
// Delete scheduled posts
// /////////////////////////////////////
if (collectionConfig.versions?.drafts && collectionConfig.versions.drafts.schedulePublish) {
await deleteScheduledPublishJobs({
id,
slug: collectionConfig.slug,
payload,
req,
})
}
// /////////////////////////////////////
// Delete document
// /////////////////////////////////////

View File

@@ -102,10 +102,7 @@ export const findOperation = async <
let accessResult: AccessResult
if (!overrideAccess) {
accessResult = await executeAccess(
{ disableErrors, locale, req },
collectionConfig.access.read,
)
accessResult = await executeAccess({ disableErrors, req }, collectionConfig.access.read)
// If errors are disabled, and access returns false, return empty results
if (accessResult === false) {

View File

@@ -88,7 +88,7 @@ export const findByIDOperation = async <
// /////////////////////////////////////
const accessResult = !overrideAccess
? await executeAccess({ id, disableErrors, locale, req }, collectionConfig.access.read)
? await executeAccess({ id, disableErrors, req }, collectionConfig.access.read)
: true
// If errors are disabled, and access returns false, return null

View File

@@ -50,10 +50,7 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// /////////////////////////////////////
const accessResults = !overrideAccess
? await executeAccess(
{ id, disableErrors, locale, req },
collectionConfig.access.readVersions,
)
? await executeAccess({ id, disableErrors, req }, collectionConfig.access.readVersions)
: true
// If errors are disabled, and access returns false, return null

View File

@@ -53,7 +53,7 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
let accessResults
if (!overrideAccess) {
accessResults = await executeAccess({ locale, req }, collectionConfig.access.readVersions)
accessResults = await executeAccess({ req }, collectionConfig.access.readVersions)
}
const versionFields = buildVersionCollectionFields(payload.config, collectionConfig, true)

View File

@@ -74,7 +74,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// /////////////////////////////////////
const accessResults = !overrideAccess
? await executeAccess({ id: parentDocID, locale, req }, collectionConfig.access.update)
? await executeAccess({ id: parentDocID, req }, collectionConfig.access.update)
: true
const hasWherePolicy = hasWhereAccessResult(accessResults)

View File

@@ -13,26 +13,18 @@ import type {
SelectFromCollectionSlug,
} from '../config/types.js'
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
import executeAccess from '../../auth/executeAccess.js'
import { combineQueries } from '../../database/combineQueries.js'
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
import { APIError } from '../../errors/index.js'
import { afterChange } from '../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles.js'
import { generateFileData } from '../../uploads/generateFileData.js'
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
import { uploadFiles } from '../../uploads/uploadFiles.js'
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey.js'
import { saveVersion } from '../../versions/saveVersion.js'
import { updateDocument } from './utilities/update.js'
import { buildAfterOperation } from './utils.js'
export type Arguments<TSlug extends CollectionSlug> = {
@@ -47,6 +39,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
overrideLock?: boolean
overwriteExistingFiles?: boolean
populate?: PopulateType
publishSpecificLocale?: string
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -91,6 +84,7 @@ export const updateOperation = async <
overrideLock,
overwriteExistingFiles = false,
populate,
publishSpecificLocale,
req: {
fallbackLocale,
locale,
@@ -116,7 +110,7 @@ export const updateOperation = async <
let accessResult: AccessResult
if (!overrideAccess) {
accessResult = await executeAccess({ locale, req }, collectionConfig.access.update)
accessResult = await executeAccess({ req }, collectionConfig.access.update)
}
await validateQueryPaths({
@@ -172,7 +166,7 @@ export const updateOperation = async <
// Generate data for all files and sizes
// /////////////////////////////////////
const { data: newFileData, files: filesToUpload } = await generateFileData({
const { data, files: filesToUpload } = await generateFileData({
collection,
config,
data: bulkUpdateData,
@@ -184,251 +178,37 @@ export const updateOperation = async <
const errors = []
const promises = docs.map(async (doc) => {
const { id } = doc
let data = {
...newFileData,
...bulkUpdateData,
}
const promises = docs.map(async (docWithLocales) => {
const { id } = docWithLocales
try {
// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
await checkDocumentLockStatus({
// ///////////////////////////////////////////////
// Update document, runs all document level hooks
// ///////////////////////////////////////////////
const updatedDoc = await updateDocument({
id,
collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
overrideLock,
req,
})
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc,
draft: draftArg,
fallbackLocale,
global: null,
locale,
overrideAccess: true,
req,
showHiddenFields: true,
})
await deleteAssociatedFiles({
accessResults: accessResult,
autosave: false,
collectionConfig,
config,
doc,
files: filesToUpload,
overrideDelete: false,
req,
})
if (args.collection.config.auth) {
ensureUsernameOrEmail<TSlug>({
authOptions: args.collection.config.auth,
collectionSlug: args.collection.config.slug,
data: args.data,
operation: 'update',
originalDoc,
req: args.req,
})
}
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
data = await beforeValidate<DeepPartial<DataFromCollectionSlug<TSlug>>>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
global: null,
operation: 'update',
overrideAccess,
req,
})
// /////////////////////////////////////
// beforeValidate - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// Write files to local storage
// /////////////////////////////////////
if (!collectionConfig.upload.disableLocalStorage) {
await uploadFiles(payload, filesToUpload, req)
}
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
let result = await beforeChange({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
docWithLocales: doc,
global: null,
operation: 'update',
req,
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate &&
data._status !== 'published',
})
// /////////////////////////////////////
// Update
// /////////////////////////////////////
if (!shouldSaveDraft || data._status === 'published') {
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,
data: result,
locale,
req,
select,
})
}
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (collectionConfig.versions) {
result = await saveVersion({
id,
collection: collectionConfig,
docWithLocales: result,
payload,
req,
select,
})
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result,
draft: draftArg,
fallbackLocale: null,
global: null,
docWithLocales,
draftArg,
fallbackLocale,
filesToUpload,
locale,
overrideAccess,
overrideLock,
payload,
populate,
publishSpecificLocale,
req,
select,
showHiddenFields,
})
// /////////////////////////////////////
// afterRead - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange({
collection: collectionConfig,
context: req.context,
data,
doc: result,
global: null,
operation: 'update',
previousDoc: originalDoc,
req,
})
// /////////////////////////////////////
// afterChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: originalDoc,
req,
})) || result
}, Promise.resolve())
await unlinkTempFiles({
collectionConfig,
config,
req,
})
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
return result
return updatedDoc
} catch (error) {
errors.push({
id,
@@ -438,6 +218,12 @@ export const updateOperation = async <
return null
})
await unlinkTempFiles({
collectionConfig,
config,
req,
})
const awaitedDocs = await Promise.all(promises)
let result = {

View File

@@ -3,7 +3,6 @@ import type { DeepPartial } from 'ts-essentials'
import httpStatus from 'http-status'
import type { FindOneArgs } from '../../database/types.js'
import type { Args } from '../../fields/hooks/beforeChange/index.js'
import type { CollectionSlug } from '../../index.js'
import type {
PayloadRequest,
@@ -13,31 +12,21 @@ import type {
} from '../../types/index.js'
import type {
Collection,
DataFromCollectionSlug,
RequiredDataFromCollectionSlug,
SelectFromCollectionSlug,
} from '../config/types.js'
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
import executeAccess from '../../auth/executeAccess.js'
import { generatePasswordSaltHash } from '../../auth/strategies/local/generatePasswordSaltHash.js'
import { hasWhereAccessResult } from '../../auth/types.js'
import { combineQueries } from '../../database/combineQueries.js'
import { APIError, Forbidden, NotFound } from '../../errors/index.js'
import { afterChange } from '../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
import { deleteAssociatedFiles } from '../../uploads/deleteAssociatedFiles.js'
import { generateFileData } from '../../uploads/generateFileData.js'
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
import { uploadFiles } from '../../uploads/uploadFiles.js'
import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion.js'
import { saveVersion } from '../../versions/saveVersion.js'
import { updateDocument } from './utilities/update.js'
import { buildAfterOperation } from './utils.js'
export type Arguments<TSlug extends CollectionSlug> = {
@@ -118,17 +107,14 @@ export const updateByIDOperation = async <
throw new APIError('Missing ID of document to update.', httpStatus.BAD_REQUEST)
}
let { data } = args
const password = data?.password
const shouldSaveDraft = Boolean(draftArg && collectionConfig.versions.drafts)
const shouldSavePassword = Boolean(password && collectionConfig.auth && !shouldSaveDraft)
const { data } = args
// /////////////////////////////////////
// Access
// /////////////////////////////////////
const accessResults = !overrideAccess
? await executeAccess({ id, data, locale, req }, collectionConfig.access.update)
? await executeAccess({ id, data, req }, collectionConfig.access.update)
: true
const hasWherePolicy = hasWhereAccessResult(accessResults)
@@ -158,43 +144,6 @@ export const updateByIDOperation = async <
throw new Forbidden(req.t)
}
// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
await checkDocumentLockStatus({
id,
collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
overrideLock,
req,
})
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: docWithLocales,
draft: draftArg,
fallbackLocale: null,
global: null,
locale,
overrideAccess: true,
req,
showHiddenFields: true,
})
if (args.collection.config.auth) {
ensureUsernameOrEmail<TSlug>({
authOptions: args.collection.config.auth,
collectionSlug: args.collection.config.slug,
data: args.data,
operation: 'update',
originalDoc,
req: args.req,
})
}
// /////////////////////////////////////
// Generate data for all files and sizes
// /////////////////////////////////////
@@ -209,266 +158,50 @@ export const updateByIDOperation = async <
throwOnMissingFile: false,
})
data = newFileData
// ///////////////////////////////////////////////
// Update document, runs all document level hooks
// ///////////////////////////////////////////////
// /////////////////////////////////////
// Delete any associated files
// /////////////////////////////////////
await deleteAssociatedFiles({
let result = await updateDocument<TSlug, TSelect>({
id,
accessResults,
autosave,
collectionConfig,
config,
doc: docWithLocales,
files: filesToUpload,
overrideDelete: false,
req,
})
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
data = await beforeValidate<DeepPartial<DataFromCollectionSlug<TSlug>>>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
global: null,
operation: 'update',
overrideAccess,
req,
})
// /////////////////////////////////////
// beforeValidate - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// Write files to local storage
// /////////////////////////////////////
if (!collectionConfig.upload.disableLocalStorage) {
await uploadFiles(payload, filesToUpload, req)
}
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
let publishedDocWithLocales = docWithLocales
let versionSnapshotResult
const beforeChangeArgs: Args<DataFromCollectionSlug<TSlug>> = {
id,
collection: collectionConfig,
context: req.context,
data: { ...data, id },
doc: originalDoc,
docWithLocales: undefined,
global: null,
operation: 'update',
req,
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate &&
data._status !== 'published',
}
if (publishSpecificLocale) {
versionSnapshotResult = await beforeChange({
...beforeChangeArgs,
docWithLocales,
})
const lastPublished = await getLatestCollectionVersion({
id,
config: collectionConfig,
payload,
published: true,
query: findOneArgs,
req,
})
publishedDocWithLocales = lastPublished ? lastPublished : {}
}
let result = await beforeChange({
...beforeChangeArgs,
docWithLocales: publishedDocWithLocales,
})
// /////////////////////////////////////
// Handle potential password update
// /////////////////////////////////////
const dataToUpdate: Record<string, unknown> = { ...result }
if (shouldSavePassword && typeof password === 'string') {
const { hash, salt } = await generatePasswordSaltHash({
collection: collectionConfig,
password,
req,
})
dataToUpdate.salt = salt
dataToUpdate.hash = hash
delete dataToUpdate.password
delete data.password
}
// /////////////////////////////////////
// Update
// /////////////////////////////////////
if (!shouldSaveDraft || data._status === 'published') {
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,
data: dataToUpdate,
locale,
req,
select,
})
}
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (collectionConfig.versions) {
result = await saveVersion({
id,
autosave,
collection: collectionConfig,
docWithLocales: result,
draft: shouldSaveDraft,
payload,
publishSpecificLocale,
req,
select,
snapshot: versionSnapshotResult,
})
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
data: newFileData,
depth,
doc: result,
draft: draftArg,
docWithLocales,
draftArg,
fallbackLocale,
global: null,
filesToUpload,
locale,
overrideAccess,
overrideLock,
payload,
populate,
publishSpecificLocale,
req,
select,
showHiddenFields,
})
// /////////////////////////////////////
// afterRead - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange({
collection: collectionConfig,
context: req.context,
data,
doc: result,
global: null,
operation: 'update',
previousDoc: originalDoc,
req,
})
// /////////////////////////////////////
// afterChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: originalDoc,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation({
args,
collection: collectionConfig,
operation: 'updateByID',
result,
})
await unlinkTempFiles({
collectionConfig,
config,
req,
})
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = (await buildAfterOperation({
args,
collection: collectionConfig,
operation: 'updateByID',
result,
})) as TransformCollectionWithSelect<TSlug, TSelect>
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
@@ -477,7 +210,7 @@ export const updateByIDOperation = async <
await commitTransaction(req)
}
return result as TransformCollectionWithSelect<TSlug, TSelect>
return result
} catch (error: unknown) {
await killTransaction(args.req)
throw error

View File

@@ -0,0 +1,380 @@
import type { DeepPartial } from 'ts-essentials'
import type { Args } from '../../../fields/hooks/beforeChange/index.js'
import type { AccessResult, CollectionSlug, FileToSave, SanitizedConfig } from '../../../index.js'
import type {
Payload,
PayloadRequest,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../../types/index.js'
import type {
DataFromCollectionSlug,
SanitizedCollectionConfig,
SelectFromCollectionSlug,
} from '../../config/types.js'
import { ensureUsernameOrEmail } from '../../../auth/ensureUsernameOrEmail.js'
import { generatePasswordSaltHash } from '../../../auth/strategies/local/generatePasswordSaltHash.js'
import { combineQueries } from '../../../database/combineQueries.js'
import { afterChange } from '../../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../../fields/hooks/afterRead/index.js'
import { beforeChange } from '../../../fields/hooks/beforeChange/index.js'
import { beforeValidate } from '../../../fields/hooks/beforeValidate/index.js'
import { deleteAssociatedFiles } from '../../../uploads/deleteAssociatedFiles.js'
import { uploadFiles } from '../../../uploads/uploadFiles.js'
import { checkDocumentLockStatus } from '../../../utilities/checkDocumentLockStatus.js'
import { getLatestCollectionVersion } from '../../../versions/getLatestCollectionVersion.js'
import { saveVersion } from '../../../versions/saveVersion.js'
export type SharedUpdateDocumentArgs<TSlug extends CollectionSlug> = {
accessResults: AccessResult
autosave: boolean
collectionConfig: SanitizedCollectionConfig
config: SanitizedConfig
data: DeepPartial<DataFromCollectionSlug<TSlug>>
depth: number
docWithLocales: any
draftArg: boolean
fallbackLocale: string
filesToUpload: FileToSave[]
id: number | string
locale: string
overrideAccess: boolean
overrideLock: boolean
payload: Payload
populate?: PopulateType
publishSpecificLocale?: string
req: PayloadRequest
select: SelectType
showHiddenFields: boolean
}
/**
* This function is used to update a document in the DB and return the result.
*
* It runs the following hooks in order:
* - beforeValidate - Fields
* - beforeValidate - Collection
* - beforeChange - Collection
* - beforeChange - Fields
* - afterRead - Fields
* - afterRead - Collection
* - afterChange - Fields
* - afterChange - Collection
*/
export const updateDocument = async <
TSlug extends CollectionSlug,
TSelect extends SelectFromCollectionSlug<TSlug> = SelectType,
>({
id,
accessResults,
autosave,
collectionConfig,
config,
data,
depth,
docWithLocales,
draftArg,
fallbackLocale,
filesToUpload,
locale,
overrideAccess,
overrideLock,
payload,
populate,
publishSpecificLocale,
req,
select,
showHiddenFields,
}: SharedUpdateDocumentArgs<TSlug>): Promise<TransformCollectionWithSelect<TSlug, TSelect>> => {
const password = data?.password
const shouldSaveDraft =
Boolean(draftArg && collectionConfig.versions.drafts) && data._status !== 'published'
const shouldSavePassword = Boolean(password && collectionConfig.auth && !shouldSaveDraft)
// /////////////////////////////////////
// Handle potentially locked documents
// /////////////////////////////////////
await checkDocumentLockStatus({
id,
collectionSlug: collectionConfig.slug,
lockErrorMessage: `Document with ID ${id} is currently locked by another user and cannot be updated.`,
overrideLock,
req,
})
const originalDoc = await afterRead({
collection: collectionConfig,
context: req.context,
depth: 0,
doc: docWithLocales,
draft: draftArg,
fallbackLocale: id ? null : fallbackLocale,
global: null,
locale,
overrideAccess: true,
req,
showHiddenFields: true,
})
if (collectionConfig.auth) {
ensureUsernameOrEmail<TSlug>({
authOptions: collectionConfig.auth,
collectionSlug: collectionConfig.slug,
data,
operation: 'update',
originalDoc,
req,
})
}
// /////////////////////////////////////
// Delete any associated files
// /////////////////////////////////////
await deleteAssociatedFiles({
collectionConfig,
config,
doc: docWithLocales,
files: filesToUpload,
overrideDelete: false,
req,
})
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////
data = await beforeValidate<DeepPartial<DataFromCollectionSlug<TSlug>>>({
id,
collection: collectionConfig,
context: req.context,
data,
doc: originalDoc,
global: null,
operation: 'update',
overrideAccess,
req,
})
// /////////////////////////////////////
// beforeValidate - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// Write files to local storage
// /////////////////////////////////////
if (!collectionConfig.upload.disableLocalStorage) {
await uploadFiles(payload, filesToUpload, req)
}
// /////////////////////////////////////
// beforeChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook
data =
(await hook({
collection: collectionConfig,
context: req.context,
data,
operation: 'update',
originalDoc,
req,
})) || data
}, Promise.resolve())
// /////////////////////////////////////
// beforeChange - Fields
// /////////////////////////////////////
let publishedDocWithLocales = docWithLocales
let versionSnapshotResult
const beforeChangeArgs: Args<DataFromCollectionSlug<TSlug>> = {
id,
collection: collectionConfig,
context: req.context,
data: { ...data, id },
doc: originalDoc,
docWithLocales: undefined,
global: null,
operation: 'update',
req,
skipValidation:
shouldSaveDraft &&
collectionConfig.versions.drafts &&
!collectionConfig.versions.drafts.validate,
}
if (publishSpecificLocale) {
versionSnapshotResult = await beforeChange({
...beforeChangeArgs,
docWithLocales,
})
const lastPublished = await getLatestCollectionVersion({
id,
config: collectionConfig,
payload,
published: true,
query: {
collection: collectionConfig.slug,
locale,
req,
where: combineQueries({ id: { equals: id } }, accessResults),
},
req,
})
publishedDocWithLocales = lastPublished ? lastPublished : {}
}
let result = await beforeChange({
...beforeChangeArgs,
docWithLocales: publishedDocWithLocales,
})
// /////////////////////////////////////
// Handle potential password update
// /////////////////////////////////////
const dataToUpdate: Record<string, unknown> = { ...result }
if (shouldSavePassword && typeof password === 'string') {
const { hash, salt } = await generatePasswordSaltHash({
collection: collectionConfig,
password,
req,
})
dataToUpdate.salt = salt
dataToUpdate.hash = hash
delete dataToUpdate.password
delete data.password
}
// /////////////////////////////////////
// Update
// /////////////////////////////////////
if (!shouldSaveDraft) {
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,
data: dataToUpdate,
locale,
req,
select,
})
}
// /////////////////////////////////////
// Create version
// /////////////////////////////////////
if (collectionConfig.versions) {
result = await saveVersion({
id,
autosave,
collection: collectionConfig,
docWithLocales: result,
draft: shouldSaveDraft,
payload,
publishSpecificLocale,
req,
select,
snapshot: versionSnapshotResult,
})
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////
result = await afterRead({
collection: collectionConfig,
context: req.context,
depth,
doc: result,
draft: draftArg,
fallbackLocale,
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,
})
// /////////////////////////////////////
// afterRead - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
req,
})) || result
}, Promise.resolve())
// /////////////////////////////////////
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange({
collection: collectionConfig,
context: req.context,
data,
doc: result,
global: null,
operation: 'update',
previousDoc: originalDoc,
req,
})
// /////////////////////////////////////
// afterChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook
result =
(await hook({
collection: collectionConfig,
context: req.context,
doc: result,
operation: 'update',
previousDoc: originalDoc,
req,
})) || result
}, Promise.resolve())
return result as TransformCollectionWithSelect<TSlug, TSelect>
}

View File

@@ -326,8 +326,6 @@ export type AccessArgs<TData = any> = {
id?: number | string
/** If true, the request is for a static file */
isReadingStaticFile?: boolean
/** The locale of the request */
locale?: string
/** The original request that requires an access check */
req: PayloadRequest
}
@@ -793,7 +791,7 @@ export type Config = {
dependencies?: AdminDependencies
/**
* @deprecated
* This option is deprecated and will be removed in the next major version.
* This option is deprecated and will be removed in v4.
* To disable the admin panel itself, delete your `/app/(payload)/admin` directory.
* To disable all REST API and GraphQL endpoints, delete your `/app/(payload)/api` directory.
* Note: If you've modified the default paths via `admin.routes`, delete those directories instead.
@@ -805,7 +803,6 @@ export type Config = {
* @default true
*/
autoGenerate?: boolean
/** The base directory for component paths starting with /.
*
* By default, this is process.cwd()

View File

@@ -216,8 +216,6 @@ export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (a
* The `id` of the current document being read or updated. `id` is undefined during the `create` operation.
*/
id?: number | string
/** The locale of the request */
locale?: string
/** The `payload` object to interface with the payload API */
req: PayloadRequest
/**

View File

@@ -46,7 +46,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
let accessResult: AccessResult
if (!overrideAccess) {
accessResult = await executeAccess({ locale, req }, globalConfig.access.read)
accessResult = await executeAccess({ req }, globalConfig.access.read)
}
// /////////////////////////////////////

View File

@@ -46,7 +46,7 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// /////////////////////////////////////
const accessResults = !overrideAccess
? await executeAccess({ id, disableErrors, locale, req }, globalConfig.access.readVersions)
? await executeAccess({ id, disableErrors, req }, globalConfig.access.readVersions)
: true
// If errors are disabled, and access returns false, return null

View File

@@ -53,7 +53,7 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
// /////////////////////////////////////
const accessResults = !overrideAccess
? await executeAccess({ locale, req }, globalConfig.access.readVersions)
? await executeAccess({ req }, globalConfig.access.readVersions)
: true
await validateQueryPaths({

View File

@@ -44,7 +44,7 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
// /////////////////////////////////////
if (!overrideAccess) {
await executeAccess({ locale, req }, globalConfig.access.update)
await executeAccess({ req }, globalConfig.access.update)
}
// /////////////////////////////////////

View File

@@ -88,7 +88,6 @@ export const updateOperation = async <
? await executeAccess(
{
data,
locale,
req,
},
globalConfig.access.update,

View File

@@ -1112,6 +1112,7 @@ export type {
UpdateVersion,
UpdateVersionArgs,
Upsert,
UpsertArgs,
} from './database/types.js'
export type { EmailAdapter as PayloadEmailAdapter, SendEmailOptions } from './email/types.js'
export {

View File

@@ -100,6 +100,12 @@ export type UploadConfig = {
* @default true
*/
bulkUpload?: boolean
/**
* Appends a cache tag to the image URL when fetching the thumbnail in the admin panel. It may be desirable to disable this when hosting via CDNs with strict parameters.
*
* @default true
*/
cacheTags?: boolean
/**
* Enables cropping of images.
* @default true

View File

@@ -24,7 +24,6 @@ type CreateAccessPromise = (args: {
access: Access | FieldAccess
accessLevel: 'entity' | 'field'
disableWhere?: boolean
locale?: string
operation: AllOperations
policiesObj: {
[key: string]: any
@@ -105,7 +104,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
await docBeingAccessed
// https://payloadcms.slack.com/archives/C048Z9C2BEX/p1702054928343769
const accessResult = await access({ id, data, doc: docBeingAccessed, locale, req })
const accessResult = await access({ id, data, doc: docBeingAccessed, req })
if (typeof accessResult === 'object' && !disableWhere) {
mutablePolicies[operation] = {

View File

@@ -8,6 +8,7 @@ import type { PayloadRequest } from '../types/index.js'
import { createPayloadRequest } from './createPayloadRequest.js'
import { headersWithCors } from './headersWithCors.js'
import { mergeHeaders } from './mergeHeaders.js'
import { routeError } from './routeError.js'
const notFoundResponse = (req: PayloadRequest) => {
@@ -82,6 +83,7 @@ export const handleEndpoints = async ({
const url = `${request.url}?${new URLSearchParams(search).toString()}`
const response = await handleEndpoints({
basePath,
config: incomingConfig,
request: new Request(url, {
cache: request.cache,
@@ -215,14 +217,11 @@ export const handleEndpoints = async ({
}
const response = await handler(req)
if (req.responseHeaders) {
for (const [key, value] of req.responseHeaders) {
response.headers.append(key, value)
}
}
return response
return new Response(response.body, {
headers: mergeHeaders(req.responseHeaders ?? new Headers(), response.headers),
status: response.status,
statusText: response.statusText,
})
} catch (err) {
return routeError({
collection,

View File

@@ -21,8 +21,9 @@ export const deleteCollectionVersions = async ({ id, slug, payload, req }: Args)
},
})
} catch (err) {
payload.logger.error(
`There was an error removing versions for the deleted ${slug} document with ID ${id}.`,
)
payload.logger.error({
err,
msg: `There was an error removing versions for the deleted ${slug} document with ID ${id}.`,
})
}
}

View File

@@ -0,0 +1,60 @@
import type { PayloadRequest } from '../types/index.js'
import { type Payload } from '../index.js'
type Args = {
id?: number | string
payload: Payload
req?: PayloadRequest
slug: string
}
export const deleteScheduledPublishJobs = async ({
id,
slug,
payload,
req,
}: Args): Promise<void> => {
try {
await payload.db.deleteMany({
collection: 'payload-jobs',
req,
where: {
and: [
// only want to delete jobs have not run yet
{
completedAt: {
exists: false,
},
},
{
processing: {
equals: false,
},
},
{
'input.doc.value': {
equals: id,
},
},
{
'input.doc.relationTo': {
equals: slug,
},
},
// data.type narrows scheduled publish jobs in case of another job having input.doc.value
{
taskSlug: {
equals: 'schedulePublish',
},
},
],
},
})
} catch (err) {
payload.logger.error({
err,
msg: `There was an error deleting scheduled publish jobs from the queue for ${slug} document with ID ${id}.`,
})
}
}

View File

@@ -42,6 +42,11 @@ export const saveVersion = async ({
if (draft) {
versionData._status = 'draft'
}
if (collection?.timestamps && draft) {
versionData.updatedAt = now
}
if (versionData._id) {
delete versionData._id
}

View File

@@ -3,10 +3,10 @@ import type { CollectionSlug, GlobalSlug } from '../../index.js'
export type SchedulePublishTaskInput = {
doc?: {
relationTo: CollectionSlug
value: number | string
value: string
}
global?: GlobalSlug
locale?: string
type: string
type?: string
user?: number | string
}

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../translations" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.16.0",
"version": "3.17.1",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.16.0",
"version": "3.17.1",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,4 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
},
"references": [{ "path": "../payload" }, { "path": "../ui" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.16.0",
"version": "3.17.1",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,4 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.16.0",
"version": "3.17.1",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,4 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.16.0",
"version": "3.17.1",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,4 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
},
"references": [{ "path": "../payload" }, { "path": "../ui" }, { "path": "../next" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
"version": "3.16.0",
"version": "3.17.1",
"description": "Sentry plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.16.0",
"version": "3.17.1",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,4 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
},
"references": [{ "path": "../payload" }, { "path": "../ui" }, { "path": "../next" }]
}

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