Compare commits

..

1 Commits

Author SHA1 Message Date
Jarrod Flesch
2b9a2a4a0c chore: adds failing int test 2025-02-25 15:47:29 -05:00
502 changed files with 2780 additions and 6170 deletions

View File

@@ -4,7 +4,7 @@ Depending on the quality of reproduction steps, this issue may be closed if no r
### Why was this issue marked with the `invalid-reproduction` label?
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@latest -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.

View File

@@ -158,7 +158,6 @@ object with:
- `docs` an array of related documents or only IDs if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
```json
{
@@ -172,8 +171,7 @@ object with:
}
// { ... }
],
"hasNextPage": false,
"totalDocs": 10, // if count: true is passed
"hasNextPage": false
}
// other fields...
}
@@ -186,7 +184,6 @@ object with:
- `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents
- `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query
```json
{
@@ -203,8 +200,7 @@ object with:
}
// { ... }
],
"hasNextPage": false,
"totalDocs": 10, // if count: true is passed
"hasNextPage": false
}
// other fields...
}
@@ -219,11 +215,10 @@ 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. Will be merged with the field `where` object. |
| **`sort`** | A string used to order related results |
| **`count`** | Whether include the count of related documents or not. Not included by default |
These can be applied to the local API, GraphQL, and REST API.

View File

