Compare commits

...

38 Commits

Author SHA1 Message Date
Elliot DeNolf
f878e35cc7 chore(release): v3.0.0-beta.126 [skip ci] 2024-11-06 16:23:57 -05:00
Dan Ribbens
f0f96e7558 fix: allow workflows to be empty or undefined (#9039)
### What?

- Makes `jobs.workflows` optional
- Dynamically include the `workflowSlugs` select field in the jobs
collection as needed

### Why?

When configuring jobs, it should be possible to define `job` with just
some simple tasks and not be forced to define workflows.

### How?

Workflows type was made optional and optional chaining is added where
needed. The workflowSlugs field is added to the jobs collection if
workflows are defined.

Fixes #

When using postgres, the workflowSlugs being an empty enum cause an
error when drizzle fails to detect the enum already exists. This results
in the error `"enum_payload_jobs_workflow_slug" already exists`. Drizzle
tries to make the enum as: `enum_payload_jobs_workflow_slug as enum();`
and the check for existing enums only works when it has values.
2024-11-06 15:50:17 -05:00
Javier
0165ab8930 fix: replace console.error with logger.errors (#9044)
## Problem
When `PayloadRequest` objects are logged using `console.log`, it creates
unstructured, multiline entries in logging services like DataDog and
Sentry. This circumvents the structured logging approach used throughout
the rest of the codebase.

## Solution
Replace `console.x` calls with the structured logging system when
logging `payload.logger.x` objects. This ensures consistent log
formatting and better integration with monitoring tools.

## Changes
- Replaced instances of `console.log` with structured logging methods
only in `@payloadcms/next`
- Maintains logging consistency across the codebase
- Improves log readability in DataDog, Sentry, and other monitoring
services

## First

<img width="914" alt="Screenshot 2024-11-06 at 09 53 44"
src="https://github.com/user-attachments/assets/019b6f4b-40ed-4e54-a92a-8d1b50baa303">

## Then

<img width="933" alt="Screenshot 2024-11-06 at 00 50 29"
src="https://github.com/user-attachments/assets/0a339db4-d706-4ff9-ba8c-80445bbef5d0">
2024-11-06 15:49:27 -05:00
Sasha
213b7c6fb6 feat: generate types for joins (#9054)
### What?
Generates types for `joins` property.
Example from our `joins` test, keys are type-safe:
<img width="708" alt="image"
src="https://github.com/user-attachments/assets/f1fbbb9d-7c39-49a2-8aa2-a4793ae4ad7e">

Output in `payload-types.ts`:
```ts
 collectionsJoins: {
    categories: {
      relatedPosts: 'posts';
      hasManyPosts: 'posts';
      hasManyPostsLocalized: 'posts';
      'group.relatedPosts': 'posts';
      'group.camelCasePosts': 'posts';
      filtered: 'posts';
      singulars: 'singular';
    };
  };
```
Additionally, we include type information about on which collection the
join is, it will help when we have types generation for `where` and
`sort`.

### Why?
It provides a better DX as you don't need to memoize your keys.

### How?
Modifies `configToJSONSchema` to generate the json schema for
`collectionsJoins`, uses that type within `JoinQuery`
2024-11-06 22:43:07 +02:00
Jessica Chowdhury
7dc52567f1 fix: publish locale with autosave enabled and close dropdown (#8719)
1. Fix publish specific locale option when no published versions exist
2. Close the publish locale dropdown on click
2024-11-06 14:36:28 -05:00
Sasha
a22c0e62fa feat: add populate property to Local / REST API (#8969)
### What?
Adds `populate` property to Local API and REST API operations that can
be used to specify `select` for a specific collection when it's
populated
```ts
const result = await payload.findByID({
  populate: {
   // type safe if you have generated types
    posts: {
      text: true,
    },
  },
  collection: 'pages',
  depth: 1,
  id: aboutPage.id,
})

result.relatedPost // only has text and id properties
``` 

```ts
fetch('https://localhost:3000/api/pages?populate[posts][text]=true') // highlight-line
  .then((res) => res.json())
  .then((data) => console.log(data))
```

It also overrides
[`defaultPopulate`](https://github.com/payloadcms/payload/pull/8934)

Ensures `defaultPopulate` doesn't affect GraphQL.

### How?
Implements the property for all operations that have the `depth`
argument.
2024-11-06 13:50:19 -05:00
Sasha
147d28e62c fix(db-postgres): handle special characters in createDatabase (#9022)
### What?
Handles database name with special characters. For example: `-` -
`my-awesome-app`.

### Why?
Previously, `my-awesome-app` led to this error:
```
Error: failed to create database my-awesome-app.
Details: syntax error at or near "-"
```
This can reproduced for example with `create-payload-app`, as the
generated db name is based on project's name.

### How?
Wraps the query variable to quotes, `create database "my-awesome-app"`
instead of `create database my-awesome-app`.
2024-11-06 13:29:57 -05:00
Sasha
f507305192 fix: handle bulk upload sequentially to prevent conflicts (#9052)
### What?
Uses sequential pattern for Bulk Upload instead of `Promise.all`.

### Why?
* Concurrent uploads led to filename conflicts for example when you have
`upload.png` and `upload(1).png` already and you try to upload
`upload.png`
* Potentially expensive for resources, especially with high amount of
files / sizes

### How?
Replaces `Promise.all` with `for` loop, adds indicator "Uploaded 2/20"
to the loading overlay.

---------

Co-authored-by: James <james@trbl.design>
2024-11-06 12:54:16 -05:00
Timothy Choi
d42529055a fix(richtext-slate, ui): use PointerEvents to show tooltips on enabled / disabled buttons (#9006)
Fixes #9005

Note: I did not replace all instances of `onMouseEnter`, just the ones
that can be disabled and have `tooltip` set.
2024-11-06 12:43:06 -05:00
Jacob Fletcher
4b4ecb386d fix(ui): dedupes custom id fields (#9050)
Setting a custom `id` field within unnamed fields causes duplicative ID
fields to be appear in the client config. When a top-level `id` field is
detected in your config, Payload uses that instead of injecting its
default field. But when nested within unnamed fields, such as an unnamed
tab, these custom `id` fields were not being found, causing the default
field to be duplicately rendered into tables columns, etc.
2024-11-06 12:19:19 -05:00
Andreas Bernhard
becf56d582 fix(ui): edit many modal draft action button order and style (#9047)
PR adjusts "draft" action button order and style on the edit-many-modal
to be consistent with default collection edit view action buttons.

This is the `v3` PR version of
https://github.com/payloadcms/payload/pull/9046 and fixes
https://github.com/payloadcms/payload/issues/9045
2024-11-06 11:19:44 -05:00
Elliot DeNolf
8a5f6f044d chore(release): v3.0.0-beta.125 [skip ci] 2024-11-06 10:24:31 -05:00
Dan Ribbens
93a55d1075 feat: add join field config where property (#8973)
### What?

Makes it possible to filter join documents using a `where` added
directly in the config.


### Why?

It makes the join field more powerful for adding contextual meaning to
the documents being returned. For example, maybe you have a
`requiresAction` field that you set and you can have a join that
automatically filters the documents to those that need attention.

### How?

In the database adapter, we merge the requested `where` to the `where`
defined on the field.
On the frontend the results are filtered using the `filterOptions`
property in the component.

Fixes
https://github.com/payloadcms/payload/discussions/8936
https://github.com/payloadcms/payload/discussions/8937

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-11-06 10:06:25 -05:00
Elliot DeNolf
cdcefa88f2 fix(cpa): remove lock file on project creation 2024-11-06 10:02:29 -05:00
Elliot DeNolf
5c049f7c9c ci(triage): adjust tag condition 2024-11-05 23:03:27 -05:00
Elliot DeNolf
ae6fb4dd1b ci(triage): add granularity in actions to be performed, enable comments 2024-11-05 22:58:33 -05:00
Sasha
9ce2ba6a3f fix: custom endpoints with method: 'put' (#9037)
### What?
Fixes support for custom endpoints with `method: 'put'`.
Previously, this didn't work:
```ts
export default buildConfigWithDefaults({
  collections: [ ],
  endpoints: [
    {
      method: 'put',
      handler: () => new Response(),
      path: '/put',
    },
  ],
})
```

### Why?
We supported this in 2.0 and docs are saying that we can use `'put'` as
`method`
https://payloadcms.com/docs/beta/rest-api/overview#custom-endpoints

### How?
Implements the `REST_PUT` export for `@payloadcms/next/routes`, updates
all templates. Additionally, adds tests to ensure root/collection level
custom endpoints with all necessary methods execute properly.

Fixes https://github.com/payloadcms/payload/issues/8807

-->
2024-11-05 23:14:34 +02:00
Sasha
f52b7c45c0 fix: type augmentation of RequestContext (#9035)
### What?

Makes this to actually work
```ts
import type { RequestContext as OriginalRequestContext } from 'payload'

declare module 'payload' {
  // Create a new interface that merges your additional fields with the original one
  export interface RequestContext extends OriginalRequestContext {
    myObject?: string
    // ...
  }
}
```
<img width="502" alt="image"
src="https://github.com/user-attachments/assets/38570d3c-e8a8-48aa-a57d-6d11e79394f5">


### Why?
This is described in our docs
https://payloadcms.com/docs/beta/hooks/context#typescript therefore it
should work.

### How?
In order to get the declaration work, we need to reuse the type from the
root file `payload/src/index.js`. Additionally, removes `RequestContext`
type duplication in both `payload/src/types/index.js` and
`payload/src/index.js`.

Fixes https://github.com/payloadcms/payload/issues/8851
2024-11-05 23:14:04 +02:00
Elliot DeNolf
2eeed4a8ae chore(templates): add lock file to with-payload-cloud 2024-11-05 15:55:57 -05:00
Elliot DeNolf
c0335aa49e chore(templates): add lock files 2024-11-05 15:27:27 -05:00
Paul
3ca203e08c fix(ui): json fields can now take a maxHeight in admin props and there's a mininum height of 3 lines (#9018)
JSON fields are now 3 lines minimum in height like so:

![image](https://github.com/user-attachments/assets/0b2ad47e-6929-4836-ac9d-022ffcdc6f27)


This helps fix an issue where long content is wrapped:

![image](https://github.com/user-attachments/assets/40fc2426-11d7-4ca5-a716-3347bb0d5a4b)

Previously it would show like this:

![image](https://github.com/user-attachments/assets/7f321220-ffa2-40ff-bc4b-2b26d21d4911)
2024-11-05 13:43:51 -06:00
James Mikrut
50f3ca93ee docs: improves jobs queue (#9038)
improves docs for jobs queue
2024-11-05 19:25:14 +00:00
Friggo
4652e8d56e feat(plugin-seo): add czech translation (#8998)
Adds Czech translation to SEO plugin.
2024-11-05 18:28:12 +00:00
Patrik
2175e5cdfb fix(ui): ensure upload field updates reflect in edit popup changes (#9034)
### What?

Any changes inside edit popup for the field with type `upload` and the
`relationTo` collection does nothing in context of the field, it has
affect only to collection.

I.e. when you make an edit to an uploads field in the edit drawer -
after saving and existing the drawer, your new changes are not present
until a refresh of the page.

### Why?

Previously, we were not performing a reload of the document fetch upon
saving of the doc in the edit drawer.

### How?

Now, we perform a reload (fetch) for updated docs on save within the
edit drawer.

Fixes #8837
2024-11-05 13:03:12 -05:00
Paul
201d68663e feat(templates): website template now has configured image sizes, updated readme and simplified env vars for setting up (#9036) 2024-11-05 17:59:29 +00:00
Patrik
ebd3c025b7 fix(ui): updatedAt field in locked-docs collection able to be updated by non-owner (#9026)
### What?

If you have a custom field that sets the value of the field using the
`useField` hook on entry into a document - the `updatedAt` field would
be updated even when a non-owner tries to enter a locked document.

### Why?

When a field is updated in the edit view - we perform an update in
`form-state` to keep the doc in `payload-locked-documents` up to date
with the current editing status. The above scenario would hit this
update operation even on non-owner users because it was previously only
checking for `updateLastEdited` (which would get hit by the `setValue`
in the `useField` hook) so we also need to check to make sure the
current user entering a locked doc is also the owner of the document.

### How?

When performing an update to `payload-locked-documents` in
`buildFormState` - only perform the update if the current user is also
the owner of the locked document otherwise skip the `update` operation.

Fixes #8781
2024-11-05 12:39:48 -05:00
Paul
ddc9d9731a feat: adds x-powered-by Payload header in next config (#9027)
Adds the `x-powered-by` header to include Payload alongside Next.js

End result looks like this
```
x-powered-by:
Next.js, Payload
```

It also respects the nextConfig `poweredBy: false` to completely disable
it
2024-11-04 18:11:51 -06:00
Jesper We
3e31b7aec9 feat(plugin-seo): add Swedish translations (#9007)
### What?

Swedish text translations

### Why?

There was no Swedish before
2024-11-04 21:25:08 +00:00
Elliot DeNolf
e390835711 chore(release): v3.0.0-beta.124 [skip ci] 2024-11-04 14:47:38 -05:00
James Mikrut
35b107a103 fix: prefetch causing stale data (#9020)
Potentially fixes #9012 by disabling prefetch for all Next.js `Link`
component usage.

With prefetch left as the default and _on_, there were cases where the
prefetch could fetch stale data for Edit routes. Then, when navigating
to the Edit route, the data could be stale.

In addition, I think there is some strangeness happening on the Next.js
side where prefetched data might still come from the router cache even
though router cache is disabled.

This fix should be done regardless, but I suspect it will solve for a
lot of stale data issues.
2024-11-04 19:24:28 +00:00
Paul
6b9f178fcb fix: graphql missing options route resulting in failed cors preflight checks in production (#8987)
GraphQL currently doesn't pass CORS checks as we don't expose an OPTIONS
endpoint which is used for browser preflights.

Should also fix situations like this
https://github.com/payloadcms/payload/issues/8974
2024-11-04 14:20:09 -05:00
vahacreative
cca6746e1e feat(plugin-seo): add Turkish translation v3 (#8993) 2024-11-04 11:54:57 -06:00
Sasha
4349b78a2b fix: invalid select type with strictNullChecks: true (#8991)
### What?
Fixes type for the `select` property when having `strictNullChecks:
true` or `strict: true` in tsconfig.

### Why?
`select` should provide autocompletion for users, at this point it
doesn't work with this condtiion

### How?
Makes `collectionsSelect` and `globalsSelect` properties required in
`configToJSONSchema.ts`.

Fixes
https://github.com/payloadcms/payload/pull/8550#issuecomment-2452669237
2024-11-04 19:16:37 +02:00
Sasha
5b97ac1a67 fix: querying relationships by id path with REST (#9013)
### What?
Fixes the issue with querying by `id` from REST / `overrideAccess:
false`.
For example, this didn't work:

`/api/loans?where[book.bibliography.id][equals]=67224d74257b3f2acddc75f4`
```
QueryError: The following path cannot be queried: id
```

### Why?
We support this syntax within the Local API.

### How?
Now, for simplicity we sanitize everything like
`relation.otherRelation.id` to `relation.otherRelation`

Fixes https://github.com/payloadcms/payload/issues/9008
2024-11-04 17:57:41 +02:00
Patrik
f10a160462 docs: improves clarity for better readability of document-locking docs (#9010) 2024-11-04 09:26:08 -05:00
Elliot DeNolf
59ff8c18f5 chore: add project id source (#8983)
Add `projectIDSource` to analytics event.
2024-11-01 09:46:44 -04:00
Elliot DeNolf
10d5a8f9ae ci: force add triage action 2024-11-01 09:00:05 -04:00
Said Akhrarov
48d2ac1fce docs: include hasMany in upload field config options (#8978)
### What?
Includes `hasMany`, `minRows`, and `maxRows` in Upload field config
options table.

### Why?
To be inline with the type definitions.

### How?
Changes to `docs/fields/upload.mdx`
2024-11-01 05:40:04 -04:00
271 changed files with 62047 additions and 468 deletions

View File

@@ -17,9 +17,9 @@ inputs:
reproduction-link-section:
description: 'A regular expression string with "(.*)" matching a valid URL in the issue body. The result is trimmed. Example: "### Link to reproduction(.*)### To reproduce"'
default: '### Link to reproduction(.*)### To reproduce'
tag-only:
description: Log and tag only. Do not perform closing or commenting actions.
default: false
actions-to-perform:
description: 'Comma-separated list of actions to perform on the issue. Example: "tag,comment,close"'
default: 'tag,comment,close'
runs:
using: 'composite'
@@ -37,4 +37,4 @@ runs:
'INPUT_REPRODUCTION_INVALID_LABEL': ${{inputs.reproduction-invalid-label}}
'INPUT_REPRODUCTION_ISSUE_LABELS': ${{inputs.reproduction-issue-labels}}
'INPUT_REPRODUCTION_LINK_SECTION': ${{inputs.reproduction-link-section}}
'INPUT_TAG_ONLY': ${{inputs.tag-only}}
'INPUT_ACTIONS_TO_PERFORM': ${{inputs.actions-to-perform}}

34068
.github/actions/triage/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,9 @@ import { join } from 'node:path'
if (!process.env.GITHUB_TOKEN) throw new TypeError('No GITHUB_TOKEN provided')
if (!process.env.GITHUB_WORKSPACE) throw new TypeError('Not a GitHub workspace')
const validActionsToPerform = ['tag', 'comment', 'close'] as const
type ActionsToPerform = (typeof validActionsToPerform)[number]
// Define the configuration object
interface Config {
invalidLink: {
@@ -17,7 +20,7 @@ interface Config {
label: string
linkSection: string
}
tagOnly: boolean
actionsToPerform: ActionsToPerform[]
token: string
workspace: string
}
@@ -33,7 +36,16 @@ const config: Config = {
linkSection:
getInput('reproduction_link_section') || '### Link to reproduction(.*)### To reproduce',
},
tagOnly: getBooleanOrUndefined('tag_only') || false,
actionsToPerform: (getInput('actions_to_perform') || validActionsToPerform.join(','))
.split(',')
.map((a) => {
const action = a.trim().toLowerCase() as ActionsToPerform
if (validActionsToPerform.includes(action)) {
return action
}
throw new TypeError(`Invalid action: ${action}`)
}),
token: process.env.GITHUB_TOKEN,
workspace: process.env.GITHUB_WORKSPACE,
}
@@ -104,23 +116,31 @@ async function checkValidReproduction(): Promise<void> {
await Promise.all(
labelsToRemove.map((label) => client.issues.removeLabel({ ...common, name: label })),
)
info(`Issue #${issue.number} - validate label removed`)
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
info(`Issue #${issue.number} - labeled`)
// If tagOnly, do not close or comment
if (config.tagOnly) {
info('Tag-only enabled, no closing/commenting actions taken')
return
// Tag
if (config.actionsToPerform.includes('tag')) {
info(`Added label: ${config.invalidLink.label}`)
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
} else {
info('Tag - skipped, not provided in actions to perform')
}
// Perform closing and commenting actions
await client.issues.update({ ...common, state: 'closed' })
info(`Issue #${issue.number} - closed`)
// Comment
if (config.actionsToPerform.includes('comment')) {
const comment = join(config.workspace, config.invalidLink.comment)
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
info(`Commented with invalid reproduction message`)
} else {
info('Comment - skipped, not provided in actions to perform')
}
const comment = join(config.workspace, config.invalidLink.comment)
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
info(`Issue #${issue.number} - commented`)
// Close
if (config.actionsToPerform.includes('close')) {
await client.issues.update({ ...common, state: 'closed' })
info(`Closed issue #${issue.number}`)
} else {
info('Close - skipped, not provided in actions to perform')
}
}
/**

View File

@@ -1,4 +1,6 @@
We cannot recreate the issue with the provided information. **Please add a reproduction in order for us to be able to investigate.**
**Please add a reproduction in order for us to be able to investigate.**
Depending on the quality of reproduction steps, this issue may be closed if no reproduction is provided.
### Why was this issue marked with the `invalid-reproduction` label?

View File

@@ -99,4 +99,4 @@ jobs:
reproduction-comment: '.github/comments/invalid-reproduction.md'
reproduction-link-section: '### Link to the code that reproduces this issue(.*)### Reproduction Steps'
reproduction-issue-labels: 'validate-reproduction'
tag-only: 'true'
actions-to-perform: 'tag,comment'

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -2,7 +2,7 @@
title: Document Locking
label: Document Locking
order: 90
desc: Ensure your documents are locked while being edited, preventing concurrent edits from multiple users and preserving data integrity.
desc: Ensure your documents are locked during editing to prevent concurrent changes from multiple users and maintain data integrity.
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
---
@@ -12,19 +12,19 @@ The lock is automatically triggered when a user begins editing a document within
## How it works
When a user starts editing a document, Payload locks the document for that user. If another user tries to access the same document, they will be notified that it is currently being edited and can choose one of the following options:
When a user starts editing a document, Payload locks it for that user. If another user attempts to access the same document, they will be notified that it is currently being edited. They can then choose one of the following options:
- View in Read-Only Mode: View the document without making any changes.
- Take Over Editing: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
- View in Read-Only: View the document without the ability to make any changes.
- Take Over: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
- Return to Dashboard: Navigate away from the locked document and continue with other tasks.
The lock will automatically expire after a set period of inactivity, configurable using the duration property in the lockDocuments configuration, after which others can resume editing.
The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing.
<Banner type="info"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
<Banner type="info"> <strong>Note:</strong> If your application does not require document locking, you can disable this feature for any collection or global by setting the <code>lockDocuments</code> property to <code>false</code>. </Banner>
### Config Options
The lockDocuments property exists on both the Collection Config and the Global Config. By default, document locking is enabled for all collections and globals, but you can customize the lock duration or disable the feature entirely.
The `lockDocuments` property exists on both the Collection Config and the Global Config. Document locking is enabled by default, but you can customize the lock duration or turn off the feature for any collection or global.
Heres an example configuration for document locking:
@@ -55,13 +55,13 @@ export const Posts: CollectionConfig = {
### Impact on APIs
Document locking affects both the Local API and the REST API, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
Document locking affects both the Local and REST APIs, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
#### Overriding Locks
For operations like update and delete, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
For operations like `update` and `delete`, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`.

View File

@@ -126,12 +126,13 @@ powerful Admin UI.
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`collection`** \* | The `slug`s having the relationship field. |
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`where`** \* | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth). |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
| **`admin`** | Admin-specific configuration. [More details](#admin-config-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
@@ -182,11 +183,11 @@ returning. This is useful for performance reasons when you don't need the relate
The following query options are supported:
| Property | Description |
|-------------|--------------------------------------------------------------|
| **`limit`** | The maximum related documents to be returned, default is 10. |
| **`where`** | An optional `Where` query to filter joined documents. |
| **`sort`** | A string used to order related results |
| Property | Description |
|-------------|-----------------------------------------------------------------------------------------------------|
| **`limit`** | The maximum related documents to be returned, default is 10. |
| **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. |
| **`sort`** | A string used to order related results |
These can be applied to the local API, GraphQL, and REST API.

View File

@@ -48,6 +48,9 @@ export const MyUploadField: Field = {
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
| **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. |
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with hasMany. |
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/depth) |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |

View File

@@ -1,16 +1,34 @@
---
title: Jobs Queue
label: Jobs Queue
label: Overview
order: 10
desc: Payload provides all you need to run job queues, which are helpful to offload long-running processes into separate workers.
keywords: jobs queue, application framework, typescript, node, react, nextjs
---
## Defining tasks
Payload's Jobs Queue gives you a simple, yet powerful way to offload large or future tasks to separate compute resources.
A task is a simple function that can be executed directly or within a workflow. The difference between tasks and functions is that tasks can be run in the background, and can be retried if they fail.
For example, when building applications with Payload, you might run into a case where you need to perform some complex logic in a Payload [Hook](/docs/hooks/overview) but you don't want that hook to "block" or slow down the response returned from the Payload API.
Tasks can either be defined within the `jobs.tasks` array in your payload config, or they can be run inline within a workflow.
Instead of running long or expensive logic in a Hook, you can instead create a Job and add it to a Queue. It can then be picked up by a separate worker which periodically checks the queue for new jobs, and then executes each job accordingly. This way, your Payload API responses can remain as fast as possible, and you can still perform logic as necessary without blocking or affecting your users' experience.
Jobs are also handy for delegating certain actions to take place in the future, such as scheduling a post to be published at a later date. In this example, you could create a Job that will automatically publish a post at a certain time.
#### How it works
There are a few concepts that you should become familiarized with before using Payload's Jobs Queue - [Tasks](#tasks), [Workflows](#workflows), [Jobs](#jobs), and finally [Queues](#queues).
## Tasks
<Banner type="default">
A <strong>"Task"<strong> is a function definition that performs business logic and whose input and output are both strongly typed.
</Banner>
You can register Tasks on the Payload config, and then create Jobs or Workflows that use them. Think of Tasks like tidy, isolated "functions that do one specific thing".
Payload Tasks can be configured to automatically retried if they fail, which makes them valuable for "durable" workflows like AI applications where LLMs can return non-deterministic results, and might need to be retried.
Tasks can either be defined within the `jobs.tasks` array in your payload config, or they can be defined inline within a workflow.
### Defining tasks in the config
@@ -28,7 +46,9 @@ Simply add a task to the `jobs.tasks` array in your Payload config. A task consi
| `onSuccess` | Function to be executed if the task fails. |
| `retries` | Specify the number of times that this step should be retried if it fails. |
The handler is the function, or a path to the function, that will run once the job picks up this task. The handler function should return an object with an `output` key, which should contain the output of the task.
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks picks up a Job that includes this task.
It should return an object with an `output` key, which should contain the output of the task as you've defined.
Example:
@@ -38,8 +58,15 @@ export default buildConfig({
jobs: {
tasks: [
{
// Configure this task to automatically retry
// up to two times
retries: 2,
// This is a unique identifier for the task
slug: 'createPost',
// These are the arguments that your Task will accept
inputSchema: [
{
name: 'title',
@@ -47,6 +74,8 @@ export default buildConfig({
required: true,
},
],
// These are the properties that the function should output
outputSchema: [
{
name: 'postID',
@@ -54,6 +83,8 @@ export default buildConfig({
required: true,
},
],
// This is the function that is run when the task is invoked
handler: async ({ input, job, req }) => {
const newPost = await req.payload.create({
collection: 'post',
@@ -74,9 +105,11 @@ export default buildConfig({
})
```
### Example: defining external tasks
In addition to defining handlers as functions directly provided to your Payload config, you can also pass an _absolute path_ to where the handler is defined. If your task has large dependencies, and you are planning on executing your jobs in a separate process that has access to the filesystem, this could be a handy way to make sure that your Payload + Next.js app remains quick to compile and has minimal dependencies.
payload.config.ts:
In general, this is an advanced use case. Here's how this would look:
`payload.config.ts:`
```ts
import { fileURLToPath } from 'node:url'
@@ -86,26 +119,11 @@ const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
// ...
jobs: {
tasks: [
{
retries: 2,
slug: 'createPost',
inputSchema: [
{
name: 'title',
type: 'text',
required: true,
},
],
outputSchema: [
{
name: 'postID',
type: 'text',
required: true,
},
],
// ...
// The #createPostHandler is a named export within the `createPost.ts` file
handler: path.resolve(dirname, 'src/tasks/createPost.ts') + '#createPostHandler',
}
]
@@ -113,7 +131,9 @@ export default buildConfig({
})
```
src/tasks/createPost.ts:
Then, the `createPost` file itself:
`src/tasks/createPost.ts:`
```ts
import type { TaskHandler } from 'payload'
@@ -134,18 +154,23 @@ export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job,
}
```
## Workflows
## Defining workflows
<Banner type="default">
A <strong>"Workflow"<strong> is an optional way to <em>combine multiple tasks together</em> in a way that can be gracefully retried from the point of failure.
</Banner>
There are two types of workflows - JS-based workflows and JSON-based workflows.
They're most helpful when you have multiple tasks in a row, and you want to configure each task to be able to be retried if they fail.
### Defining JS-based workflows
If a task within a workflow fails, the Workflow will automatically "pick back up" on the task where it failed and **not re-execute any prior tasks that have already been executed**.
A JS-based function is a function in which you decide yourself when the tasks should run, by simply calling the `runTask` function. If the job, or any task within the job, fails, the entire function will re-run.
#### Defining a workflow
Tasks that have successfully been completed will simply re-return the cached output without running again, and failed tasks will be re-run.
The most important aspect of a Workflow is the `handler`, where you can declare when and how the tasks should run by simply calling the `runTask` function. If any task within the workflow, fails, the entire `handler` function will re-run.
Simply add a workflow to the `jobs.wokflows` array in your Payload config. A wokflow consists of the following fields:
However, importantly, tasks that have successfully been completed will simply re-return the cached and saved output without running again. The Workflow will pick back up where it failed and only task from the failure point onward will be re-executed.
To define a JS-based workflow, simply add a workflow to the `jobs.wokflows` array in your Payload config. A workflow consists of the following fields:
| Option | Description |
| --------------------------- | -------------------------------------------------------------------------------- |
@@ -168,6 +193,8 @@ export default buildConfig({
workflows: [
{
slug: 'createPostAndUpdate',
// The arguments that the workflow will accept
inputSchema: [
{
name: 'title',
@@ -175,15 +202,26 @@ export default buildConfig({
required: true,
},
],
// The handler that defines the "control flow" of the workflow
// Notice how it calls `runTask` to execute tasks
handler: async ({ job, runTask }) => {
// This workflow first runs a task called `createPost`
const output = await runTask({
task: 'createPost',
// You need to define a unique ID for this task invocation
// that will always be the same if this workflow fails
// and is re-executed in the future
id: '1',
input: {
title: job.input.title,
},
})
// Once the prior task completes, it will run a task
// called `updatePost`
await runTask({
task: 'updatePost',
id: '2',
@@ -201,9 +239,11 @@ export default buildConfig({
#### Running tasks inline
In order to run tasks inline without predefining them, you can use the `runTaskInline` function.
In the above example, our workflow was executing tasks that we already had defined in our Payload config. But, you can also run tasks without predefining them.
The drawbacks of this approach are that tasks cannot be re-used as easily, and the **task data stored in the job** will not be typed. In the following example, the inline task data will be stored on the job under `job.taskStatus.inline['2']` but completely untyped, as types for dynamic tasks like these cannot be generated beforehand.
To do this, you can use the `runTaskInline` function.
The drawbacks of this approach are that tasks cannot be re-used across workflows as easily, and the **task data stored in the job** will not be typed. In the following example, the inline task data will be stored on the job under `job.taskStatus.inline['2']` but completely untyped, as types for dynamic tasks like these cannot be generated beforehand.
Example:
@@ -225,6 +265,9 @@ export default buildConfig({
},
],
handler: async ({ job, runTask }) => {
// Here, we run a predefined task.
// The `createPost` handler arguments and return type
// are both strongly typed
const output = await runTask({
task: 'createPost',
id: '1',
@@ -233,11 +276,15 @@ export default buildConfig({
},
})
// Here, this task is not defined in the Payload config
// and is "inline". Its output will be stored on the Job in the database
// however its arguments will be untyped.
const { newPost } = await runTaskInline({
task: async ({ req }) => {
const newPost = await req.payload.update({
collection: 'post',
id: output.postID,
id: '2',
req,
retries: 3,
data: {
@@ -259,28 +306,37 @@ export default buildConfig({
})
```
### Defining JSON-based workflows
## Jobs
JSON-based workflows are a way to define the tasks the workflow should run in an array. The relationships between the tasks, their run order and their conditions are defined in the JSON object, which allows payload to statically analyze the workflow and will generate more helpful graphs.
Now that we have covered Tasks and Workflows, we can tie them together with a concept called a Job.
This functionality is not available yet, but it will be available in the future.
<Banner type="default">
Whereas you define Workflows and Tasks, which control your business logic, a <strong>Job</strong> is an individual instance of either a Task or a Workflow which contains many tasks.
</Banner>
## Queueing workflows and tasks
For example, let's say we have a Workflow or Task that describes the logic to sync information from Payload to a third-party system. This is how you'd declare how to sync that info, but it wouldn't do anything on its own. In order to run that task or workflow, you'd create a Job that references the corresponding Task or Workflow.
In order to queue a workflow or a task (= create them and add them to the queue), you can use the `payload.jobs.queue` function.
Jobs are stored in the Payload database in the `payload-jobs` collection, and you can decide to keep a running list of all jobs, or configure Payload to delete the job when it has been successfully executed.
Example: queueing workflows:
#### Queuing a new job
In order to queue a job, you can use the `payload.jobs.queue` function.
Here's how you'd queue a new Job, which will run a `createPostAndUpdate` workflow:
```ts
const createdJob = await payload.jobs.queue({
workflows: 'createPostAndUpdate',
// Pass the name of the workflow
workflow: 'createPostAndUpdate',
// The input type will be automatically typed
// according to the input you've defined for this workflow
input: {
title: 'my title',
},
})
```
Example: queueing tasks:
In addition to being able to queue new Jobs based on Workflows, you can also queue a job for a single Task:
```ts
const createdJob = await payload.jobs.queue({
@@ -291,55 +347,51 @@ const createdJob = await payload.jobs.queue({
})
```
## Running workflows and tasks
## Queues
Workflows and tasks added to the queue will not run unless a worker picks it up and runs it. This can be done in two ways:
Now let's talk about how to _run these jobs_. Right now, all we've covered is how to queue up jobs to run, but so far, we aren't actually running any jobs. This is the final piece of the puzzle.
### Endpoint
<Banner type="default">
A <strong>Queue</strong> is a list of jobs that should be executed in order of when they were added.
</Banner>
Make a fetch request to the `api/payload-jobs/run` endpoint:
When you go to run jobs, Payload will query for any jobs that are added to the queue and then run them. By default, all queued jobs are added to the `default` queue.
**But, imagine if you wanted to have some jobs that run nightly, and other jobs which should run every five minutes.**
By specifying the `queue` name when you queue a new job using `payload.jobs.queue()`, you can queue certain jobs with `queue: 'nightly'`, and other jobs can be left as the default queue.
Then, you could configure two different runner strategies:
1. A `cron` that runs nightly, querying for jobs added to the `nightly` queue
2. Another that runs any jobs that were added to the `default` queue every ~5 minutes or so
## Executing jobs
As mentioned above, you can queue jobs, but the jobs won't run unless a worker picks up your jobs and runs them. This can be done in two ways:
#### Endpoint
You can execute jobs by making a fetch request to the `/api/payload-jobs/run` endpoint:
```ts
await fetch('/api/payload-jobs/run', {
// Here, we're saying we want to run only 100 jobs for this invocation
// and we want to pull jobs from the `nightly` queue:
await fetch('/api/payload-jobs/run?limit=100&queue=nightly', {
method: 'GET',
headers: {
'Authorization': `JWT ${token}`,
'Authorization': `Bearer ${token}`,
},
});
```
### Local API
This endpoint is automatically mounted for you and is helpful in conjunction with serverless platforms like Vercel, where you might want to use Vercel Cron to invoke a serverless function that executes your jobs.
Run the payload.jobs.run function:
**Vercel Cron Example**
```ts
const results = await payload.jobs.run()
If you're deploying on Vercel, you can add a `vercel.json` file in the root of your project that configures Vercel Cron to invoke the `run` endpoint on a cron schedule.
// You can customize the queue name by passing it as an argument
await payload.jobs.run({ queue: 'posts' })
```
### Script
You can run the jobs:run script from the command line:
```sh
npx payload jobs:run --queue default --limit 10
```
#### Triggering jobs as cronjob
You can pass the --cron flag to the jobs:run script to run the jobs in a cronjob:
```sh
npx payload jobs:run --cron "*/5 * * * *"
```
### Vercel Cron
Vercel Cron allows scheduled tasks to be executed automatically by triggering specific endpoints. Below is a step-by-step guide to configuring Vercel Cron for running queued jobs on apps hosted on Vercel:
1. Add Vercel Cron Configuration: Place a vercel.json file at the root of your project with the following content:
Here's an example of what this file will look like:
```json
{
@@ -352,13 +404,13 @@ Vercel Cron allows scheduled tasks to be executed automatically by triggering sp
}
```
This configuration schedules the endpoint `/api/payload-jobs/run` to be triggered every 5 minutes. This endpoint is added automatically by payload and is responsible for running the queued jobs.
The configuration above schedules the endpoint `/api/payload-jobs/run` to be invoked every 5 minutes.
2. Environment Variable Setup: By default, the endpoint may require a JWT token for authorization. However, Vercel Cron jobs cannot pass JWT tokens. Instead, you can use an environment variable to secure the endpoint:
The last step will be to secure your `run` endpoint so that only the proper users can invoke the runner.
Add a new environment variable named `CRON_SECRET` to your Vercel project settings. This should be a random string, ideally 16 characters or longer.
To do this, you can set an environment variable on your Vercel project called `CRON_SECRET`, which should be a random stringideally 16 characters or longer.
3. Modify Authentication for Job Running: Adjust the job running authorization logic in your project to accept the `CRON_SECRET` as a valid token. Modify your `payload.config.ts` file as follows:
Then, you can modify the `access` function for running jobs by ensuring that only Vercel can invoke your runner.
```ts
export default buildConfig({
@@ -366,6 +418,12 @@ export default buildConfig({
jobs: {
access: {
run: ({ req }: { req: PayloadRequest }): boolean => {
// Allow logged in users to execute this endpoint (default)
if (req.user) return true
// If there is no logged in user, then check
// for the Vercel Cron secret to be present as an
// Authorization header:
const authHeader = req.headers.get('authorization');
return authHeader === `Bearer ${process.env.CRON_SECRET}`;
},
@@ -375,8 +433,31 @@ export default buildConfig({
})
```
This code snippet ensures that the jobs can only be triggered if the correct `CRON_SECRET` is provided in the authorization header.
Vercel will automatically make the `CRON_SECRET` environment variable available to the endpoint when triggered by the Vercel Cron, ensuring that the jobs can be run securely.
This works because Vercel automatically makes the `CRON_SECRET` environment variable available to the endpoint as the `Authorization` header when triggered by the Vercel Cron, ensuring that the jobs can be run securely.
After the project is deployed to Vercel, the Vercel Cron job will automatically trigger the `/api/payload-jobs/run` endpoint in the specified schedule, running the queued jobs in the background.
#### Local API
If you want to process jobs programmatically from your server-side code, you can use the Local API:
```ts
const results = await payload.jobs.run()
// You can customize the queue name and limit by passing them as arguments:
await payload.jobs.run({ queue: 'nightly', limit: 100 })
```
#### Bin script
Finally, you can process jobs via the bin script that comes with Payload out of the box.
```sh
npx payload jobs:run --queue default --limit 10
```
In addition, the bin script allows you to pass a `--cron` flag to the `jobs:run` command to run the jobs on a scheduled, cron basis:
```sh
npx payload jobs:run --cron "*/5 * * * *"
```

View File

@@ -84,6 +84,7 @@ You can specify more options within the Local API vs. REST or GraphQL due to the
| `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. |
| `locale` | Specify [locale](/docs/configuration/localization) for any returned documents. |
| `select` | Specify [select](../queries/select) to control which fields to include to the result. |
| `populate` | Specify [populate](../queries/select#populate) to control which fields to include to the result from populated documents. |
| `fallbackLocale` | Specify a [fallback locale](/docs/configuration/localization) to use for any returned documents. |
| `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. |
| `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). |

View File

@@ -128,3 +128,31 @@ export const Pages: CollectionConfig<'pages'> = {
],
}
```
## `populate`
You can override `defaultPopulate` with the `populate` property in the Local and REST API
Local API:
```ts
const getPosts = async () => {
const posts = await payload.find({
collection: 'posts',
populate: {
// Select only `text` from populated docs in the "pages" collection
pages: {
text: true,
}, // highlight-line
},
})
return posts
}
```
REST API:
```ts
fetch('https://localhost:3000/api/posts?populate[pages][text]=true') // highlight-line
.then((res) => res.json())
.then((data) => console.log(data))
```

View File

@@ -19,6 +19,7 @@ All Payload API routes are mounted and prefixed to your config's `routes.api` UR
- [locale](/docs/configuration/localization#retrieving-localized-docs) - retrieves document(s) in a specific locale
- [fallback-locale](/docs/configuration/localization#retrieving-localized-docs) - specifies a fallback locale if no locale value exists
- [select](../queries/select) - specifies which fields to include to the result
- [populate](../queries/select#populate) - specifies which fields to include to the result from populated documents
## Collections

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,8 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.123",
"version": "3.0.0-beta.126",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.123",
"version": "3.0.0-beta.126",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -101,7 +101,7 @@ export async function createProject(args: {
})
// Remove yarn.lock file. This is only desired in Payload Cloud.
const lockPath = path.resolve(projectDir, 'yarn.lock')
const lockPath = path.resolve(projectDir, 'pnpm-lock.yaml')
if (fse.existsSync(lockPath)) {
await fse.remove(lockPath)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.123",
"version": "3.0.0-beta.126",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,8 @@
import type { PipelineStage } from 'mongoose'
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
import { combineQueries } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { buildSortParam } from '../queries/buildSortParam.js'
@@ -62,6 +64,10 @@ export const buildJoinAggregation = async ({
continue
}
if (joins?.[join.schemaPath] === false) {
continue
}
const {
limit: limitJoin = join.field.defaultLimit ?? 10,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { Field, JoinQuery, SelectMode, SelectType, TabAsField } from 'payload'
import { and, eq, sql } from 'drizzle-orm'
import { combineQueries } from 'payload'
import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
@@ -402,11 +403,17 @@ export const traverseFields = ({
break
}
const joinSchemaPath = `${path.replaceAll('_', '.')}${field.name}`
if (joinQuery[joinSchemaPath] === false) {
break
}
const {
limit: limitArg = field.defaultLimit ?? 10,
sort = field.defaultSort,
where,
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
} = joinQuery[joinSchemaPath] || {}
let limit = limitArg
if (limit !== 0) {

View File

@@ -61,7 +61,7 @@ export const createDatabase = async function (this: BasePostgresAdapter, args: A
try {
await managementClient.connect()
await managementClient.query(`CREATE DATABASE ${dbName}`)
await managementClient.query(`CREATE DATABASE "${dbName}"`)
this.payload.logger.info(`Created database "${dbName}"`)

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.123",
"version": "3.0.0-beta.126",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.123",
"version": "3.0.0-beta.126",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -67,6 +67,7 @@ export const DocumentTabLink: React.FC<{
<Link
className={`${baseClass}__link`}
href={!isActive || href !== pathname ? hrefWithLocale : ''}
prefetch={false}
{...(newTab && { rel: 'noopener noreferrer', target: '_blank' })}
tabIndex={isActive ? -1 : 0}
>

View File

@@ -98,6 +98,7 @@ export const DefaultNavClient: React.FC = () => {
href={href}
id={id}
key={i}
prefetch={Link ? false : undefined}
tabIndex={!navOpen ? -1 : undefined}
>
{activeCollection && <div className={`${baseClass}__link-indicator`} />}

View File

@@ -6,4 +6,5 @@ export {
OPTIONS as REST_OPTIONS,
PATCH as REST_PATCH,
POST as REST_POST,
PUT as REST_PUT,
} from '../routes/rest/index.js'

View File

@@ -6,6 +6,7 @@ import { isNumber } from 'payload/shared'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const create: CollectionRouteHandler = async ({ collection, req }) => {
@@ -20,6 +21,7 @@ export const create: CollectionRouteHandler = async ({ collection, req }) => {
data: req.data,
depth: isNumber(depth) ? depth : undefined,
draft,
populate: sanitizePopulate(req.query.populate),
req,
select: sanitizeSelect(req.query.select),
})

View File

@@ -8,12 +8,14 @@ import { isNumber } from 'payload/shared'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, overrideLock, select, where } = req.query as {
const { depth, overrideLock, populate, select, where } = req.query as {
depth?: string
overrideLock?: string
populate?: Record<string, unknown>
select?: Record<string, unknown>
where?: Where
}
@@ -22,6 +24,7 @@ export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) =>
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
overrideLock: Boolean(overrideLock === 'true'),
populate: sanitizePopulate(populate),
req,
select: sanitizeSelect(select),
where,

View File

@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const deleteByID: CollectionRouteHandlerWithID = async ({
@@ -28,6 +29,7 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({
collection,
depth: isNumber(depth) ? depth : undefined,
overrideLock: Boolean(overrideLock === 'true'),
populate: sanitizePopulate(req.query.populate),
req,
select: sanitizeSelect(req.query.select),
})

View File

@@ -7,6 +7,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const duplicate: CollectionRouteHandlerWithID = async ({
@@ -30,6 +31,7 @@ export const duplicate: CollectionRouteHandlerWithID = async ({
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft,
populate: sanitizePopulate(req.query.populate),
req,
select: sanitizeSelect(req.query.select),
})

View File

@@ -8,15 +8,17 @@ import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const find: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, draft, joins, limit, page, select, sort, where } = req.query as {
const { depth, draft, joins, limit, page, populate, select, sort, where } = req.query as {
depth?: string
draft?: string
joins?: JoinQuery
limit?: string
page?: string
populate?: Record<string, unknown>
select?: Record<string, unknown>
sort?: string
where?: Where
@@ -29,6 +31,7 @@ export const find: CollectionRouteHandler = async ({ collection, req }) => {
joins: sanitizeJoinParams(joins),
limit: isNumber(limit) ? Number(limit) : undefined,
page: isNumber(page) ? Number(page) : undefined,
populate: sanitizePopulate(populate),
req,
select: sanitizeSelect(select),
sort: typeof sort === 'string' ? sort.split(',') : undefined,

View File

@@ -9,6 +9,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
import { sanitizeJoinParams } from '../utilities/sanitizeJoinParams.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const findByID: CollectionRouteHandlerWithID = async ({
@@ -31,6 +32,7 @@ export const findByID: CollectionRouteHandlerWithID = async ({
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
joins: sanitizeJoinParams(req.query.joins as JoinQuery),
populate: sanitizePopulate(req.query.populate),
req,
select: sanitizeSelect(req.query.select),
})

View File

@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const findVersionByID: CollectionRouteHandlerWithID = async ({
@@ -26,6 +27,7 @@ export const findVersionByID: CollectionRouteHandlerWithID = async ({
id,
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
populate: sanitizePopulate(req.query.populate),
req,
select: sanitizeSelect(req.query.select),
})

View File

@@ -7,13 +7,15 @@ import { isNumber } from 'payload/shared'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const findVersions: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, limit, page, select, sort, where } = req.query as {
const { depth, limit, page, populate, select, sort, where } = req.query as {
depth?: string
limit?: string
page?: string
populate?: Record<string, unknown>
select?: Record<string, unknown>
sort?: string
where?: Where
@@ -24,6 +26,7 @@ export const findVersions: CollectionRouteHandler = async ({ collection, req })
depth: isNumber(depth) ? Number(depth) : undefined,
limit: isNumber(limit) ? Number(limit) : undefined,
page: isNumber(page) ? Number(page) : undefined,
populate: sanitizePopulate(populate),
req,
select: sanitizeSelect(select),
sort: typeof sort === 'string' ? sort.split(',') : undefined,

View File

@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
export const restoreVersion: CollectionRouteHandlerWithID = async ({
id: incomingID,
@@ -27,6 +28,7 @@ export const restoreVersion: CollectionRouteHandlerWithID = async ({
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: draft === 'true' ? true : undefined,
populate: sanitizePopulate(req.query.populate),
req,
})

View File

@@ -8,14 +8,16 @@ import { isNumber } from 'payload/shared'
import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const update: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, draft, limit, overrideLock, select, where } = req.query as {
const { depth, draft, limit, overrideLock, populate, select, where } = req.query as {
depth?: string
draft?: string
limit?: string
overrideLock?: string
populate?: Record<string, unknown>
select?: Record<string, unknown>
where?: Where
}
@@ -27,6 +29,7 @@ export const update: CollectionRouteHandler = async ({ collection, req }) => {
draft: draft === 'true',
limit: isNumber(limit) ? Number(limit) : undefined,
overrideLock: Boolean(overrideLock === 'true'),
populate: sanitizePopulate(populate),
req,
select: sanitizeSelect(select),
where,

View File

@@ -6,6 +6,7 @@ import type { CollectionRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const updateByID: CollectionRouteHandlerWithID = async ({
@@ -34,6 +35,7 @@ export const updateByID: CollectionRouteHandlerWithID = async ({
depth: isNumber(depth) ? Number(depth) : undefined,
draft,
overrideLock: Boolean(overrideLock === 'true'),
populate: sanitizePopulate(req.query.populate),
publishSpecificLocale,
req,
select: sanitizeSelect(req.query.select),

View File

@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
import type { GlobalRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => {
@@ -16,6 +17,7 @@ export const findOne: GlobalRouteHandler = async ({ globalConfig, req }) => {
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
globalConfig,
populate: sanitizePopulate(req.query.populate),
req,
select: sanitizeSelect(req.query.select),
})

View File

@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
import type { GlobalRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConfig, req }) => {
@@ -15,6 +16,7 @@ export const findVersionByID: GlobalRouteHandlerWithID = async ({ id, globalConf
id,
depth: isNumber(depth) ? Number(depth) : undefined,
globalConfig,
populate: sanitizePopulate(req.query.populate),
req,
select: sanitizeSelect(req.query.select),
})

View File

@@ -7,13 +7,15 @@ import { isNumber } from 'payload/shared'
import type { GlobalRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) => {
const { depth, limit, page, select, sort, where } = req.query as {
const { depth, limit, page, populate, select, sort, where } = req.query as {
depth?: string
limit?: string
page?: string
populate?: Record<string, unknown>
select?: Record<string, unknown>
sort?: string
where?: Where
@@ -24,6 +26,7 @@ export const findVersions: GlobalRouteHandler = async ({ globalConfig, req }) =>
globalConfig,
limit: isNumber(limit) ? Number(limit) : undefined,
page: isNumber(page) ? Number(page) : undefined,
populate: sanitizePopulate(populate),
req,
select: sanitizeSelect(select),
sort: typeof sort === 'string' ? sort.split(',') : undefined,

View File

@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
import type { GlobalRouteHandlerWithID } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
export const restoreVersion: GlobalRouteHandlerWithID = async ({ id, globalConfig, req }) => {
const { searchParams } = req
@@ -16,6 +17,7 @@ export const restoreVersion: GlobalRouteHandlerWithID = async ({ id, globalConfi
depth: isNumber(depth) ? Number(depth) : undefined,
draft: draft === 'true' ? true : undefined,
globalConfig,
populate: sanitizePopulate(req.query.populate),
req,
})

View File

@@ -5,6 +5,7 @@ import { isNumber } from 'payload/shared'
import type { GlobalRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'
import { sanitizePopulate } from '../utilities/sanitizePopulate.js'
import { sanitizeSelect } from '../utilities/sanitizeSelect.js'
export const update: GlobalRouteHandler = async ({ globalConfig, req }) => {
@@ -21,6 +22,7 @@ export const update: GlobalRouteHandler = async ({ globalConfig, req }) => {
depth: isNumber(depth) ? Number(depth) : undefined,
draft,
globalConfig,
populate: sanitizePopulate(req.query.populate),
publishSpecificLocale,
req,
select: sanitizeSelect(req.query.select),

View File

@@ -821,3 +821,87 @@ export const PATCH =
})
}
}
export const PUT =
(config: Promise<SanitizedConfig> | SanitizedConfig) =>
async (request: Request, { params: paramsPromise }: { params: Promise<{ slug: string[] }> }) => {
const { slug } = await paramsPromise
const [slug1] = slug
let req: PayloadRequest
let res: Response
let collection: Collection
try {
req = await createPayloadRequest({
config,
request,
})
collection = req.payload.collections?.[slug1]
const disableEndpoints = endpointsAreDisabled({
endpoints: req.payload.config.endpoints,
request,
})
if (disableEndpoints) {
return disableEndpoints
}
if (collection) {
req.routeParams.collection = slug1
const disableEndpoints = endpointsAreDisabled({
endpoints: collection.config.endpoints,
request,
})
if (disableEndpoints) {
return disableEndpoints
}
const customEndpointResponse = await handleCustomEndpoints({
endpoints: collection.config.endpoints,
entitySlug: slug1,
req,
})
if (customEndpointResponse) {
return customEndpointResponse
}
}
if (res instanceof Response) {
if (req.responseHeaders) {
const mergedResponse = new Response(res.body, {
headers: mergeHeaders(req.responseHeaders, res.headers),
status: res.status,
statusText: res.statusText,
})
return mergedResponse
}
return res
}
// root routes
const customEndpointResponse = await handleCustomEndpoints({
endpoints: req.payload.config.endpoints,
req,
})
if (customEndpointResponse) {
return customEndpointResponse
}
return RouteNotFoundResponse({
slug,
req,
})
} catch (error) {
return routeError({
collection,
config,
err: error,
req: req || request,
})
}
}

View File

@@ -53,7 +53,7 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
// Or better yet, use a CDN like Google Fonts if ever supported
fontData = fs.readFile(path.join(dirname, 'roboto-regular.woff'))
} catch (e) {
console.error(`Error reading font file or not readable: ${e.message}`) // eslint-disable-line no-console
req.payload.logger.error(`Error reading font file or not readable: ${e.message}`)
}
const fontFamily = 'Roboto, sans-serif'
@@ -86,7 +86,7 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
},
)
} catch (e: any) {
console.error(`${e.message}`) // eslint-disable-line no-console
req.payload.logger.error(`Error generating Open Graph image: ${e.message}`)
return NextResponse.json({ error: `Internal Server Error: ${e.message}` }, { status: 500 })
}
}

View File

@@ -9,21 +9,27 @@ import { isNumber } from 'payload/shared'
export const sanitizeJoinParams = (
joins:
| {
[schemaPath: string]: {
limit?: unknown
sort?: string
where?: unknown
}
[schemaPath: string]:
| {
limit?: unknown
sort?: string
where?: unknown
}
| false
}
| false = {},
): JoinQuery => {
const joinQuery = {}
Object.keys(joins).forEach((schemaPath) => {
joinQuery[schemaPath] = {
limit: isNumber(joins[schemaPath]?.limit) ? Number(joins[schemaPath].limit) : undefined,
sort: joins[schemaPath]?.sort ? joins[schemaPath].sort : undefined,
where: joins[schemaPath]?.where ? joins[schemaPath].where : undefined,
if (joins[schemaPath] === 'false' || joins[schemaPath] === false) {
joinQuery[schemaPath] = false
} else {
joinQuery[schemaPath] = {
limit: isNumber(joins[schemaPath]?.limit) ? Number(joins[schemaPath].limit) : undefined,
sort: joins[schemaPath]?.sort ? joins[schemaPath].sort : undefined,
where: joins[schemaPath]?.where ? joins[schemaPath].where : undefined,
}
}
})

View File

@@ -0,0 +1,15 @@
import type { PopulateType } from 'payload'
import { sanitizeSelect } from './sanitizeSelect.js'
export const sanitizePopulate = (unsanitizedPopulate: unknown): PopulateType | undefined => {
if (!unsanitizedPopulate || typeof unsanitizedPopulate !== 'object') {
return
}
for (const k in unsanitizedPopulate) {
unsanitizedPopulate[k] = sanitizeSelect(unsanitizedPopulate[k])
}
return unsanitizedPopulate as PopulateType
}

View File

@@ -47,7 +47,7 @@ export const getDocumentData = async (args: {
formState,
}
} catch (error) {
console.error('Error getting document data', error) // eslint-disable-line no-console
req.payload.logger.error({ err: error, msg: 'Error getting document data' })
return {
data: null,
formState: {

View File

@@ -60,7 +60,7 @@ export const getDocumentPermissions = async (args: {
}).then(({ update }) => update?.permission)
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
req.payload.logger.error(error)
}
}
@@ -87,7 +87,7 @@ export const getDocumentPermissions = async (args: {
}).then(({ update }) => update?.permission)
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
req.payload.logger.error(error)
}
}

View File

@@ -42,6 +42,7 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
adminRoute,
path: accountRoute,
})}
prefetch={false}
>
{children}
</Link>
@@ -68,6 +69,7 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
adminRoute,
path: loginRoute,
})}
prefetch={false}
>
{i18n.t('authentication:backToLogin')}
</Link>

View File

@@ -105,6 +105,7 @@ export const LoginForm: React.FC<{
adminRoute,
path: forgotRoute,
})}
prefetch={false}
>
{t('authentication:forgotPasswordQuestion')}
</Link>

View File

@@ -48,6 +48,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
adminRoute,
path: accountRoute,
})}
prefetch={false}
>
{children}
</Link>
@@ -75,6 +76,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
adminRoute,
path: loginRoute,
})}
prefetch={false}
>
{i18n.t('authentication:backToLogin')}
</Link>

View File

@@ -100,7 +100,7 @@ const Restore: React.FC<Props> = ({
className={[canRestoreAsDraft && `${baseClass}__button`].filter(Boolean).join(' ')}
onClick={() => toggleModal(modalSlug)}
size="small"
SubMenuPopupContent={
SubMenuPopupContent={() =>
canRestoreAsDraft && (
<PopupList.ButtonGroup>
<PopupList.Button onClick={() => [setDraft(true), toggleModal(modalSlug)]}>

View File

@@ -47,7 +47,7 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
}
return (
<Link href={to}>
<Link href={to} prefetch={false}>
{cellData &&
formatDate({ date: cellData as Date | number | string, i18n, pattern: dateFormat })}
</Link>

View File

@@ -61,7 +61,7 @@ export async function getLatestVersion(args: Args): Promise<ReturnType> {
updatedAt: response.docs[0].updatedAt,
}
} catch (e) {
console.error(e)
payload.logger.error(e)
return null
}
}

View File

@@ -96,7 +96,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
})
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
payload.logger.error(error)
}
}
@@ -139,7 +139,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
})
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
payload.logger.error(error)
}
if (!versionsData) {

View File

@@ -13,6 +13,11 @@ export const withPayload = (nextConfig = {}) => {
env.NEXT_PUBLIC_ENABLE_ROUTER_CACHE_REFRESH = 'true'
}
const poweredByHeader = {
key: 'X-Powered-By',
value: 'Next.js, Payload',
}
/**
* @type {import('next').NextConfig}
*/
@@ -41,6 +46,8 @@ export const withPayload = (nextConfig = {}) => {
},
},
},
// We disable the poweredByHeader here because we add it manually in the headers function below
...(nextConfig?.poweredByHeader !== false ? { poweredByHeader: false } : {}),
headers: async () => {
const headersFromConfig = 'headers' in nextConfig ? await nextConfig.headers() : []
@@ -61,6 +68,7 @@ export const withPayload = (nextConfig = {}) => {
key: 'Critical-CH',
value: 'Sec-CH-Prefers-Color-Scheme',
},
...(nextConfig?.poweredByHeader !== false ? [poweredByHeader] : []),
],
},
]

View File

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

View File

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

View File

@@ -12,7 +12,8 @@ import type {
Validate,
} from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { JsonObject, Payload, PayloadRequest, RequestContext } from '../types/index.js'
import type { RequestContext } from '../index.js'
import type { JsonObject, Payload, PayloadRequest, PopulateType } from '../types/index.js'
import type { RichTextFieldClientProps } from './fields/RichText.js'
import type { CreateMappedComponent } from './types.js'
@@ -28,7 +29,6 @@ export type AfterReadRichTextHookArgs<
draft?: boolean
fallbackLocale?: string
fieldPromises?: Promise<void>[]
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
@@ -43,6 +43,8 @@ export type AfterReadRichTextHookArgs<
overrideAccess?: boolean
populate?: PopulateType
populationPromises?: Promise<void>[]
showHiddenFields?: boolean
triggerAccessControl?: boolean
@@ -224,6 +226,7 @@ type RichTextAdapterBase<
findMany: boolean
flattenLocales: boolean
overrideAccess?: boolean
populateArg?: PopulateType
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean

View File

@@ -35,13 +35,13 @@ import type { Field, JoinField, RelationshipField, UploadField } from '../../fie
import type {
CollectionSlug,
JsonObject,
RequestContext,
TypedAuthOperations,
TypedCollection,
TypedCollectionSelect,
} from '../../index.js'
import type {
PayloadRequest,
RequestContext,
SelectType,
Sort,
TransformCollectionWithSelect,

View File

@@ -4,6 +4,7 @@ import type { CollectionSlug, JsonObject } from '../../index.js'
import type {
Document,
PayloadRequest,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../types/index.js'
@@ -44,6 +45,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
draft?: boolean
overrideAccess?: boolean
overwriteExistingFiles?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -97,11 +99,12 @@ export const createOperation = async <
draft = false,
overrideAccess,
overwriteExistingFiles = false,
populate,
req: {
fallbackLocale,
locale,
payload,
payload: { config, email },
payload: { config },
},
req,
select,
@@ -304,6 +307,7 @@ export const createOperation = async <
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -2,7 +2,7 @@ import httpStatus from 'http-status'
import type { AccessResult } from '../../config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, SelectType, Where } from '../../types/index.js'
import type { PayloadRequest, PopulateType, SelectType, Where } from '../../types/index.js'
import type {
BeforeOperationHook,
BulkOperationResult,
@@ -31,6 +31,7 @@ export type Arguments = {
disableTransaction?: boolean
overrideAccess?: boolean
overrideLock?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -72,6 +73,7 @@ export const deleteOperation = async <
depth,
overrideAccess,
overrideLock,
populate,
req: {
fallbackLocale,
locale,
@@ -203,6 +205,7 @@ export const deleteOperation = async <
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -1,6 +1,7 @@
import type { CollectionSlug } from '../../index.js'
import type {
PayloadRequest,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../types/index.js'
@@ -27,6 +28,7 @@ export type Arguments = {
id: number | string
overrideAccess?: boolean
overrideLock?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -66,6 +68,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
depth,
overrideAccess,
overrideLock,
populate,
req: {
fallbackLocale,
locale,
@@ -188,6 +191,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -6,6 +6,7 @@ import type { FindOneArgs } from '../../database/types.js'
import type { CollectionSlug } from '../../index.js'
import type {
PayloadRequest,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../types/index.js'
@@ -41,6 +42,7 @@ export type Arguments = {
draft?: boolean
id: number | string
overrideAccess?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -81,6 +83,7 @@ export const duplicateOperation = async <
depth,
draft: draftArg = true,
overrideAccess,
populate,
req: { fallbackLocale, locale: localeArg, payload },
req,
select,
@@ -306,6 +309,7 @@ export const duplicateOperation = async <
global: null,
locale: localeArg,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -3,6 +3,7 @@ import type { PaginatedDocs } from '../../database/types.js'
import type { CollectionSlug, JoinQuery } from '../../index.js'
import type {
PayloadRequest,
PopulateType,
SelectType,
Sort,
TransformCollectionWithSelect,
@@ -17,6 +18,7 @@ import type {
import executeAccess from '../../auth/executeAccess.js'
import { combineQueries } from '../../database/combineQueries.js'
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
import { sanitizeJoinQuery } from '../../database/sanitizeJoinQuery.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
@@ -37,6 +39,7 @@ export type Arguments = {
overrideAccess?: boolean
page?: number
pagination?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -83,6 +86,7 @@ export const findOperation = async <
overrideAccess,
page,
pagination = true,
populate,
req: { fallbackLocale, locale, payload },
req,
select,
@@ -129,6 +133,13 @@ export const findOperation = async <
let fullWhere = combineQueries(where, accessResult)
const sanitizedJoins = await sanitizeJoinQuery({
collectionConfig,
joins,
overrideAccess,
req,
})
if (collectionConfig.versions?.drafts && draftsEnabled) {
fullWhere = appendVersionToQueryKey(fullWhere)
@@ -142,7 +153,7 @@ export const findOperation = async <
result = await payload.db.queryDrafts<DataFromCollectionSlug<TSlug>>({
collection: collectionConfig.slug,
joins: req.payloadAPI === 'GraphQL' ? false : joins,
joins: req.payloadAPI === 'GraphQL' ? false : sanitizedJoins,
limit: sanitizedLimit,
locale,
page: sanitizedPage,
@@ -162,7 +173,7 @@ export const findOperation = async <
result = await payload.db.find<DataFromCollectionSlug<TSlug>>({
collection: collectionConfig.slug,
joins: req.payloadAPI === 'GraphQL' ? false : joins,
joins: req.payloadAPI === 'GraphQL' ? false : sanitizedJoins,
limit: sanitizedLimit,
locale,
page: sanitizedPage,
@@ -286,6 +297,7 @@ export const findOperation = async <
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -3,6 +3,7 @@ import type { CollectionSlug, JoinQuery } from '../../index.js'
import type {
ApplyDisableErrors,
PayloadRequest,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../types/index.js'
@@ -14,8 +15,10 @@ import type {
import executeAccess from '../../auth/executeAccess.js'
import { combineQueries } from '../../database/combineQueries.js'
import { sanitizeJoinQuery } from '../../database/sanitizeJoinQuery.js'
import { NotFound } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { validateQueryPaths } from '../../index.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable.js'
import { buildAfterOperation } from './utils.js'
@@ -30,6 +33,7 @@ export type Arguments = {
includeLockStatus?: boolean
joins?: JoinQuery
overrideAccess?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -72,6 +76,7 @@ export const findByIDOperation = async <
includeLockStatus,
joins,
overrideAccess = false,
populate,
req: { fallbackLocale, locale, t },
req,
select,
@@ -91,17 +96,33 @@ export const findByIDOperation = async <
return null
}
const where = combineQueries({ id: { equals: id } }, accessResult)
const sanitizedJoins = await sanitizeJoinQuery({
collectionConfig,
joins,
overrideAccess,
req,
})
const findOneArgs: FindOneArgs = {
collection: collectionConfig.slug,
joins: req.payloadAPI === 'GraphQL' ? false : joins,
joins: req.payloadAPI === 'GraphQL' ? false : sanitizedJoins,
locale,
req: {
transactionID: req.transactionID,
} as PayloadRequest,
select,
where: combineQueries({ id: { equals: id } }, accessResult),
where,
}
await validateQueryPaths({
collectionConfig,
overrideAccess,
req,
where,
})
// /////////////////////////////////////
// Find by ID
// /////////////////////////////////////
@@ -223,6 +244,7 @@ export const findByIDOperation = async <
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -1,6 +1,6 @@
import httpStatus from 'http-status'
import type { PayloadRequest, SelectType } from '../../types/index.js'
import type { PayloadRequest, PopulateType, SelectType } from '../../types/index.js'
import type { TypeWithVersion } from '../../versions/types.js'
import type { Collection, TypeWithID } from '../config/types.js'
@@ -17,6 +17,7 @@ export type Arguments = {
disableErrors?: boolean
id: number | string
overrideAccess?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -32,6 +33,7 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
depth,
disableErrors,
overrideAccess,
populate,
req: { fallbackLocale, locale, payload },
req,
select,
@@ -121,6 +123,7 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
global: null,
locale,
overrideAccess,
populate,
req,
select: typeof select?.version === 'object' ? select.version : undefined,
showHiddenFields,

View File

@@ -1,5 +1,5 @@
import type { PaginatedDocs } from '../../database/types.js'
import type { PayloadRequest, SelectType, Sort, Where } from '../../types/index.js'
import type { PayloadRequest, PopulateType, SelectType, Sort, Where } from '../../types/index.js'
import type { TypeWithVersion } from '../../versions/types.js'
import type { Collection } from '../config/types.js'
@@ -18,6 +18,7 @@ export type Arguments = {
overrideAccess?: boolean
page?: number
pagination?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -35,6 +36,7 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
overrideAccess,
page,
pagination = true,
populate,
req: { fallbackLocale, locale, payload },
req,
select,
@@ -129,6 +131,7 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
global: null,
locale,
overrideAccess,
populate,
req,
select: typeof select?.version === 'object' ? select.version : undefined,
showHiddenFields,

View File

@@ -1,5 +1,5 @@
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, RequestContext, Where } from '../../../types/index.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, Where } from '../../../types/index.js'
import { APIError } from '../../../errors/index.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js'

View File

@@ -1,8 +1,8 @@
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type {
Document,
PayloadRequest,
RequestContext,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../../types/index.js'
@@ -34,6 +34,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
locale?: TypedLocale
overrideAccess?: boolean
overwriteExistingFiles?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: TSelect
showHiddenFields?: boolean
@@ -59,6 +60,7 @@ export default async function createLocal<
filePath,
overrideAccess = true,
overwriteExistingFiles = false,
populate,
select,
showHiddenFields,
} = options
@@ -82,6 +84,7 @@ export default async function createLocal<
draft,
overrideAccess,
overwriteExistingFiles,
populate,
req,
select,
showHiddenFields,

View File

@@ -1,8 +1,8 @@
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type {
Document,
PayloadRequest,
RequestContext,
PopulateType,
SelectType,
TransformCollectionWithSelect,
Where,
@@ -26,6 +26,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
locale?: TypedLocale
overrideAccess?: boolean
overrideLock?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: TSelect
showHiddenFields?: boolean
@@ -88,6 +89,7 @@ async function deleteLocal<
disableTransaction,
overrideAccess = true,
overrideLock,
populate,
select,
showHiddenFields,
where,
@@ -108,6 +110,7 @@ async function deleteLocal<
disableTransaction,
overrideAccess,
overrideLock,
populate,
req: await createLocalReq(options, payload),
select,
showHiddenFields,

View File

@@ -1,13 +1,13 @@
import type { CollectionSlug, TypedLocale } from '../../..//index.js'
import type { Payload } from '../../../index.js'
import type { Payload, RequestContext } from '../../../index.js'
import type {
Document,
PayloadRequest,
RequestContext,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../../types/index.js'
import type { DataFromCollectionSlug, SelectFromCollectionSlug } from '../../config/types.js'
import type { SelectFromCollectionSlug } from '../../config/types.js'
import { APIError } from '../../../errors/index.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js'
@@ -26,6 +26,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
id: number | string
locale?: TypedLocale
overrideAccess?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: TSelect
showHiddenFields?: boolean
@@ -46,6 +47,7 @@ export async function duplicate<
disableTransaction,
draft,
overrideAccess = true,
populate,
select,
showHiddenFields,
} = options
@@ -73,6 +75,7 @@ export async function duplicate<
disableTransaction,
draft,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -1,9 +1,15 @@
import type { PaginatedDocs } from '../../../database/types.js'
import type { CollectionSlug, JoinQuery, Payload, TypedLocale } from '../../../index.js'
import type {
CollectionSlug,
JoinQuery,
Payload,
RequestContext,
TypedLocale,
} from '../../../index.js'
import type {
Document,
PayloadRequest,
RequestContext,
PopulateType,
SelectType,
Sort,
TransformCollectionWithSelect,
@@ -27,12 +33,13 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
draft?: boolean
fallbackLocale?: TypedLocale
includeLockStatus?: boolean
joins?: JoinQuery
joins?: JoinQuery<TSlug>
limit?: number
locale?: 'all' | TypedLocale
overrideAccess?: boolean
page?: number
pagination?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: TSelect
showHiddenFields?: boolean

View File

@@ -1,10 +1,17 @@
/* eslint-disable no-restricted-exports */
import type { CollectionSlug, JoinQuery, Payload, SelectType, TypedLocale } from '../../../index.js'
import type {
CollectionSlug,
JoinQuery,
Payload,
RequestContext,
SelectType,
TypedLocale,
} from '../../../index.js'
import type {
ApplyDisableErrors,
Document,
PayloadRequest,
RequestContext,
PopulateType,
TransformCollectionWithSelect,
} from '../../../types/index.js'
import type { SelectFromCollectionSlug } from '../../config/types.js'
@@ -30,9 +37,10 @@ export type Options<
fallbackLocale?: TypedLocale
id: number | string
includeLockStatus?: boolean
joins?: JoinQuery
joins?: JoinQuery<TSlug>
locale?: 'all' | TypedLocale
overrideAccess?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: TSelect
showHiddenFields?: boolean
@@ -57,6 +65,7 @@ export default async function findByIDLocal<
includeLockStatus,
joins,
overrideAccess = true,
populate,
select,
showHiddenFields,
} = options
@@ -79,6 +88,7 @@ export default async function findByIDLocal<
includeLockStatus,
joins,
overrideAccess,
populate,
req: await createLocalReq(options, payload),
select,
showHiddenFields,

View File

@@ -1,5 +1,5 @@
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, RequestContext, SelectType } from '../../../types/index.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, PopulateType, SelectType } from '../../../types/index.js'
import type { TypeWithVersion } from '../../../versions/types.js'
import type { DataFromCollectionSlug } from '../../config/types.js'
@@ -20,6 +20,7 @@ export type Options<TSlug extends CollectionSlug> = {
id: string
locale?: 'all' | TypedLocale
overrideAccess?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -36,6 +37,7 @@ export default async function findVersionByIDLocal<TSlug extends CollectionSlug>
depth,
disableErrors = false,
overrideAccess = true,
populate,
select,
showHiddenFields,
} = options
@@ -56,6 +58,7 @@ export default async function findVersionByIDLocal<TSlug extends CollectionSlug>
depth,
disableErrors,
overrideAccess,
populate,
req: await createLocalReq(options, payload),
select,
showHiddenFields,

View File

@@ -1,9 +1,9 @@
import type { PaginatedDocs } from '../../../database/types.js'
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type {
Document,
PayloadRequest,
RequestContext,
PopulateType,
SelectType,
Sort,
Where,
@@ -28,6 +28,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: 'all' | TypedLocale
overrideAccess?: boolean
page?: number
populate?: PopulateType
req?: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -46,6 +47,7 @@ export default async function findVersionsLocal<TSlug extends CollectionSlug>(
limit,
overrideAccess = true,
page,
populate,
select,
showHiddenFields,
sort,
@@ -66,6 +68,7 @@ export default async function findVersionsLocal<TSlug extends CollectionSlug>(
limit,
overrideAccess,
page,
populate,
req: await createLocalReq(options, payload),
select,
showHiddenFields,

View File

@@ -1,5 +1,5 @@
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, RequestContext, SelectType } from '../../../types/index.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, PopulateType, SelectType } from '../../../types/index.js'
import type { DataFromCollectionSlug } from '../../config/types.js'
import { APIError } from '../../../errors/index.js'
@@ -18,6 +18,7 @@ export type Options<TSlug extends CollectionSlug> = {
id: string
locale?: TypedLocale
overrideAccess?: boolean
populate?: PopulateType
req?: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -33,6 +34,7 @@ export default async function restoreVersionLocal<TSlug extends CollectionSlug>(
collection: collectionSlug,
depth,
overrideAccess = true,
populate,
select,
showHiddenFields,
} = options
@@ -53,6 +55,7 @@ export default async function restoreVersionLocal<TSlug extends CollectionSlug>(
depth,
overrideAccess,
payload,
populate,
req: await createLocalReq(options, payload),
select,
showHiddenFields,

View File

@@ -1,10 +1,10 @@
import type { DeepPartial } from 'ts-essentials'
import type { CollectionSlug, Payload, TypedLocale } from '../../../index.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type {
Document,
PayloadRequest,
RequestContext,
PopulateType,
SelectType,
TransformCollectionWithSelect,
Where,
@@ -40,6 +40,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
overrideAccess?: boolean
overrideLock?: boolean
overwriteExistingFiles?: boolean
populate?: PopulateType
publishSpecificLocale?: string
req?: PayloadRequest
select?: TSelect
@@ -112,6 +113,7 @@ async function updateLocal<
overrideAccess = true,
overrideLock,
overwriteExistingFiles = false,
populate,
publishSpecificLocale,
select,
showHiddenFields,
@@ -142,6 +144,7 @@ async function updateLocal<
overrideLock,
overwriteExistingFiles,
payload,
populate,
publishSpecificLocale,
req,
select,

View File

@@ -1,7 +1,7 @@
import httpStatus from 'http-status'
import type { FindOneArgs } from '../../database/types.js'
import type { PayloadRequest, SelectType } from '../../types/index.js'
import type { PayloadRequest, PopulateType, SelectType } from '../../types/index.js'
import type { Collection, TypeWithID } from '../config/types.js'
import executeAccess from '../../auth/executeAccess.js'
@@ -21,6 +21,7 @@ export type Arguments = {
draft?: boolean
id: number | string
overrideAccess?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -35,6 +36,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
depth,
draft,
overrideAccess = false,
populate,
req,
req: { fallbackLocale, locale, payload },
select,
@@ -152,6 +154,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -4,7 +4,7 @@ import httpStatus from 'http-status'
import type { AccessResult } from '../../config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, SelectType, Where } from '../../types/index.js'
import type { PayloadRequest, PopulateType, SelectType, Where } from '../../types/index.js'
import type {
BulkOperationResult,
Collection,
@@ -46,6 +46,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
overrideAccess?: boolean
overrideLock?: boolean
overwriteExistingFiles?: boolean
populate?: PopulateType
req: PayloadRequest
select?: SelectType
showHiddenFields?: boolean
@@ -89,6 +90,7 @@ export const updateOperation = async <
overrideAccess,
overrideLock,
overwriteExistingFiles = false,
populate,
req: {
fallbackLocale,
locale,
@@ -361,6 +363,7 @@ export const updateOperation = async <
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -7,6 +7,7 @@ import type { Args } from '../../fields/hooks/beforeChange/index.js'
import type { CollectionSlug } from '../../index.js'
import type {
PayloadRequest,
PopulateType,
SelectType,
TransformCollectionWithSelect,
} from '../../types/index.js'
@@ -51,6 +52,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
overrideAccess?: boolean
overrideLock?: boolean
overwriteExistingFiles?: boolean
populate?: PopulateType
publishSpecificLocale?: string
req: PayloadRequest
select?: SelectType
@@ -99,6 +101,7 @@ export const updateByIDOperation = async <
overrideAccess,
overrideLock,
overwriteExistingFiles = false,
populate,
publishSpecificLocale,
req: {
fallbackLocale,
@@ -306,7 +309,12 @@ export const updateByIDOperation = async <
}
if (publishSpecificLocale) {
publishedDocWithLocales = await getLatestCollectionVersion({
versionSnapshotResult = await beforeChange({
...beforeChangeArgs,
docWithLocales,
})
const lastPublished = await getLatestCollectionVersion({
id,
config: collectionConfig,
payload,
@@ -315,10 +323,7 @@ export const updateByIDOperation = async <
req,
})
versionSnapshotResult = await beforeChange({
...beforeChangeArgs,
docWithLocales,
})
publishedDocWithLocales = lastPublished ? lastPublished : {}
}
let result = await beforeChange({
@@ -392,6 +397,7 @@ export const updateByIDOperation = async <
global: null,
locale,
overrideAccess,
populate,
req,
select,
showHiddenFields,

View File

@@ -73,6 +73,19 @@ export async function validateSearchParam({
})
}
const promises = []
// Sanitize relation.otherRelation.id to relation.otherRelation
if (paths.at(-1)?.path === 'id') {
const previousField = paths.at(-2)?.field
if (
previousField &&
(previousField.type === 'relationship' || previousField.type === 'upload') &&
typeof previousField.relationTo === 'string'
) {
paths.pop()
}
}
promises.push(
...paths.map(async ({ collectionSlug, field, invalid, path }, i) => {
if (invalid) {
@@ -115,6 +128,7 @@ export async function validateSearchParam({
) {
fieldPath = fieldPath.replace('.value', '')
}
const entityType: 'collections' | 'globals' = globalConfig ? 'globals' : 'collections'
const entitySlug = collectionSlug || globalConfig.slug
const segments = fieldPath.split('.')

View File

@@ -0,0 +1,92 @@
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { JoinQuery, PayloadRequest } from '../types/index.js'
import executeAccess from '../auth/executeAccess.js'
import { QueryError } from '../errors/QueryError.js'
import { combineQueries } from './combineQueries.js'
import { validateQueryPaths } from './queryValidation/validateQueryPaths.js'
type Args = {
collectionConfig: SanitizedCollectionConfig
joins?: JoinQuery
overrideAccess: boolean
req: PayloadRequest
}
/**
* * Validates `where` for each join
* * Combines the access result for joined collection
* * Combines the default join's `where`
*/
export const sanitizeJoinQuery = async ({
collectionConfig,
joins: joinsQuery,
overrideAccess,
req,
}: Args) => {
if (joinsQuery === false) {
return false
}
if (!joinsQuery) {
joinsQuery = {}
}
const errors: { path: string }[] = []
const promises: Promise<void>[] = []
for (const collectionSlug in collectionConfig.joins) {
for (const { field, schemaPath } of collectionConfig.joins[collectionSlug]) {
if (joinsQuery[schemaPath] === false) {
continue
}
const joinCollectionConfig = req.payload.collections[collectionSlug].config
const accessResult = !overrideAccess
? await executeAccess({ disableErrors: true, req }, joinCollectionConfig.access.read)
: true
if (accessResult === false) {
joinsQuery[schemaPath] = false
continue
}
if (!joinsQuery[schemaPath]) {
joinsQuery[schemaPath] = {}
}
const joinQuery = joinsQuery[schemaPath]
if (!joinQuery.where) {
joinQuery.where = {}
}
if (field.where) {
joinQuery.where = combineQueries(joinQuery.where, field.where)
}
if (typeof accessResult === 'object') {
joinQuery.where = combineQueries(joinQuery.where, accessResult)
}
promises.push(
validateQueryPaths({
collectionConfig: joinCollectionConfig,
errors,
overrideAccess,
req,
where: joinQuery.where,
}),
)
}
}
await Promise.all(promises)
if (errors.length > 0) {
throw new QueryError(errors)
}
return joinsQuery
}

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