Compare commits

..

10 Commits

Author SHA1 Message Date
Jacob Fletcher
c550411a86 fixes args 2025-05-01 12:44:24 -04:00
Jacob Fletcher
172ecc014f adds test 2025-05-01 12:11:11 -04:00
Jacob Fletcher
da1c3ff9b4 lifts potentially stale path 2025-05-01 11:33:19 -04:00
Janus Reith
35c0404817 feat(live-preview): expose requestHandler to subscribe.ts (#10947)
### What?
As described in https://github.com/payloadcms/payload/discussions/10946,
allow passing a custom `collectionPopulationRequestHandler` function to
`subscribe`, which passes it along to `handleMessage` and `mergeData`

### Why?
`mergeData` already supports a custom function for this, that
functionality however isn't exposed.
My use case so far was passing along custom Authorization headers.


### How?
Move the functions type defined in `mergeData` to a dedicated
`CollectionPopulationRequestHandler` type, reuse it across `subscribe`,
`handleMessage` and `mergeData`.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-04-30 15:08:53 -04:00
Elliot DeNolf
cfe8c97ab7 chore(release): v3.36.1 [skip ci] 2025-04-30 14:52:46 -04:00
Dan Ribbens
6133a1d183 perf: optimize file access promises (#12275)
Improves performance in local strategy uploads by reading the file and
metadata info synchronously. This change uses `promise.all` for three
separately awaited calls. This improves the perf by making all calls in
a non-blocking way.
2025-04-30 18:26:28 +00:00
Sasha
710fe0949b fix: duplicate with orderable (#12274)
Previously, duplication with orderable collections worked incorrectly,
for example

Document 1 is created - `_order: 'a5'`
Document 2 is duplicated from 1, - `_order: 'a5 - copy'` (result from
47a1eee765/packages/payload/src/fields/setDefaultBeforeDuplicate.ts (L6))

Now, the `_order` value is re-calculated properly.
2025-04-30 17:28:13 +00:00
Sasha
4a56597b92 fix(db-postgres): count crashes when query contains subqueries and doesn't return any rows (#12273)
Fixes https://github.com/payloadcms/payload/issues/12264

Uses safe object access in `countDistinct`, fallbacks to `0`
2025-04-30 16:53:36 +00:00
Sasha
27d644f2f9 perf(db-postgres): skip pagination overhead if limit: 0 is passed (#12261)
This improves performance when querying data in Postgers / SQLite with
`limit: 0`. Before, unless you additionally passed `pagination: false`
we executed additional count query to calculate the pagination. Now we
skip this as this is unnecessary since we can retrieve the count just
from `rows.length`.

This logic already existed in `db-mongodb` -
1b17df9e0b/packages/db-mongodb/src/find.ts (L114-L124)
2025-04-30 19:31:04 +03:00
Sasha
564fdb0e17 fix: virtual relationship fields with select (#12266)
Continuation of https://github.com/payloadcms/payload/pull/12265.

Currently, using `select` on new relationship virtual fields:
```
const doc = await payload.findByID({
  collection: 'virtual-relations',
  depth: 0,
  id,
  select: { postTitle: true },
})
```
doesn't work, because in order to calculate `post.title`, the `post`
field must be selected as well. This PR adds logic that sanitizes the
incoming `select` to include those relationships into `select` (that are
related to selected virtual fields)

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-04-30 12:27:04 -04:00
113 changed files with 703 additions and 159 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.36.0",
"version": "3.36.1",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.36.0",
"version": "3.36.1",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.36.0",
"version": "3.36.1",
"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.36.0",
"version": "3.36.1",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -16,7 +16,7 @@ export const countDistinct: CountDistinct = async function countDistinct(
})
.from(this.tables[tableName])
.where(where)
return Number(countResult[0]?.count)
return Number(countResult?.[0]?.count ?? 0)
}
let query: SQLiteSelect = db
@@ -39,5 +39,5 @@ export const countDistinct: CountDistinct = async function countDistinct(
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
const countResult = await query
return Number(countResult[0]?.count)
return Number(countResult?.[0]?.count ?? 0)
}

View File

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

View File

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

View File

@@ -46,6 +46,7 @@ export const findMany = async function find({
const offset = skip || (page - 1) * limit
if (limit === 0) {
pagination = false
limit = undefined
}

View File

@@ -16,7 +16,8 @@ export const countDistinct: CountDistinct = async function countDistinct(
})
.from(this.tables[tableName])
.where(where)
return Number(countResult[0].count)
return Number(countResult?.[0]?.count ?? 0)
}
let query = db
@@ -39,5 +40,5 @@ export const countDistinct: CountDistinct = async function countDistinct(
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
const countResult = await query
return Number(countResult[0].count)
return Number(countResult?.[0]?.count ?? 0)
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.36.0",
"version": "3.36.1",
"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.36.0",
"version": "3.36.1",
"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.36.0",
"version": "3.36.1",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
import type { FieldSchemaJSON } from 'payload'
import type { LivePreviewMessageEvent } from './types.js'
import type { CollectionPopulationRequestHandler, LivePreviewMessageEvent } from './types.js'
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
import { mergeData } from './mergeData.js'
@@ -29,9 +29,10 @@ export const handleMessage = async <T extends Record<string, any>>(args: {
depth?: number
event: LivePreviewMessageEvent<T>
initialData: T
requestHandler?: CollectionPopulationRequestHandler
serverURL: string
}): Promise<T> => {
const { apiRoute, depth, event, initialData, serverURL } = args
const { apiRoute, depth, event, initialData, requestHandler, serverURL } = args
if (isLivePreviewEvent(event, serverURL)) {
const { data, externallyUpdatedRelationship, fieldSchemaJSON, locale } = event.data
@@ -57,6 +58,7 @@ export const handleMessage = async <T extends Record<string, any>>(args: {
incomingData: data,
initialData: _payloadLivePreview?.previousData || initialData,
locale,
requestHandler,
serverURL,
})

View File

@@ -1,6 +1,6 @@
import type { DocumentEvent, FieldSchemaJSON, PaginatedDocs } from 'payload'
import type { PopulationsByCollection } from './types.js'
import type { CollectionPopulationRequestHandler, PopulationsByCollection } from './types.js'
import { traverseFields } from './traverseFields.js'
@@ -29,21 +29,17 @@ let prevLocale: string | undefined
export const mergeData = async <T extends Record<string, any>>(args: {
apiRoute?: string
collectionPopulationRequestHandler?: ({
apiPath,
endpoint,
serverURL,
}: {
apiPath: string
endpoint: string
serverURL: string
}) => Promise<Response>
/**
* @deprecated Use `requestHandler` instead
*/
collectionPopulationRequestHandler?: CollectionPopulationRequestHandler
depth?: number
externallyUpdatedRelationship?: DocumentEvent
fieldSchema: FieldSchemaJSON
incomingData: Partial<T>
initialData: T
locale?: string
requestHandler?: CollectionPopulationRequestHandler
returnNumberOfRequests?: boolean
serverURL: string
}): Promise<
@@ -81,7 +77,8 @@ export const mergeData = async <T extends Record<string, any>>(args: {
let res: PaginatedDocs
const ids = new Set(populations.map(({ id }) => id))
const requestHandler = args.collectionPopulationRequestHandler || defaultRequestHandler
const requestHandler =
args.collectionPopulationRequestHandler || args.requestHandler || defaultRequestHandler
try {
res = await requestHandler({

View File

@@ -1,3 +1,5 @@
import type { CollectionPopulationRequestHandler } from './types.js'
import { handleMessage } from './handleMessage.js'
export const subscribe = <T extends Record<string, any>>(args: {
@@ -5,9 +7,10 @@ export const subscribe = <T extends Record<string, any>>(args: {
callback: (data: T) => void
depth?: number
initialData: T
requestHandler?: CollectionPopulationRequestHandler
serverURL: string
}): ((event: MessageEvent) => Promise<void> | void) => {
const { apiRoute, callback, depth, initialData, serverURL } = args
const { apiRoute, callback, depth, initialData, requestHandler, serverURL } = args
const onMessage = async (event: MessageEvent) => {
const mergedData = await handleMessage<T>({
@@ -15,6 +18,7 @@ export const subscribe = <T extends Record<string, any>>(args: {
depth,
event,
initialData,
requestHandler,
serverURL,
})

View File

@@ -1,5 +1,15 @@
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
export type CollectionPopulationRequestHandler = ({
apiPath,
endpoint,
serverURL,
}: {
apiPath: string
endpoint: string
serverURL: string
}) => Promise<Response>
export type LivePreviewArgs = {}
export type LivePreview = void

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ import type {
type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
type ArrayFieldBaseClientProps = {
readonly potentiallyStalePath?: string
readonly validate?: ArrayFieldValidation
} & FieldPaths

View File

@@ -23,6 +23,7 @@ import type {
type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
type BlocksFieldBaseClientProps = {
readonly potentiallyStalePath?: string
readonly validate?: BlocksFieldValidation
} & FieldPaths

View File

@@ -28,6 +28,7 @@ type CheckboxFieldBaseClientProps = {
readonly onChange?: (value: boolean) => void
readonly partialChecked?: boolean
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: CheckboxFieldValidation
}

View File

@@ -26,6 +26,7 @@ type CodeFieldBaseClientProps = {
readonly autoComplete?: string
readonly onMount?: EditorProps['onMount']
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: CodeFieldValidation
}

View File

@@ -18,7 +18,9 @@ import type {
FieldLabelServerComponent,
} from '../types.js'
type CollapsibleFieldBaseClientProps = FieldPaths
type CollapsibleFieldBaseClientProps = {
readonly potentiallyStalePath?: string
} & FieldPaths
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>

View File

@@ -23,6 +23,7 @@ type DateFieldClientWithoutType = MarkOptional<DateFieldClient, 'type'>
type DateFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: DateFieldValidation
}

View File

@@ -23,6 +23,7 @@ type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
type EmailFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: EmailFieldValidation
}

View File

@@ -22,7 +22,9 @@ type GroupFieldClientWithoutType = MarkOptional<GroupFieldClient, 'type'>
type GroupFieldBaseServerProps = Pick<FieldPaths, 'path'>
export type GroupFieldBaseClientProps = FieldPaths
export type GroupFieldBaseClientProps = {
readonly potentiallyStalePath?: string
} & FieldPaths
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType> &
GroupFieldBaseClientProps

View File

@@ -4,6 +4,7 @@ type HiddenFieldBaseClientProps = {
readonly disableModifyingForm?: false
readonly field?: never
readonly path: string
readonly potentiallyStalePath?: string
readonly value?: unknown
}

View File

@@ -23,6 +23,7 @@ type JSONFieldClientWithoutType = MarkOptional<JSONFieldClient, 'type'>
type JSONFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: JSONFieldValidation
}

View File

@@ -22,6 +22,7 @@ type JoinFieldClientWithoutType = MarkOptional<JoinFieldClient, 'type'>
type JoinFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
}
type JoinFieldBaseServerProps = Pick<FieldPaths, 'path'>

View File

@@ -24,6 +24,7 @@ type NumberFieldClientWithoutType = MarkOptional<NumberFieldClient, 'type'>
type NumberFieldBaseClientProps = {
readonly onChange?: (e: number) => void
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: NumberFieldValidation
}

View File

@@ -23,6 +23,7 @@ type PointFieldClientWithoutType = MarkOptional<PointFieldClient, 'type'>
type PointFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: PointFieldValidation
}

View File

@@ -28,6 +28,7 @@ type RadioFieldBaseClientProps = {
readonly disableModifyingForm?: boolean
readonly onChange?: OnChange
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: RadioFieldValidation
readonly value?: string
}

View File

@@ -23,6 +23,7 @@ type RelationshipFieldClientWithoutType = MarkOptional<RelationshipFieldClient,
type RelationshipFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: RelationshipFieldValidation
}

View File

@@ -31,6 +31,7 @@ type RichTextFieldBaseClientProps<
TExtraProperties = object,
> = {
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: RichTextFieldValidation
}

View File

@@ -24,6 +24,7 @@ type SelectFieldClientWithoutType = MarkOptional<SelectFieldClient, 'type'>
type SelectFieldBaseClientProps = {
readonly onChange?: (e: string | string[]) => void
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: SelectFieldValidation
readonly value?: string
}

View File

@@ -31,7 +31,9 @@ export type ClientTab =
>)
| ({ fields: ClientField[]; passesCondition?: boolean } & Omit<UnnamedTab, 'fields'>)
type TabsFieldBaseClientProps = FieldPaths
type TabsFieldBaseClientProps = {
potentiallyStalePath?: string
} & FieldPaths
type TabsFieldClientWithoutType = MarkOptional<TabsFieldClient, 'type'>

View File

@@ -26,6 +26,7 @@ type TextFieldBaseClientProps = {
readonly inputRef?: React.RefObject<HTMLInputElement>
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: TextFieldValidation
}

View File

@@ -26,6 +26,7 @@ type TextareaFieldBaseClientProps = {
readonly inputRef?: React.Ref<HTMLInputElement>
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: TextareaFieldValidation
}

View File

@@ -15,6 +15,7 @@ type UIFieldClientWithoutType = MarkOptional<UIFieldClient, 'type'>
type UIFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
}
type UIFieldBaseServerProps = Pick<FieldPaths, 'path'>

View File

@@ -23,6 +23,7 @@ type UploadFieldClientWithoutType = MarkOptional<UploadFieldClient, 'type'>
type UploadFieldBaseClientProps = {
readonly path: string
readonly potentiallyStalePath?: string
readonly validate?: UploadFieldValidation
}

View File

@@ -247,6 +247,7 @@ export const createOperation = async <
let doc
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -110,6 +110,7 @@ export const deleteOperation = async <
const fullWhere = combineQueries(where, accessResult)
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -168,6 +168,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
}
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -102,6 +102,7 @@ export const findOperation = async <
} = args
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -87,6 +87,7 @@ export const findByIDOperation = async <
} = args
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -11,6 +11,7 @@ import { APIError, Forbidden, NotFound } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
export type Arguments = {
@@ -70,8 +71,10 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// /////////////////////////////////////
const select = sanitizeSelect({
fields: buildVersionCollectionFields(payload.config, collectionConfig, true),
forceSelect: getQueryDraftsSelect({ select: collectionConfig.forceSelect }),
select: incomingSelect,
versions: true,
})
const versionsQuery = await payload.db.findVersions<TData>({

View File

@@ -72,8 +72,10 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
const fullWhere = combineQueries(where, accessResults)
const select = sanitizeSelect({
fields: buildVersionCollectionFields(payload.config, collectionConfig, true),
forceSelect: getQueryDraftsSelect({ select: collectionConfig.forceSelect }),
select: incomingSelect,
versions: true,
})
// /////////////////////////////////////

View File

@@ -117,6 +117,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// /////////////////////////////////////
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -201,6 +201,7 @@ export const updateOperation = async <
try {
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -161,6 +161,7 @@ export const updateByIDOperation = async <
})
const select = sanitizeSelect({
fields: collectionConfig.flattenedFields,
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -83,6 +83,13 @@ export const addOrderableFieldsAndHook = (
hidden: true,
readOnly: true,
},
hooks: {
beforeDuplicate: [
({ siblingData }) => {
delete siblingData[orderableFieldName]
},
],
},
index: true,
required: true,
// override the schema to make order fields optional for payload.create()
@@ -275,5 +282,6 @@ export const addOrderableEndpoint = (config: SanitizedConfig) => {
if (!config.endpoints) {
config.endpoints = []
}
config.endpoints.push(reorderEndpoint)
}

View File

@@ -58,6 +58,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
// add default user collection if none provided
if (!sanitizedConfig?.admin?.user) {
const firstCollectionWithAuth = sanitizedConfig.collections.find(({ auth }) => Boolean(auth))
if (firstCollectionWithAuth) {
sanitizedConfig.admin.user = firstCollectionWithAuth.slug
} else {
@@ -69,6 +70,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
const userCollection = sanitizedConfig.collections.find(
({ slug }) => slug === sanitizedConfig.admin.user,
)
if (!userCollection || !userCollection.auth) {
throw new InvalidConfiguration(
`${sanitizedConfig.admin.user} is not a valid admin user collection`,

View File

@@ -53,6 +53,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
}
const select = sanitizeSelect({
fields: globalConfig.flattenedFields,
forceSelect: globalConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -11,6 +11,8 @@ import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
import { buildVersionGlobalFields } from '../../versions/buildGlobalFields.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
export type Arguments = {
@@ -60,8 +62,10 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
const hasWhereAccess = typeof accessResults === 'object'
const select = sanitizeSelect({
fields: buildVersionGlobalFields(payload.config, globalConfig, true),
forceSelect: getQueryDraftsSelect({ select: globalConfig.forceSelect }),
select: incomingSelect,
versions: true,
})
const findGlobalVersionsArgs: FindGlobalVersionsArgs = {

View File

@@ -70,8 +70,10 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
const fullWhere = combineQueries(where, accessResults)
const select = sanitizeSelect({
fields: buildVersionGlobalFields(payload.config, globalConfig, true),
forceSelect: getQueryDraftsSelect({ select: globalConfig.forceSelect }),
select: incomingSelect,
versions: true,
})
// /////////////////////////////////////

View File

@@ -246,6 +246,7 @@ export const updateOperation = async <
// /////////////////////////////////////
const select = sanitizeSelect({
fields: globalConfig.flattenedFields,
forceSelect: globalConfig.forceSelect,
select: incomingSelect,
})

View File

@@ -10,7 +10,7 @@ import type { SanitizedConfig } from '../config/types.js'
import type { PayloadRequest } from '../types/index.js'
import type { FileData, FileToSave, ProbedImageSize, UploadEdits } from './types.js'
import { FileRetrievalError, FileUploadError, MissingFile } from '../errors/index.js'
import { FileRetrievalError, FileUploadError, Forbidden, MissingFile } from '../errors/index.js'
import { canResizeImage } from './canResizeImage.js'
import { cropImage } from './cropImage.js'
import { getExternalFile } from './getExternalFile.js'
@@ -85,6 +85,10 @@ export const generateFileData = async <T>({
if (!file && uploadEdits && incomingFileData) {
const { filename, url } = incomingFileData as FileData
if (filename && (filename.includes('../') || filename.includes('..\\'))) {
throw new Forbidden(req.t)
}
try {
if (url && url.startsWith('/') && !disableLocalStorage) {
const filePath = `${staticPath}/${filename}`

View File

@@ -5,28 +5,28 @@ import path from 'path'
import type { PayloadRequest } from '../types/index.js'
const mimeTypeEstimate = {
const mimeTypeEstimate: Record<string, string> = {
svg: 'image/svg+xml',
}
export const getFileByPath = async (filePath: string): Promise<PayloadRequest['file']> => {
if (typeof filePath === 'string') {
const data = await fs.readFile(filePath)
const mimetype = fileTypeFromFile(filePath)
const { size } = await fs.stat(filePath)
const name = path.basename(filePath)
const ext = path.extname(filePath).slice(1)
const mime = (await mimetype)?.mime || mimeTypeEstimate[ext]
return {
name,
data,
mimetype: mime,
size,
}
if (typeof filePath !== 'string') {
return undefined
}
return undefined
const name = path.basename(filePath)
const ext = path.extname(filePath).slice(1)
const [data, stat, type] = await Promise.all([
fs.readFile(filePath),
fs.stat(filePath),
fileTypeFromFile(filePath),
])
return {
name,
data,
mimetype: type?.mime || mimeTypeEstimate[ext],
size: stat.size,
}
}

View File

@@ -1,17 +1,129 @@
import { deepMergeSimple } from '@payloadcms/translations/utilities'
import type { SelectType } from '../types/index.js'
import type { FlattenedField } from '../fields/config/types.js'
import type { SelectIncludeType, SelectType } from '../types/index.js'
import { getSelectMode } from './getSelectMode.js'
// Transform post.title -> post, post.category.title -> post
const stripVirtualPathToCurrentCollection = ({
fields,
path,
versions,
}: {
fields: FlattenedField[]
path: string
versions: boolean
}) => {
const resultSegments: string[] = []
if (versions) {
resultSegments.push('version')
const versionField = fields.find((each) => each.name === 'version')
if (versionField && versionField.type === 'group') {
fields = versionField.flattenedFields
}
}
for (const segment of path.split('.')) {
const field = fields.find((each) => each.name === segment)
if (!field) {
continue
}
resultSegments.push(segment)
if (field.type === 'relationship' || field.type === 'upload') {
return resultSegments.join('.')
}
}
return resultSegments.join('.')
}
const getAllVirtualRelations = ({ fields }: { fields: FlattenedField[] }) => {
const result: string[] = []
for (const field of fields) {
if ('virtual' in field && typeof field.virtual === 'string') {
result.push(field.virtual)
} else if (field.type === 'group' || field.type === 'tab') {
const nestedResult = getAllVirtualRelations({ fields: field.flattenedFields })
for (const nestedItem of nestedResult) {
result.push(nestedItem)
}
}
}
return result
}
const resolveVirtualRelationsToSelect = ({
fields,
selectValue,
topLevelFields,
versions,
}: {
fields: FlattenedField[]
selectValue: SelectIncludeType | true
topLevelFields: FlattenedField[]
versions: boolean
}) => {
const result: string[] = []
if (selectValue === true) {
for (const item of getAllVirtualRelations({ fields })) {
result.push(
stripVirtualPathToCurrentCollection({ fields: topLevelFields, path: item, versions }),
)
}
} else {
for (const fieldName in selectValue) {
const field = fields.find((each) => each.name === fieldName)
if (!field) {
continue
}
if ('virtual' in field && typeof field.virtual === 'string') {
result.push(
stripVirtualPathToCurrentCollection({
fields: topLevelFields,
path: field.virtual,
versions,
}),
)
} else if (field.type === 'group' || field.type === 'tab') {
for (const item of resolveVirtualRelationsToSelect({
fields: field.flattenedFields,
selectValue: selectValue[fieldName],
topLevelFields,
versions,
})) {
result.push(
stripVirtualPathToCurrentCollection({ fields: topLevelFields, path: item, versions }),
)
}
}
}
}
return result
}
export const sanitizeSelect = ({
fields,
forceSelect,
select,
versions,
}: {
fields: FlattenedField[]
forceSelect?: SelectType
select?: SelectType
versions?: boolean
}): SelectType | undefined => {
if (!forceSelect || !select) {
if (!select) {
return select
}
@@ -21,5 +133,36 @@ export const sanitizeSelect = ({
return select
}
return deepMergeSimple(select, forceSelect)
if (forceSelect) {
select = deepMergeSimple(select, forceSelect)
}
if (select) {
const virtualRelations = resolveVirtualRelationsToSelect({
fields,
selectValue: select as SelectIncludeType,
topLevelFields: fields,
versions: versions ?? false,
})
for (const path of virtualRelations) {
let currentRef = select
const segments = path.split('.')
for (let i = 0; i < segments.length; i++) {
const isLast = segments.length - 1 === i
const segment = segments[i]
if (isLast) {
currentRef[segment] = true
} else {
if (!(segment in currentRef)) {
currentRef[segment] = {}
currentRef = currentRef[segment]
}
}
}
}
}
return select
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-import-export",
"version": "3.36.0",
"version": "3.36.1",
"description": "Import-Export plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-multi-tenant",
"version": "3.36.0",
"version": "3.36.1",
"description": "Multi Tenant plugin for Payload",
"keywords": [
"payload",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.36.0",
"version": "3.36.1",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.36.0",
"version": "3.36.1",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -42,6 +42,7 @@ const RichTextComponent: React.FC<
required,
},
path: pathFromProps,
potentiallyStalePath,
readOnly: readOnlyFromTopLevelProps,
validate, // Users can pass in client side validation if they WANT to, but it's not required anymore
} = props
@@ -73,7 +74,8 @@ const RichTextComponent: React.FC<
showError,
value,
} = useField<SerializedEditorState>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.36.0",
"version": "3.36.1",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -61,6 +61,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
leaves,
path: pathFromProps,
plugins,
potentiallyStalePath,
readOnly: readOnlyFromTopLevelProps,
schemaPath: schemaPathFromProps,
validate = richTextValidate,
@@ -101,7 +102,8 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
showError,
value,
} = useField({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.36.0",
"version": "3.36.1",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
"version": "3.36.0",
"version": "3.36.1",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
"version": "3.36.0",
"version": "3.36.1",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
"version": "3.36.0",
"version": "3.36.1",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
"version": "3.36.0",
"version": "3.36.1",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.36.0",
"version": "3.36.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/ui",
"version": "3.36.0",
"version": "3.36.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -48,6 +48,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
forceRender = false,
path: pathFromProps,
permissions,
potentiallyStalePath,
readOnly,
schemaPath: schemaPathFromProps,
validate,
@@ -120,7 +121,8 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
value,
} = useField<number>({
hasRows: true,
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -50,6 +50,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
},
path: pathFromProps,
permissions,
potentiallyStalePath,
readOnly,
schemaPath: schemaPathFromProps,
validate,
@@ -108,7 +109,8 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
value,
} = useField<number>({
hasRows: true,
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -40,6 +40,7 @@ const CheckboxFieldComponent: CheckboxFieldClientComponent = (props) => {
onChange: onChangeFromProps,
partialChecked,
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -66,7 +67,8 @@ const CheckboxFieldComponent: CheckboxFieldClientComponent = (props) => {
value,
} = useField({
disableFormData,
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -32,6 +32,7 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => {
},
onMount,
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -53,7 +54,8 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => {
showError,
value,
} = useField({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -34,6 +34,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
timezone,
},
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -64,7 +65,8 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
showError,
value,
} = useField<string>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -34,6 +34,7 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => {
required,
} = {} as EmailFieldClientProps['field'],
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -57,7 +58,8 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => {
showError,
value,
} = useField({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -13,10 +13,16 @@ import { withCondition } from '../../forms/withCondition/index.js'
* For example, this sets the `ìd` property of a block in the Blocks field.
*/
const HiddenFieldComponent: React.FC<HiddenFieldProps> = (props) => {
const { disableModifyingForm = true, path: pathFromProps, value: valueFromProps } = props
const {
disableModifyingForm = true,
path: pathFromProps,
potentiallyStalePath,
value: valueFromProps,
} = props
const { path, setValue, value } = useField({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
})
useEffect(() => {

View File

@@ -29,6 +29,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
required,
},
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -55,7 +56,8 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
showError,
value,
} = useField<string>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -132,6 +132,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
required,
},
path: pathFromProps,
potentiallyStalePath,
} = props
const { id: docID, docConfig } = useDocumentInfo()
@@ -144,7 +145,8 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
showError,
value,
} = useField<PaginatedDocs>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
})
const filterOptions: null | Where = useMemo(() => {

View File

@@ -39,6 +39,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
},
onChange: onChangeFromProps,
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -62,7 +63,8 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
showError,
value,
} = useField<number | number[]>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -27,6 +27,7 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => {
required,
},
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -50,7 +51,8 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => {
showError,
value = [null, null],
} = useField<[number, number]>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -35,6 +35,7 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => {
} = {} as RadioFieldClientProps['field'],
onChange: onChangeFromProps,
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
value: valueFromProps,
@@ -59,7 +60,8 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => {
showError,
value: valueFromContext,
} = useField<string>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -65,6 +65,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
required,
},
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -117,7 +118,8 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
showError,
value,
} = useField<Value | Value[]>({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

View File

@@ -47,6 +47,7 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
},
onChange: onChangeFromProps,
path: pathFromProps,
potentiallyStalePath,
readOnly,
validate,
} = props
@@ -70,7 +71,8 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
showError,
value,
} = useField({
potentiallyStalePath: pathFromProps,
path: pathFromProps,
potentiallyStalePath,
validate: memoizedValidate,
})

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