@@ -44,31 +44,3 @@ const createdJob = await payload.jobs.queue({
},
})
```
#### Cancelling Jobs
Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks.
##### Cancel a Single Job
To cancel a specific job, use the `payload.jobs.cancelByID` method with the job's ID:
```ts
await payload.jobs.cancelByID({
id: createdJob.id,
})
```
##### Cancel Multiple Jobs
To cancel multiple jobs at once, use the `payload.jobs.cancel` method with a `Where` query:
```ts
await payload.jobs.cancel({
where: {
workflowSlug: {
equals: 'createPost',
},
},
})
```

View File

@@ -1113,57 +1113,6 @@ plugins: [
If you have custom features for `@payloadcms/richtext-lexical` you will need to migrate your code to the new API. Read more about the new API in the [documentation](https://payloadcms.com/docs/rich-text/building-custom-features).
## Reserved Field names
Payload reserves certain field names for internal use. Using any of the following names in your collections or globals will result in those fields being sanitized from the config, which can cause deployment errors. Ensure that any conflicting fields are renamed before migrating.
### General Reserved Names
- `file`
- `_id` (MongoDB only)
- `__v` (MongoDB only)
**Important Note**: It is recommended to avoid using field names with an underscore (`_`) prefix unless explicitly required by a plugin. Payload uses this prefix for internal columns, which can lead to conflicts in certain SQL conditions. The following are examples of reserved internal columns (this list is not exhaustive and other internal fields may also apply):
- `_order`
- `_path`
- `_uuid`
- `_parent_id`
- `_locale`
### Auth-Related Reserved Names
These are restricted if your collection uses `auth: true` and does not have `disableAuthStrategy: true`:
- `salt`
- `hash`
- `apiKey` (when `auth.useAPIKey: true` is enabled)
- `useAPIKey` (when `auth.useAPIKey: true` is enabled)
- `resetPasswordToken`
- `resetPasswordExpiration`
- `password`
- `email`
- `username`
### Upload-Related Reserved Names
These apply if your collection has `upload: true` configured:
- `filename`
- `mimetype`
- `filesize`
- `width`
- `height`
- `focalX`
- `focalY`
- `url`
- `thumbnailURL`
If `imageSizes` is configured, the following are also reserved:
- `sizes`
If any of these names are found in your collection / global fields, update them before migrating to avoid unexpected issues.
## Upgrade from previous beta
Reference this [community-made site](https://payload-releases-filter.vercel.app/?version=3&from=152429656&to=188243150&sort=asc&breaking=on). Set your version, sort by oldest first, enable breaking changes only.

View File

@@ -6,7 +6,7 @@ desc: Starting to build your own plugin? Find everything you need and learn best
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
<Banner type="success">
To use the template, run `npx create-payload-app@latest --template plugin` directly in
@@ -19,7 +19,7 @@ Our plugin template includes everything you need to build a full life-cycle plug
- A local dev environment to develop the plugin
- Test suite with integrated GitHub workflow
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
By abstracting your code into a plugin, you&apos;ll be able to reuse your feature across multiple projects and make it available for other developers to use.
## Plugins Recap
@@ -75,7 +75,7 @@ The purpose of the **dev** folder is to provide a sanitized local Payload projec
Do **not** store any of the plugin functionality in this folder - it is purely an environment to _assist_ you with developing the plugin.
If you're starting from scratch, you can easily setup a dev environment like this:
If you&apos;re starting from scratch, you can easily setup a dev environment like this:
```
mkdir dev
@@ -83,7 +83,7 @@ cd dev
npx create-payload-app@latest
```
If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
If you&apos;re using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`.
```
plugins: [
@@ -96,7 +96,7 @@ If you're using the plugin template, the dev folder is built out for you and the
You can add to the `dev/payload.config.ts` and build out the dev project as needed to test your plugin.
When you're ready to start development, navigate into this folder with `cd dev`
When you&apos;re ready to start development, navigate into this folder with `cd dev`
And then start the project with `pnpm dev` and pull up `http://localhost:3000` in your browser.
@@ -108,7 +108,7 @@ A good test suite is essential to ensure quality and stability in your plugin. P
Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the [Jest documentation.](https://jestjs.io/)
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set!
The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you&apos;re all set!
```
let payload: Payload
@@ -160,7 +160,7 @@ export const seed = async (payload: Payload): Promise<void> => {
## Building a Plugin
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
Now that we have our environment setup and dev project ready to go - it&apos;s time to build the plugin!
```
@@ -217,7 +217,7 @@ To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend
We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly, else this can cause adverse behavior and conflicts with Payload Config and other plugins.
Let's say you want to build a plugin that adds a new collection:
Let&apos;s say you want to build a plugin that adds a new collection:
```
config.collections = [
@@ -227,7 +227,7 @@ config.collections = [
]
```
First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
First, you need to spread the `config.collections` to ensure that we don&apos;t lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
This same logic is applied to other array and object like properties such as admin, globals and hooks:
@@ -284,7 +284,7 @@ For a better user experience, provide a way to disable the plugin without uninst
### Include tests in your GitHub CI workflow
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
If you&apos;ve configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
### Publish your finished plugin to npm

View File

@@ -52,7 +52,7 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
@@ -158,16 +158,6 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
rowFields?: never
tenantFieldAccess?: never
}
/**
* Customize tenant selector label
*
* Either a string or an object where the keys are locales and the values are the string labels
*/
tenantSelectorLabel?:
| Partial<{
[key in AcceptedLanguages]?: string
}>
| string
/**
* The slug for the tenant collection
*
@@ -186,14 +176,6 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter users by selected tenant
*/
useUsersTenantFilter?: boolean
}
```

View File

@@ -10,7 +10,7 @@ Lexical saves data in JSON - this is great for storage and flexibility and allow
## Lexical => JSX
For React-based frontends, converting Lexical content to JSX is the recommended rendering approach. Import the RichText component from @payloadcms/richtext-lexical/react and pass the Lexical content to it:
If your frontend uses React, converting Lexical to JSX is the recommended way to render rich text content. Import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the Lexical content to it:
```tsx
import React from 'react'
@@ -24,130 +24,46 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
}
```
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop. In our [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) you have an example of how to use `converters` to render custom blocks, custom nodes and override existing converters.
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop.
In our website template [you have an example](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) of how to use `converters` to render custom blocks.
<Banner type="default">
When fetching data, ensure your `depth` setting is high enough to fully populate Lexical nodes such as uploads. The JSX converter requires fully populated data to work correctly.
The JSX converter expects the input data to be fully populated. When fetching data, ensure the `depth` setting is high enough, to ensure that lexical nodes such as uploads are populated.
</Banner>
### Converting Internal Links
### Converting Lexical Blocks to JSX
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document.
In order to convert lexical blocks or inline blocks to JSX, you will have to pass the converter for your block to the RichText component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
```tsx
import type { DefaultNodeTypes, SerializedLinkNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import React from 'react'
import {
type JSXConvertersFunction,
LinkJSXConverter,
RichText,
} from '@payloadcms/richtext-lexical/react'
import React from 'react'
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
const { relationTo, value } = linkNode.fields.doc!
if (typeof value !== 'object') {
throw new Error('Expected value to be an object')
}
const slug = value.slug
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
}
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
...LinkJSXConverter({ internalDocToHref }),
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Converting Lexical Blocks
To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your block to the `RichText` component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
import React from 'react'
// Extend the default node types with your custom blocks for full type safety
type NodeTypes = DefaultNodeTypes | SerializedBlockNode<MyInlineBlock | MyTextBlock>
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
// myTextBlock is the slug of the block
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
},
inlineBlocks: {
// Each key should match your inline block's slug
// myInlineBlock is the slug of the block
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Overriding Default JSX Converters
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
Example - overriding the upload node converter to use next/image:
```tsx
'use client'
import type { DefaultNodeTypes, SerializedUploadNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
import Image from 'next/image'
import React from 'react'
type NodeTypes = DefaultNodeTypes
// Custom upload converter component that uses next/image
const CustomUploadComponent: React.FC<{
node: SerializedUploadNode
}> = ({ node }) => {
if (node.relationTo === 'uploads') {
const uploadDoc = node.value
if (typeof uploadDoc !== 'object') {
return null
}
const { alt, height, url, width } = uploadDoc
return <Image alt={alt} height={height} src={url} width={width} />
}
return null
}
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
// Override the default upload converter
upload: ({ node }) => {
return <CustomUploadComponent node={node} />
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
export const MyComponent = ({ lexicalData }) => {
return (
<RichText
converters={jsxConverters}
data={lexicalData.lexicalWithBlocks as SerializedEditorState}
/>
)
}
```

View File

@@ -29,9 +29,9 @@ Using the BlocksFeature, you can add both inline blocks (= can be inserted into
### Example: Code Field Block with language picker
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. First, make sure to explicitly install `@payloadcms/ui` in your project.
This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. Make sure to manually install `@payloadcms/ui`first.
Field Config:
Field config:
```ts
import {
@@ -91,6 +91,7 @@ CodeComponent.tsx:
```tsx
'use client'
import type { CodeFieldClient, CodeFieldClientProps } from 'payload'
import { CodeField, useFormFields } from '@payloadcms/ui'
@@ -104,8 +105,6 @@ const languageKeyToMonacoLanguageMap = {
tsx: 'typescript',
}
type Language = keyof typeof languageKeyToMonacoLanguageMap
export const Code: React.FC<CodeFieldClientProps> = ({
autoComplete,
field,
@@ -119,10 +118,10 @@ export const Code: React.FC<CodeFieldClientProps> = ({
}) => {
const languageField = useFormFields(([fields]) => fields['language'])
const language: Language =
(languageField?.value as Language) || (languageField?.initialValue as Language) || 'ts'
const language: string =
(languageField?.value as string) || (languageField.initialValue as string) || 'typescript'
const label = languages[language]
const label = languages[language as keyof typeof languages]
const props: CodeFieldClient = useMemo<CodeFieldClient>(
() => ({
@@ -130,10 +129,9 @@ export const Code: React.FC<CodeFieldClientProps> = ({
type: 'code',
admin: {
...field.admin,
editorOptions: undefined,
label,
language: languageKeyToMonacoLanguageMap[language] || language,
},
label,
}),
[field, language, label],
)

View File

@@ -30,7 +30,6 @@ pnpm add @payloadcms/storage-vercel-blob
- Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs.
- Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
```ts
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
@@ -65,7 +64,6 @@ export default buildConfig({
| `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` |
| `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) |
| `token` | Vercel Blob storage read/write token | `''` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## S3 Storage
[`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3)
@@ -81,7 +79,6 @@ pnpm add @payloadcms/storage-s3
- Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs.
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
```ts
import { s3Storage } from '@payloadcms/storage-s3'
@@ -129,7 +126,6 @@ pnpm add @payloadcms/storage-azure
- Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website.
```ts
import { azureStorage } from '@payloadcms/storage-azure'
@@ -165,7 +161,6 @@ export default buildConfig({
| `baseURL` | Base URL for the Azure Blob storage account | |
| `connectionString` | Azure Blob storage connection string | |
| `containerName` | Azure Blob storage container name | |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
## Google Cloud Storage
[`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs)
@@ -180,7 +175,6 @@ pnpm add @payloadcms/storage-gcs
- Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
```ts
import { gcsStorage } from '@payloadcms/storage-gcs'
@@ -209,14 +203,13 @@ export default buildConfig({
### Configuration Options#gcs-configuration
| Option | Description | Default |
| --------------- | --------------------------------------------------------------------------------------------------- | --------- |
| `enabled` | Whether or not to enable the plugin | `true` |
| `collections` | Collections to apply the storage to | |
| `bucket` | The name of the bucket to use | |
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
| `acl` | Access control list for files that are uploaded | `Private` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
| Option | Description | Default |
| ------------- | --------------------------------------------------------------------------------------------------- | --------- |
| `enabled` | Whether or not to enable the plugin | `true` |
| `collections` | Collections to apply the storage to | |
| `bucket` | The name of the bucket to use | |
| `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | |
| `acl` | Access control list for files that are uploaded | `Private` |
## Uploadthing Storage
@@ -233,7 +226,6 @@ pnpm add @payloadcms/storage-uploadthing
- Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type.
- Get a token from Uploadthing and set it as `token` in the `options` object.
- `acl` is optional and defaults to `public-read`.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client.
```ts
export default buildConfig({
@@ -254,14 +246,13 @@ export default buildConfig({
### Configuration Options#uploadthing-configuration
| Option | Description | Default |
| ---------------- | ------------------------------------------------------------- | ------------- |
| `token` | Token from Uploadthing. Required. | |
| `acl` | Access control list for files that are uploaded | `public-read` |
| `logLevel` | Log level for Uploadthing | `info` |
| `fetch` | Custom fetch function | `fetch` |
| `defaultKeyType` | Default key type for file operations | `fileKey` |
| `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | |
| Option | Description | Default |
| ---------------- | ----------------------------------------------- | ------------- |
| `token` | Token from Uploadthing. Required. | |
| `acl` | Access control list for files that are uploaded | `public-read` |
| `logLevel` | Log level for Uploadthing | `info` |
| `fetch` | Custom fetch function | `fetch` |
| `defaultKeyType` | Default key type for file operations | `fileKey` |
## Custom Storage Adapters

View File

@@ -1,6 +1,7 @@
import type { CollectionAfterLoginHook } from 'payload'
import { mergeHeaders, generateCookie, getCookieExpiration } from 'payload'
import { mergeHeaders } from '@payloadcms/next/utilities'
import { generateCookie, getCookieExpiration } from 'payload'
export const setCookieBasedOnDomain: CollectionAfterLoginHook = async ({ req, user }) => {
const relatedOrg = await req.payload.find({

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.25.0",
"version": "3.24.0",
"private": true,
"type": "module",
"scripts": {
@@ -117,7 +117,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.2.0",
"@next/bundle-analyzer": "15.1.5",
"@payloadcms/db-postgres": "workspace:*",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
@@ -132,8 +132,8 @@
"@types/jest": "29.5.12",
"@types/minimist": "1.2.5",
"@types/node": "22.5.4",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@types/shelljs": "0.8.15",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
@@ -153,7 +153,7 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^10",
"next": "15.2.0",
"next": "15.1.5",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.50.0",
@@ -173,6 +173,10 @@
"turbo": "^2.3.3",
"typescript": "5.7.3"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
},
"packageManager": "pnpm@9.7.1",
"engines": {
"node": "^18.20.2 || >=20.9.0",

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import type { CreateOptions } from 'mongoose'
import type { Create } from 'payload'
import type { Create, Document } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -9,7 +9,7 @@ import { transform } from './utilities/transform.js'
export const create: Create = async function create(
this: MongooseAdapter,
{ collection, data, req, returning },
{ collection, data, req },
) {
const Model = this.collections[collection]
const options: CreateOptions = {
@@ -34,9 +34,6 @@ export const create: Create = async function create(
} catch (error) {
handleError({ collection, error, req })
}
if (returning === false) {
return null
}
doc = doc.toObject()

View File

@@ -8,7 +8,7 @@ import { transform } from './utilities/transform.js'
export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
{ slug, data, req, returning },
{ slug, data, req },
) {
const Model = this.globals
@@ -25,9 +25,6 @@ export const createGlobal: CreateGlobal = async function createGlobal(
}
let [result] = (await Model.create([data], options)) as any
if (returning === false) {
return null
}
result = result.toObject()

View File

@@ -16,7 +16,6 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
parent,
publishedLocale,
req,
returning,
snapshot,
updatedAt,
versionData,
@@ -76,10 +75,6 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
options,
)
if (returning === false) {
return null
}
doc = doc.toObject()
transform({

View File

@@ -16,7 +16,6 @@ export const createVersion: CreateVersion = async function createVersion(
parent,
publishedLocale,
req,
returning,
snapshot,
updatedAt,
versionData,
@@ -87,10 +86,6 @@ export const createVersion: CreateVersion = async function createVersion(
options,
)
if (returning === false) {
return null
}
doc = doc.toObject()
transform({

View File

@@ -1,4 +1,4 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { QueryOptions } from 'mongoose'
import type { DeleteOne } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -10,10 +10,10 @@ import { transform } from './utilities/transform.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter,
{ collection, req, returning, select, where },
{ collection, req, select, where },
) {
const Model = this.collections[collection]
const options: MongooseUpdateQueryOptions = {
const options: QueryOptions = {
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
@@ -29,11 +29,6 @@ export const deleteOne: DeleteOne = async function deleteOne(
where,
})
if (returning === false) {
await Model.deleteOne(query, options)?.lean()
return null
}
const doc = await Model.findOneAndDelete(query, options)?.lean()
if (!doc) {

View File

@@ -16,7 +16,6 @@ import type {
TypeWithVersion,
UpdateGlobalArgs,
UpdateGlobalVersionArgs,
UpdateManyArgs,
UpdateOneArgs,
UpdateVersionArgs,
} from 'payload'
@@ -54,7 +53,6 @@ import { commitTransaction } from './transactions/commitTransaction.js'
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
import { updateGlobal } from './updateGlobal.js'
import { updateGlobalVersion } from './updateGlobalVersion.js'
import { updateMany } from './updateMany.js'
import { updateOne } from './updateOne.js'
import { updateVersion } from './updateVersion.js'
import { upsert } from './upsert.js'
@@ -162,7 +160,6 @@ declare module 'payload' {
updateGlobalVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateGlobalVersionArgs<T>,
) => Promise<TypeWithVersion<T>>
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
updateVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
@@ -203,7 +200,6 @@ export function mongooseAdapter({
mongoMemoryServer,
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
updateMany,
url,
versions: {},
// DatabaseAdapter

View File

@@ -1,4 +1,4 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { QueryOptions } from 'mongoose'
import type { UpdateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -9,12 +9,12 @@ import { transform } from './utilities/transform.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter,
{ slug, data, options: optionsArgs = {}, req, returning, select },
{ slug, data, options: optionsArgs = {}, req, select },
) {
const Model = this.globals
const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
const options: MongooseUpdateQueryOptions = {
const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -28,11 +28,6 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
transform({ adapter: this, data, fields, globalSlug: slug, operation: 'write' })
if (returning === false) {
await Model.updateOne({ globalType: slug }, data, options)
return null
}
const result: any = await Model.findOneAndUpdate({ globalType: slug }, data, options)
transform({ adapter: this, data: result, fields, globalSlug: slug, operation: 'read' })

View File

@@ -1,4 +1,4 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { QueryOptions } from 'mongoose'
import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload'
@@ -17,7 +17,6 @@ export async function updateGlobalVersion<T extends TypeWithID>(
locale,
options: optionsArgs = {},
req,
returning,
select,
versionData,
where,
@@ -29,7 +28,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
const flattenedFields = buildVersionGlobalFields(this.payload.config, currentGlobal, true)
const options: MongooseUpdateQueryOptions = {
const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -50,11 +49,6 @@ export async function updateGlobalVersion<T extends TypeWithID>(
transform({ adapter: this, data: versionData, fields, operation: 'write' })
if (returning === false) {
await VersionModel.updateOne(query, versionData, options)
return null
}
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) {

View File

@@ -1,61 +0,0 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { UpdateMany } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js'
import { transform } from './utilities/transform.js'
export const updateMany: UpdateMany = async function updateMany(
this: MongooseAdapter,
{ collection, data, locale, options: optionsArgs = {}, req, returning, select, where },
) {
const Model = this.collections[collection]
const fields = this.payload.collections[collection].config.fields
const options: MongooseUpdateQueryOptions = {
...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
select,
}),
session: await getSession(this, req),
}
const query = await buildQuery({
adapter: this,
collectionSlug: collection,
fields: this.payload.collections[collection].config.flattenedFields,
locale,
where,
})
transform({ adapter: this, data, fields, operation: 'write' })
try {
await Model.updateMany(query, data, options)
} catch (error) {
handleError({ collection, error, req })
}
if (returning === false) {
return null
}
const result = await Model.find(query, {}, options)
transform({
adapter: this,
data: result,
fields,
operation: 'read',
})
return result
}

View File

@@ -1,4 +1,4 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { QueryOptions } from 'mongoose'
import type { UpdateOne } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -11,22 +11,12 @@ import { transform } from './utilities/transform.js'
export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter,
{
id,
collection,
data,
locale,
options: optionsArgs = {},
req,
returning,
select,
where: whereArg,
},
{ id, collection, data, locale, options: optionsArgs = {}, req, select, where: whereArg },
) {
const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection]
const fields = this.payload.collections[collection].config.fields
const options: MongooseUpdateQueryOptions = {
const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -51,12 +41,7 @@ export const updateOne: UpdateOne = async function updateOne(
transform({ adapter: this, data, fields, operation: 'write' })
try {
if (returning === false) {
await Model.updateOne(query, data, options)
return null
} else {
result = await Model.findOneAndUpdate(query, data, options)
}
result = await Model.findOneAndUpdate(query, data, options)
} catch (error) {
handleError({ collection, error, req })
}

View File

@@ -1,4 +1,4 @@
import type { MongooseUpdateQueryOptions } from 'mongoose'
import type { QueryOptions } from 'mongoose'
import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
@@ -11,7 +11,7 @@ import { transform } from './utilities/transform.js'
export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter,
{ id, collection, locale, options: optionsArgs = {}, req, returning, select, versionData, where },
{ id, collection, locale, options: optionsArgs = {}, req, select, versionData, where },
) {
const VersionModel = this.versions[collection]
const whereToUse = where || { id: { equals: id } }
@@ -26,7 +26,7 @@ export const updateVersion: UpdateVersion = async function updateVersion(
true,
)
const options: MongooseUpdateQueryOptions = {
const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
@@ -47,11 +47,6 @@ export const updateVersion: UpdateVersion = async function updateVersion(
transform({ adapter: this, data: versionData, fields, operation: 'write' })
if (returning === false) {
await VersionModel.updateOne(query, versionData, options)
return null
}
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) {

View File

@@ -4,16 +4,7 @@ import type { MongooseAdapter } from './index.js'
export const upsert: Upsert = async function upsert(
this: MongooseAdapter,
{ collection, data, locale, req, returning, select, where },
{ collection, data, locale, req, select, where },
) {
return this.updateOne({
collection,
data,
locale,
options: { upsert: true },
req,
returning,
select,
where,
})
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, select, where })
}

View File

@@ -78,7 +78,6 @@ export const buildJoinAggregation = async ({
}
const {
count = false,
limit: limitJoin = join.field.defaultLimit ?? 10,
page,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
@@ -122,28 +121,6 @@ export const buildJoinAggregation = async ({
const alias = `${as}.docs.${collectionSlug}`
aliases.push(alias)
const basePipeline = [
{
$addFields: {
relationTo: {
$literal: collectionSlug,
},
},
},
{
$match: {
$and: [
{
$expr: {
$eq: [`$${join.field.on}`, '$$root_id_'],
},
},
$match,
],
},
},
]
aggregate.push({
$lookup: {
as: alias,
@@ -152,7 +129,25 @@ export const buildJoinAggregation = async ({
root_id_: '$_id',
},
pipeline: [
...basePipeline,
{
$addFields: {
relationTo: {
$literal: collectionSlug,
},
},
},
{
$match: {
$and: [
{
$expr: {
$eq: [`$${join.field.on}`, '$$root_id_'],
},
},
$match,
],
},
},
{
$sort: {
[sortProperty]: sortDirection,
@@ -174,24 +169,6 @@ export const buildJoinAggregation = async ({
],
},
})
if (count) {
aggregate.push({
$lookup: {
as: `${as}.totalDocs.${alias}`,
from: adapter.collections[collectionSlug].collection.name,
let: {
root_id_: '$_id',
},
pipeline: [
...basePipeline,
{
$count: 'result',
},
],
},
})
}
}
aggregate.push({
@@ -202,23 +179,6 @@ export const buildJoinAggregation = async ({
},
})
if (count) {
aggregate.push({
$addFields: {
[`${as}.totalDocs`]: {
$add: aliases.map((alias) => ({
$ifNull: [
{
$first: `$${as}.totalDocs.${alias}.result`,
},
0,
],
})),
},
},
})
}
aggregate.push({
$set: {
[`${as}.docs`]: {
@@ -235,17 +195,17 @@ export const buildJoinAggregation = async ({
const sliceValue = page ? [(page - 1) * limitJoin, limitJoin] : [limitJoin]
aggregate.push({
$addFields: {
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
$set: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, ...sliceValue],
},
},
})
aggregate.push({
$set: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, ...sliceValue],
$addFields: {
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
},
},
})
@@ -262,7 +222,6 @@ export const buildJoinAggregation = async ({
}
const {
count,
limit: limitJoin = join.field.defaultLimit ?? 10,
page,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
@@ -315,31 +274,6 @@ export const buildJoinAggregation = async ({
polymorphicSuffix = '.value'
}
const addTotalDocsAggregation = (as: string, foreignField: string) =>
aggregate.push(
{
$lookup: {
as: `${as}.totalDocs`,
foreignField,
from: adapter.collections[slug].collection.name,
localField: versions ? 'parent' : '_id',
pipeline: [
{
$match,
},
{
$count: 'result',
},
],
},
},
{
$addFields: {
[`${as}.totalDocs`]: { $ifNull: [{ $first: `$${as}.totalDocs.result` }, 0] },
},
},
)
if (adapter.payload.config.localization && locale === 'all') {
adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
@@ -370,7 +304,6 @@ export const buildJoinAggregation = async ({
},
},
)
if (limitJoin > 0) {
aggregate.push({
$addFields: {
@@ -380,10 +313,6 @@ export const buildJoinAggregation = async ({
},
})
}
if (count) {
addTotalDocsAggregation(as, `${join.field.on}${code}${polymorphicSuffix}`)
}
})
} else {
const localeSuffix =
@@ -430,11 +359,6 @@ export const buildJoinAggregation = async ({
},
},
)
if (count) {
addTotalDocsAggregation(as, foreignField)
}
if (limitJoin > 0) {
aggregate.push({
$addFields: {

View File

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

View File

@@ -33,7 +33,6 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
@@ -186,7 +185,6 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
upsert: updateOne,

View File

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

View File

@@ -34,7 +34,6 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
@@ -121,7 +120,6 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
tableNameMap: new Map<string, string>(),
tables: {},
transactionOptions: args.transactionOptions || undefined,
updateMany,
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter

View File

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

View File

@@ -33,7 +33,6 @@ import {
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
@@ -187,7 +186,6 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
rollbackTransaction,
updateGlobal,
updateGlobalVersion,
updateMany,
updateOne,
updateVersion,
upsert: updateOne,

View File

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

View File

@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export const create: Create = async function create(
this: DrizzleAdapter,
{ collection: collectionSlug, data, req, select, returning },
{ collection: collectionSlug, data, req, select },
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
@@ -25,12 +25,7 @@ export const create: Create = async function create(
req,
select,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function createGlobal<T extends Record<string, unknown>>(
this: DrizzleAdapter,
{ slug, data, req, returning }: CreateGlobalArgs,
{ slug, data, req }: CreateGlobalArgs,
): Promise<T> {
const db = await getTransaction(this, req)
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
@@ -26,13 +26,8 @@ export async function createGlobal<T extends Record<string, unknown>>(
operation: 'create',
req,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
result.globalType = slug
return result

View File

@@ -21,7 +21,6 @@ export async function createGlobalVersion<T extends TypeWithID>(
snapshot,
updatedAt,
versionData,
returning,
}: CreateGlobalVersionArgs,
) {
const db = await getTransaction(this, req)
@@ -46,7 +45,6 @@ export async function createGlobalVersion<T extends TypeWithID>(
req,
select,
tableName,
ignoreResult: returning === false ? 'idOnly' : false,
})
const table = this.tables[tableName]
@@ -61,9 +59,5 @@ export async function createGlobalVersion<T extends TypeWithID>(
})
}
if (returning === false) {
return null
}
return result
}

View File

@@ -22,7 +22,6 @@ export async function createVersion<T extends TypeWithID>(
snapshot,
updatedAt,
versionData,
returning,
}: CreateVersionArgs<T>,
) {
const db = await getTransaction(this, req)
@@ -73,9 +72,5 @@ export async function createVersion<T extends TypeWithID>(
})
}
if (returning === false) {
return null
}
return result
}

View File

@@ -13,7 +13,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: DrizzleAdapter,
{ collection: collectionSlug, req, select, where: whereArg, returning },
{ collection: collectionSlug, req, select, where: whereArg },
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
@@ -59,16 +59,13 @@ export const deleteOne: DeleteOne = async function deleteOne(
docToDelete = await db.query[tableName].findFirst(findManyArgs)
}
const result =
returning === false
? null
: transform({
adapter: this,
config: this.payload.config,
data: docToDelete,
fields: collection.flattenedFields,
joinQuery: false,
})
const result = transform({
adapter: this,
config: this.payload.config,
data: docToDelete,
fields: collection.flattenedFields,
joinQuery: false,
})
await this.deleteWhere({
db,

View File

@@ -2,7 +2,7 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
import type { FlattenedField, JoinQuery, SelectMode, SelectType, Where } from 'payload'
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
import { and, asc, desc, eq, or, sql } from 'drizzle-orm'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
@@ -386,7 +386,6 @@ export const traverseFields = ({
}
const {
count: shouldCount = false,
limit: limitArg = field.defaultLimit ?? 10,
page,
sort = field.defaultSort,
@@ -481,13 +480,6 @@ export const traverseFields = ({
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias))
}
if (shouldCount) {
currentArgs.extras[`${columnName}_count`] = sql`${db
.select({ count: count() })
.from(sql`${currentQuery.as(subQueryAlias)}`)
.where(sqlWhere)}`.as(`${columnName}_count`)
}
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`)) as SQLSelect
if (page && limit !== 0) {
@@ -619,20 +611,6 @@ export const traverseFields = ({
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
}).as(subQueryAlias)
if (shouldCount) {
currentArgs.extras[`${columnName}_count`] = sql`${db
.select({
count: count(),
})
.from(
sql`${db
.select(selectFields as any)
.from(newAliasTable)
.where(subQueryWhere)
.as(`${subQueryAlias}_count_subquery`)}`,
)}`.as(`${subQueryAlias}_count`)
}
currentArgs.extras[columnName] = sql`${db
.select({
result: jsonAggBuildObject(adapter, {

View File

@@ -31,10 +31,9 @@ export { buildRawSchema } from './schema/buildRawSchema.js'
export { beginTransaction } from './transactions/beginTransaction.js'
export { commitTransaction } from './transactions/commitTransaction.js'
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
export { updateOne } from './update.js'
export { updateGlobal } from './updateGlobal.js'
export { updateGlobalVersion } from './updateGlobalVersion.js'
export { updateMany } from './updateMany.js'
export { updateOne } from './updateOne.js'
export { updateVersion } from './updateVersion.js'
export { upsertRow } from './upsertRow/index.js'
export { buildCreateMigration } from './utilities/buildCreateMigration.js'

View File

@@ -1,7 +1,6 @@
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
@@ -399,7 +398,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
if (field.type === 'join') {
const { count, limit = field.defaultLimit ?? 10 } =
const { limit = field.defaultLimit ?? 10 } =
joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
// raw hasMany results from SQLite
@@ -408,8 +407,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
let fieldResult:
| { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }
| Record<string, { docs: unknown[]; hasNextPage: boolean; totalDocs?: number }>
| { docs: unknown[]; hasNextPage: boolean }
| Record<string, { docs: unknown[]; hasNextPage: boolean }>
if (Array.isArray(fieldData)) {
if (isLocalized && adapter.payload.config.localization) {
fieldResult = fieldData.reduce(
@@ -450,17 +449,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
}
if (count) {
const countPath = `${fieldName}_count`
if (typeof table[countPath] !== 'undefined') {
let value = Number(table[countPath])
if (Number.isNaN(value)) {
value = 0
}
fieldResult.totalDocs = value
}
}
result[field.name] = fieldResult
return result
}
@@ -619,7 +607,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.flattenedFields,
joinQuery,
numbers,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${sanitizedPath}${field.name}`,

View File

@@ -1,10 +1,10 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { UpdateOne } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
@@ -12,17 +12,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export const updateOne: UpdateOne = async function updateOne(
this: DrizzleAdapter,
{
id,
collection: collectionSlug,
data,
joins: joinQuery,
locale,
req,
select,
where: whereArg,
returning,
},
{ id, collection: collectionSlug, data, joins: joinQuery, locale, req, select, where: whereArg },
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
@@ -38,7 +28,6 @@ export const updateOne: UpdateOne = async function updateOne(
where: whereToUse,
})
// selectDistinct will only return if there are joins
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: [{ args: [1], method: 'limit' }],
@@ -51,18 +40,22 @@ export const updateOne: UpdateOne = async function updateOne(
if (selectDistinctResult?.[0]?.id) {
idToUpdate = selectDistinctResult?.[0]?.id
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
} else if (whereArg && !joins.length) {
const table = this.tables[tableName]
const findManyArgs = buildFindManyArgs({
adapter: this,
depth: 0,
fields: collection.flattenedFields,
joinQuery: false,
select: {},
tableName,
})
const docsToUpdate = await (db as LibSQLDatabase)
.select({
id: table.id,
})
.from(table)
.where(where)
.limit(1)
idToUpdate = docsToUpdate?.[0]?.id
findManyArgs.where = where
const docToUpdate = await db.query[tableName].findFirst(findManyArgs)
idToUpdate = docToUpdate?.id
}
const result = await upsertRow({
@@ -76,12 +69,7 @@ export const updateOne: UpdateOne = async function updateOne(
req,
select,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function updateGlobal<T extends Record<string, unknown>>(
this: DrizzleAdapter,
{ slug, data, req, select, returning }: UpdateGlobalArgs,
{ slug, data, req, select }: UpdateGlobalArgs,
): Promise<T> {
const db = await getTransaction(this, req)
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
@@ -26,13 +26,8 @@ export async function updateGlobal<T extends Record<string, unknown>>(
req,
select,
tableName,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
result.globalType = slug
return result

View File

@@ -16,16 +16,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function updateGlobalVersion<T extends TypeWithID>(
this: DrizzleAdapter,
{
id,
global,
locale,
req,
select,
versionData,
where: whereArg,
returning,
}: UpdateGlobalVersionArgs<T>,
{ id, global, locale, req, select, versionData, where: whereArg }: UpdateGlobalVersionArgs<T>,
) {
const db = await getTransaction(this, req)
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
@@ -58,12 +49,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
select,
tableName,
where,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -1,97 +0,0 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { UpdateMany } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from './types.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
import { getTransaction } from './utilities/getTransaction.js'
export const updateMany: UpdateMany = async function updateMany(
this: DrizzleAdapter,
{
collection: collectionSlug,
data,
joins: joinQuery,
locale,
req,
returning,
select,
where: whereToUse,
},
) {
const db = await getTransaction(this, req)
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const { joins, selectFields, where } = buildQuery({
adapter: this,
fields: collection.flattenedFields,
locale,
tableName,
where: whereToUse,
})
let idsToUpdate: (number | string)[] = []
const selectDistinctResult = await selectDistinct({
adapter: this,
db,
joins,
selectFields,
tableName,
where,
})
if (selectDistinctResult?.[0]?.id) {
idsToUpdate = selectDistinctResult?.map((doc) => doc.id)
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
} else if (whereToUse && !joins.length) {
const _db = db as LibSQLDatabase
const table = this.tables[tableName]
const docsToUpdate = await _db
.select({
id: table.id,
})
.from(table)
.where(where)
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
}
if (!idsToUpdate.length) {
return []
}
const results = []
// TODO: We need to batch this to reduce the amount of db calls. This can get very slow if we are updating a lot of rows.
for (const idToUpdate of idsToUpdate) {
const result = await upsertRow({
id: idToUpdate,
adapter: this,
data,
db,
fields: collection.flattenedFields,
ignoreResult: returning === false,
joinQuery,
operation: 'update',
req,
select,
tableName,
})
results.push(result)
}
if (returning === false) {
return null
}
return results
}

View File

@@ -16,16 +16,7 @@ import { getTransaction } from './utilities/getTransaction.js'
export async function updateVersion<T extends TypeWithID>(
this: DrizzleAdapter,
{
id,
collection,
locale,
req,
select,
versionData,
where: whereArg,
returning,
}: UpdateVersionArgs<T>,
{ id, collection, locale, req, select, versionData, where: whereArg }: UpdateVersionArgs<T>,
) {
const db = await getTransaction(this, req)
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
@@ -56,12 +47,7 @@ export async function updateVersion<T extends TypeWithID>(
select,
tableName,
where,
ignoreResult: returning === false,
})
if (returning === false) {
return null
}
return result
}

View File

@@ -428,10 +428,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
}
}
if (ignoreResult === 'idOnly') {
return { id: insertedRow.id } as T
}
if (ignoreResult) {
return data as T
}

View File

@@ -12,7 +12,7 @@ type BaseArgs = {
* When true, skips reading the data back from the database and returns the input data
* @default false
*/
ignoreResult?: boolean | 'idOnly'
ignoreResult?: boolean
joinQuery?: JoinQuery
path?: string
req?: Partial<PayloadRequest>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.25.0",
"version": "3.24.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
@@ -45,8 +45,8 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.25.0",
"version": "3.24.0",
"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.25.0",
"version": "3.24.0",
"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.25.0",
"version": "3.24.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -107,8 +107,8 @@
"@next/eslint-plugin-next": "15.1.5",
"@payloadcms/eslint-config": "workspace:*",
"@types/busboy": "1.5.4",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1",
"@types/uuid": "10.0.0",
"babel-plugin-react-compiler": "19.0.0-beta-714736e-20250131",
"esbuild": "0.24.2",

View File

@@ -44,26 +44,23 @@ export const DefaultNavClient: React.FC<{
id = `nav-global-${slug}`
}
const LinkElement = Link || 'a'
const activeCollection =
pathname.startsWith(href) && ['/', undefined].includes(pathname[href.length])
const Label = (
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
)
if (activeCollection) {
return (
<div className={`${baseClass}__link active`} id={id} key={i}>
<div className={`${baseClass}__link-indicator`} />
{Label}
</div>
)
}
return (
<Link className={`${baseClass}__link`} href={href} id={id} key={i} prefetch={false}>
{Label}
</Link>
<LinkElement
className={[`${baseClass}__link`, activeCollection && `active`]
.filter(Boolean)
.join(' ')}
href={href}
id={id}
key={i}
prefetch={Link ? false : undefined}
>
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
</LinkElement>
)
})}
</NavGroup>

View File

@@ -93,32 +93,36 @@
}
}
&__link {
display: flex;
align-items: center;
position: relative;
padding-block: base(0.125);
padding-inline-start: 0;
padding-inline-end: base(1.5);
text-decoration: none;
nav {
a {
position: relative;
padding-block: base(0.125);
padding-inline-start: 0;
padding-inline-end: base(1.5);
display: flex;
text-decoration: none;
&:focus:not(:focus-visible) {
box-shadow: none;
font-weight: 600;
}
&:focus:not(:focus-visible) {
box-shadow: none;
font-weight: 600;
}
&.active {
font-weight: normal;
padding-left: 0;
font-weight: 600;
&:hover,
&:focus-visible {
text-decoration: underline;
}
&.active {
font-weight: normal;
padding-left: 0;
font-weight: 600;
}
}
}
a.nav__link {
&:hover,
&:focus-visible {
text-decoration: underline;
}
&__link {
display: flex;
align-items: center;
}
&__link-indicator {
@@ -144,7 +148,7 @@
padding: var(--app-header-height) var(--gutter-h) base(2);
}
&__link {
nav a {
font-size: base(0.875);
line-height: base(1.5);
}

View File

@@ -4,7 +4,8 @@ import type { ImportMap, LanguageOptions, SanitizedConfig, ServerFunctionClient
import { rtlLanguages } from '@payloadcms/translations'
import { ProgressBar, RootProvider } from '@payloadcms/ui'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { cookies as nextCookies } from 'next/headers.js'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { getPayload, getRequestLanguage, parseCookies } from 'payload'
import React from 'react'
import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
@@ -33,16 +34,16 @@ export const RootLayout = async ({
}) => {
checkDependencies()
const {
const config = await configPromise
const headers = await getHeaders()
const cookies = parseCookies(headers)
const languageCode = getRequestLanguage({
config,
cookies,
headers,
languageCode,
permissions,
req,
req: {
payload: { config },
},
} = await initReq({ configPromise, importMap, key: 'RootLayout' })
})
const theme = getRequestTheme({
config,
@@ -50,6 +51,10 @@ export const RootLayout = async ({
headers,
})
const payload = await getPayload({ config, importMap })
const { permissions, req } = await initReq(config)
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
: 'LTR'
@@ -129,11 +134,11 @@ export const RootLayout = async ({
{Array.isArray(config.admin?.components?.providers) &&
config.admin?.components?.providers.length > 0 ? (
<NestProviders
importMap={req.payload.importMap}
importMap={payload.importMap}
providers={config.admin?.components?.providers}
serverProps={{
i18n: req.i18n,
payload: req.payload,
payload,
permissions,
user: req.user,
}}

View File

@@ -13,11 +13,7 @@ import { initReq } from './initReq.js'
export const handleServerFunctions: ServerFunctionHandler = async (args) => {
const { name: fnKey, args: fnArgs, config: configPromise, importMap } = args
const { req } = await initReq({
configPromise,
importMap,
key: 'RootLayout',
})
const { req } = await initReq(configPromise)
const augmentedArgs: Parameters<ServerFunction>[0] = {
...fnArgs,

View File

@@ -1,7 +1,8 @@
import type { InitPageResult, VisibleEntities } from 'payload'
import { headers as getHeaders } from 'next/headers.js'
import { notFound } from 'next/navigation.js'
import { isEntityHidden } from 'payload'
import { getPayload, isEntityHidden, parseCookies } from 'payload'
import * as qs from 'qs-esm'
import type { Args } from './types.js'
@@ -17,38 +18,31 @@ export const initPage = async ({
importMap,
route,
searchParams,
useLayoutReq,
}: Args): Promise<InitPageResult> => {
const headers = await getHeaders()
const payload = await getPayload({ config: configPromise, importMap })
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
const {
cookies,
locale,
permissions,
req,
req: { payload },
} = await initReq({
configPromise,
importMap,
key: useLayoutReq ? 'RootLayout' : 'initPage',
overrides: {
fallbackLocale: false,
req: {
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
},
urlSuffix: `${route}${searchParams ? queryString : ''}`,
},
})
const {
collections,
globals,
routes: { admin: adminRoute },
} = payload.config
const cookies = parseCookies(headers)
const { locale, permissions, req } = await initReq(payload.config, {
fallbackLocale: false,
req: {
headers,
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
},
})
const languageOptions = Object.entries(payload.config.i18n.supportedLanguages || {}).reduce(
(acc, [language, languageConfig]) => {
if (Object.keys(payload.config.i18n.supportedLanguages).includes(language)) {

View File

@@ -20,15 +20,4 @@ export type Args = {
* The search parameters of the current route provided to all pages in Next.js.
*/
searchParams: { [key: string]: string | string[] | undefined }
/**
* If `useLayoutReq` is `true`, this page will use the cached `req` created by the root layout
* instead of creating a new one.
*
* This improves performance for pages that are able to share the same `req` as the root layout,
* as permissions do not need to be re-calculated.
*
* If the page has unique query and url params that need to be part of the `req` object, or if you
* need permissions calculation to respect those you should not use this property.
*/
useLayoutReq?: boolean
}

View File

@@ -1,13 +1,5 @@
import type { AcceptedLanguages, I18n, I18nClient } from '@payloadcms/translations'
import type {
ImportMap,
Locale,
Payload,
PayloadRequest,
SanitizedConfig,
SanitizedPermissions,
User,
} from 'payload'
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { Locale, PayloadRequest, SanitizedConfig, SanitizedPermissions } from 'payload'
import { initI18n } from '@payloadcms/translations'
import { headers as getHeaders } from 'next/headers.js'
@@ -19,114 +11,78 @@ import {
getRequestLanguage,
parseCookies,
} from 'payload'
import { cache } from 'react'
import { getRequestLocale } from './getRequestLocale.js'
import { selectiveCache } from './selectiveCache.js'
type Result = {
cookies: Map<string, string>
headers: Awaited<ReturnType<typeof getHeaders>>
languageCode: AcceptedLanguages
locale?: Locale
permissions: SanitizedPermissions
req: PayloadRequest
}
type PartialResult = {
i18n: I18nClient
languageCode: AcceptedLanguages
payload: Payload
responseHeaders: Headers
user: null | User
}
export const initReq = cache(async function (
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
overrides?: Parameters<typeof createLocalReq>[0],
): Promise<Result> {
const config = await configPromise
const payload = await getPayload({ config })
// Create cache instances for different parts of our application
const partialReqCache = selectiveCache<PartialResult>('partialReq')
const reqCache = selectiveCache<Result>('req')
/**
* Initializes a full request object, including the `req` object and access control.
* As access control and getting the request locale is dependent on the current URL and
*/
export const initReq = async function ({
configPromise,
importMap,
key,
overrides,
}: {
configPromise: Promise<SanitizedConfig> | SanitizedConfig
importMap: ImportMap
key: string
overrides?: Parameters<typeof createLocalReq>[0]
}): Promise<Result> {
const headers = await getHeaders()
const cookies = parseCookies(headers)
const partialResult = await partialReqCache.get(async () => {
const config = await configPromise
const payload = await getPayload({ config, importMap })
const languageCode = getRequestLanguage({
config,
cookies,
headers,
})
const i18n: I18nClient = await initI18n({
config: config.i18n,
context: 'client',
language: languageCode,
})
const languageCode = getRequestLanguage({
config,
cookies,
headers,
})
const { responseHeaders, user } = await executeAuthStrategies({
headers,
payload,
})
const i18n: I18nClient = await initI18n({
config: config.i18n,
context: 'client',
language: languageCode,
})
return {
i18n,
languageCode,
payload,
responseHeaders,
user,
}
}, 'global')
/**
* Cannot simply call `payload.auth` here, as we need the user to get the locale, and we need the locale to get the access results
* I.e. the `payload.auth` function would call `getAccessResults` without a fully-formed `req` object
*/
const { responseHeaders, user } = await executeAuthStrategies({
headers,
payload,
})
return reqCache.get(async () => {
const { i18n, languageCode, payload, responseHeaders, user } = partialResult
const { req: reqOverrides, ...optionsOverrides } = overrides || {}
const { req: reqOverrides, ...optionsOverrides } = overrides || {}
const req = await createLocalReq(
{
req: {
headers,
host: headers.get('host'),
i18n: i18n as I18n,
responseHeaders,
user,
...(reqOverrides || {}),
},
...(optionsOverrides || {}),
const req = await createLocalReq(
{
req: {
headers,
host: headers.get('host'),
i18n: i18n as I18n,
responseHeaders,
url: `${payload.config.serverURL}`,
user,
...(reqOverrides || {}),
},
payload,
)
...(optionsOverrides || {}),
},
payload,
)
const locale = await getRequestLocale({
req,
})
const locale = await getRequestLocale({
req,
})
req.locale = locale?.code
req.locale = locale?.code
const permissions = await getAccessResults({
req,
})
const permissions = await getAccessResults({
req,
})
return {
cookies,
headers,
languageCode,
locale,
permissions,
req,
}
}, key)
}
return {
locale,
permissions,
req,
}
})

View File

@@ -1,57 +0,0 @@
import { cache } from 'react'
type CachedValue = object
// Module-scoped cache container that holds all cached, stable containers
// - these may hold the stable value, or a promise to the stable value
const globalCacheContainer: Record<
string,
<TValue extends object = CachedValue>(
...args: unknown[]
) => {
value: null | Promise<TValue> | TValue
}
> = {}
/**
* Creates a selective cache function that provides more control over React's request-level caching behavior.
*
* @param namespace - A namespace to group related cached values
* @returns A function that manages cached values within the specified namespace
*/
export function selectiveCache<TValue extends object = CachedValue>(namespace: string) {
// Create a stable namespace container if it doesn't exist
if (!globalCacheContainer[namespace]) {
globalCacheContainer[namespace] = cache((...args) => ({
value: null,
}))
}
/**
* Gets or creates a cached value for a specific key within the namespace
*
* @param key - The key to identify the cached value
* @param factory - A function that produces the value if not cached
* @returns The cached or newly created value
*/
const getCached = async (factory: () => Promise<TValue>, ...cacheArgs): Promise<TValue> => {
const stableObjectFn = globalCacheContainer[namespace]
const stableObject = stableObjectFn<TValue>(...cacheArgs)
if (
stableObject?.value &&
'then' in stableObject.value &&
typeof stableObject.value?.then === 'function'
) {
return await stableObject.value
}
stableObject.value = factory()
return await stableObject.value
}
return {
get: getCached,
}
}

View File

@@ -1,18 +1,18 @@
import type {
AdminViewServerProps,
ListPreferences,
ListQuery,
ListViewClientProps,
ListViewServerPropsOnly,
Where,
} from 'payload'
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import {
type AdminViewServerProps,
type ColumnPreference,
type ListPreferences,
type ListQuery,
type ListViewClientProps,
type ListViewServerPropsOnly,
type Where,
} from 'payload'
import { isNumber, transformColumnsToPreferences } from 'payload/shared'
import { isNumber } from 'payload/shared'
import React, { Fragment } from 'react'
import { renderListViewSlots } from './renderListViewSlots.js'
@@ -72,20 +72,10 @@ export const renderListView = async (
const query = queryFromArgs || queryFromReq
const columns: ColumnPreference[] = transformColumnsToPreferences(
query?.columns as ColumnPreference[] | string,
)
/**
* @todo: find a pattern to avoid setting preferences on hard navigation, i.e. direct links, page refresh, etc.
* This will ensure that prefs are only updated when explicitly set by the user
* This could potentially be done by injecting a `sessionID` into the params and comparing it against a session cookie
*/
const listPreferences = await upsertPreferences<ListPreferences>({
key: `${collectionSlug}-list`,
req,
value: {
columns,
limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
sort: query?.sort as string,
},
@@ -151,7 +141,6 @@ export const renderListView = async (
clientCollectionConfig,
collectionConfig,
columnPreferences: listPreferences?.columns,
columns,
customCellProps,
docs: data.docs,
drawerSlug,
@@ -214,11 +203,9 @@ export const renderListView = async (
<Fragment>
<HydrateAuthProvider permissions={permissions} />
<ListQueryProvider
columns={transformColumnsToPreferences(columnState)}
data={data}
defaultLimit={limit}
defaultSort={sort}
listPreferences={listPreferences}
modifySearchParams={!isInDrawer}
>
{RenderServerComponent({

View File

@@ -58,7 +58,6 @@ export const NotFoundPage = async ({
redirectUnauthenticatedUser: true,
route: formatAdminURL({ adminRoute, path: '/not-found' }),
searchParams,
useLayoutReq: true,
})
const params = await paramsPromise

View File

@@ -14,7 +14,7 @@ export { generateVerifyMetadata } from './meta.js'
export async function Verify({ initPageResult, params, searchParams }: AdminViewServerProps) {
// /:collectionSlug/verify/:token
const [collectionSlug, verify, token] = params.segments
const [collectionSlug, token] = params.segments
const { locale, permissions, req } = initPageResult
const {

View File

@@ -1,7 +1,6 @@
import type {
Document,
DocumentViewServerProps,
Locale,
OptionObject,
SanitizedCollectionPermission,
SanitizedGlobalPermission,
@@ -143,29 +142,26 @@ export async function VersionView(props: DocumentViewServerProps) {
}
}
let selectedLocales: OptionObject[] = []
const selectedLocales: OptionObject[] = []
if (localization) {
let locales: Locale[] = []
if (localeCodesFromParams) {
for (const code of localeCodesFromParams) {
const locale = localization.locales.find((locale) => locale.code === code)
if (!locale) {
continue
if (locale) {
selectedLocales.push({
label: locale.label,
value: locale.code,
})
}
locales.push(locale)
}
} else {
locales = localization.locales
for (const { code, label } of localization.locales) {
selectedLocales.push({
label,
value: code,
})
}
}
if (localization.filterAvailableLocales) {
locales = (await localization.filterAvailableLocales({ locales, req })) || []
}
selectedLocales = locales.map((locale) => ({
label: locale.label,
value: locale.code,
}))
}
const latestVersion =

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.25.0",
"version": "3.24.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -74,7 +74,7 @@
"build": "rimraf .dist && rimraf tsconfig.tsbuildinfo && pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:esbuild",
"build:esbuild": "echo skipping esbuild",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:types": "concurrently --group \"tsc --emitDeclarationOnly --outDir dist\" \"tsc-strict\"",
"clean": "rimraf -g {dist,*.tsbuildinfo}",
"clean:cache": "rimraf node_modules/.cache",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
@@ -124,13 +124,15 @@
"@types/pluralize": "0.0.33",
"@types/uuid": "10.0.0",
"@types/ws": "^8.5.10",
"concurrently": "9.1.2",
"copyfiles": "2.4.1",
"cross-env": "7.0.3",
"esbuild": "0.24.2",
"graphql-http": "^1.22.0",
"react-datepicker": "7.6.0",
"rimraf": "6.0.1",
"sharp": "0.32.6"
"sharp": "0.32.6",
"typescript-strict-plugin": "2.4.4"
},
"peerDependencies": {
"graphql": "^16.8.1"

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { GenericLanguages, I18n } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'

View File

@@ -1,3 +1,5 @@
// @ts-strict-ignore
import type { I18nClient } from '@payloadcms/translations'
import type { ClientField, Field, FieldTypes, Tab } from '../../fields/config/types.js'

View File

@@ -3,7 +3,6 @@ import type { SanitizedConfig } from '../../config/types.js'
import type { PaginatedDocs } from '../../database/types.js'
import type { CollectionSlug, ColumnPreference } from '../../index.js'
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
import type { ColumnsFromURL } from '../../utilities/transformColumnPreferences.js'
export type DefaultServerFunctionArgs = {
importMap: ImportMap
@@ -39,11 +38,6 @@ export type ServerFunctionHandler = (
) => Promise<unknown>
export type ListQuery = {
/*
* This is an of strings, i.e. `['title', '-slug']`
* Use `transformColumnsToPreferences` to convert it back and forth
*/
columns?: ColumnsFromURL
limit?: string
page?: string
/*

View File

@@ -42,14 +42,8 @@ export type ListViewClientProps = {
disableBulkEdit?: boolean
enableRowSelections?: boolean
hasCreatePermission: boolean
/**
* @deprecated
*/
listPreferences?: ListPreferences
newDocumentURL: string
/**
* @deprecated
*/
preferenceKey?: string
renderedFilters?: Map<string, React.ReactNode>
resolvedFilterOptions?: Map<string, ResolvedFilterOptions>

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { SanitizedCollectionConfig } from './../collections/config/types.js'
type CookieOptions = {

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import crypto from 'crypto'
const algorithm = 'aes-256-ctr'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type { PayloadHandler } from '../../config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { AllOperations, PayloadRequest } from '../types/index.js'
import type { Permissions, SanitizedPermissions } from './types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { CollectionConfig } from '../collections/config/types.js'
import type { Field, TabAsField } from '../fields/config/types.js'
import type { PayloadRequest } from '../types/index.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { Auth } from './types.js'
export const getLoginOptions = (

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import crypto from 'crypto'
import { status as httpStatus } from 'http-status'
import { URL } from 'url'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { CollectionSlug, Payload, RequestContext } from '../../../index.js'
import type { PayloadRequest } from '../../../types/index.js'
import type { Result } from '../forgotPassword.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type {
AuthOperationsFromCollectionSlug,
Collection,

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { decodeJwt } from 'jose'
import type { Collection } from '../../collections/config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import url from 'url'
import type { BeforeOperationHook, Collection } from '../../collections/config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { status as httpStatus } from 'http-status'
import type {

View File

@@ -48,7 +48,6 @@ export const verifyEmailOperation = async (args: Args): Promise<boolean> => {
_verified: true,
},
req,
returning: false,
})
if (shouldCommit) {

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { URL } from 'url'
import type { Collection } from '../collections/config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import crypto from 'crypto'
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { jwtVerify } from 'jose'
import type { Payload, Where } from '../../types/index.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import crypto from 'crypto'
import scmp from 'scmp'

View File

@@ -1,3 +1,5 @@
// @ts-strict-ignore
import type { AdminViewConfig } from '../../admin/views/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { AddToImportMap, Imports, InternalImportMap } from './index.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
/* eslint-disable @typescript-eslint/no-unused-expressions */
import type { PayloadComponent, SanitizedConfig } from '../../config/types.js'
import type { Block, Field, Tab } from '../../fields/config/types.js'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import type { PayloadComponent } from '../../config/types.js'
export function parsePayloadComponent(PayloadComponent: PayloadComponent): {

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
/* eslint-disable no-console */
import { Cron } from 'croner'
import minimist from 'minimist'

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import nextEnvImport from '@next/env'
import { findUpSync } from '../utilities/findUp.js'

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