Compare commits
2 Commits
docs/resou
...
perf/postg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16170b84f1 | ||
|
|
300cb0d7e7 |
2
.github/workflows/post-release-templates.yml
vendored
2
.github/workflows/post-release-templates.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
echo "DATABASE_URI=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.12.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
|
||||
|
||||
@@ -474,7 +474,7 @@ Field: '/path/to/CustomArrayManagerField',
|
||||
rows={[
|
||||
[
|
||||
{
|
||||
value: '**\\\`path\\\`**',
|
||||
value: '**\\`path\\`**',
|
||||
},
|
||||
{
|
||||
value: 'The path to the array or block field',
|
||||
@@ -482,7 +482,7 @@ Field: '/path/to/CustomArrayManagerField',
|
||||
],
|
||||
[
|
||||
{
|
||||
value: '**\\\`rowIndex\\\`**',
|
||||
value: '**\\`rowIndex\\`**',
|
||||
},
|
||||
{
|
||||
value: 'The index of the row to remove',
|
||||
@@ -561,7 +561,7 @@ Field: '/path/to/CustomArrayManagerField',
|
||||
rows={[
|
||||
[
|
||||
{
|
||||
value: '**\\\`path\\\`**',
|
||||
value: '**\\`path\\`**',
|
||||
},
|
||||
{
|
||||
value: 'The path to the array or block field',
|
||||
@@ -569,7 +569,7 @@ Field: '/path/to/CustomArrayManagerField',
|
||||
],
|
||||
[
|
||||
{
|
||||
value: '**\\\`rowIndex\\\`**',
|
||||
value: '**\\`rowIndex\\`**',
|
||||
},
|
||||
{
|
||||
value: 'The index of the row to replace',
|
||||
@@ -577,7 +577,7 @@ Field: '/path/to/CustomArrayManagerField',
|
||||
],
|
||||
[
|
||||
{
|
||||
value: '**\\\`data\\\`**',
|
||||
value: '**\\`data\\`**',
|
||||
},
|
||||
{
|
||||
value: 'The data to replace within the row',
|
||||
@@ -718,7 +718,7 @@ The `useDocumentInfo` hook provides information about the current document being
|
||||
| **`currentEditor`** | The user currently editing the document. |
|
||||
| **`docConfig`** | Either the Collection or Global config of the document, depending on what is being edited. |
|
||||
| **`docPermissions`** | The current document's permissions. Fallback to collection permissions when no id is present. |
|
||||
| **`documentIsLocked`** | Whether the document is currently locked by another user. [More details](./locked-documents). |
|
||||
| **`documentIsLocked`** | Whether the document is currently locked by another user. [More details](./locked-documents). |
|
||||
| **`getDocPermissions`** | Method to retrieve document-level permissions. |
|
||||
| **`getDocPreferences`** | Method to retrieve document-level user preferences. [More details](./preferences). |
|
||||
| **`globalSlug`** | The slug of the global if editing a global document. |
|
||||
@@ -730,7 +730,7 @@ The `useDocumentInfo` hook provides information about the current document being
|
||||
| **`initialData`** | The initial data of the document. |
|
||||
| **`isEditing`** | Whether the document is being edited (as opposed to created). |
|
||||
| **`isInitializing`** | Whether the document info is still initializing. |
|
||||
| **`isLocked`** | Whether the document is locked. [More details](./locked-documents). |
|
||||
| **`isLocked`** | Whether the document is locked. [More details](./locked-documents). |
|
||||
| **`lastUpdateTime`** | Timestamp of the last update to the document. |
|
||||
| **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. |
|
||||
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). |
|
||||
@@ -739,9 +739,9 @@ The `useDocumentInfo` hook provides information about the current document being
|
||||
| **`setDocumentTitle`** | Method to set the document title. |
|
||||
| **`setHasPublishedDoc`** | Method to update whether the document has been published. |
|
||||
| **`title`** | The title of the document. |
|
||||
| **`unlockDocument`** | Method to unlock a document. [More details](./locked-documents). |
|
||||
| **`unlockDocument`** | Method to unlock a document. [More details](./locked-documents). |
|
||||
| **`unpublishedVersionCount`** | The number of unpublished versions of the document. |
|
||||
| **`updateDocumentEditor`** | Method to update who is currently editing the document. [More details](./locked-documents). |
|
||||
| **`updateDocumentEditor`** | Method to update who is currently editing the document. [More details](./locked-documents). |
|
||||
| **`updateSavedDocumentData`** | Method to update the saved document data. |
|
||||
| **`uploadStatus`** | Status of any uploads in progress ('idle', 'uploading', or 'failed'). |
|
||||
| **`versionCount`** | The current version count of the document. |
|
||||
|
||||
@@ -60,31 +60,30 @@ export const Posts: CollectionConfig = {
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
| Option | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
|
||||
| `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](../access-control/collections). |
|
||||
| `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
| `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. |
|
||||
| `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. |
|
||||
| `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. |
|
||||
| `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](../rest-api/overview#custom-endpoints). |
|
||||
| `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). |
|
||||
| `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) |
|
||||
| `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). |
|
||||
| `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| `enableQueryPresets` | Enable query presets for this Collection. [More details](../query-presets/overview). |
|
||||
| `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](../upload/overview) documentation. |
|
||||
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
|
||||
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
|
||||
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
|
||||
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
@@ -178,7 +177,7 @@ The following options are available:
|
||||
```ts
|
||||
import type { CollectionCOnfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
export const MyCollection: CollectionCOnfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
|
||||
@@ -147,7 +147,7 @@ _\* Config location detection is different between development and production en
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:** Ensure your `tsconfig.json` is properly configured for Payload
|
||||
to auto-detect your config location. If it does not exist, or does not specify
|
||||
to auto-detect your config location. If if does not exist, or does not specify
|
||||
the proper `compilerOptions`, Payload will default to the current working
|
||||
directory.
|
||||
</Banner>
|
||||
@@ -239,7 +239,7 @@ export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
cors: {
|
||||
origins: ['http://localhost:3000'],
|
||||
origins: ['http://localhost:3000']
|
||||
headers: ['x-custom-header']
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
@@ -55,7 +55,7 @@ For more granular control, pass a configuration object instead. Payload exposes
|
||||
| `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
|
||||
| `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
|
||||
| `sensitive` | When true, will match if the path is case sensitive. |
|
||||
| `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](../admin/metadata). |
|
||||
| `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ desc:
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The List View is where users interact with a list of [Collection](../configuration/collections) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many.
|
||||
The List View is where users interact with a list of [Collection](../collections/overview) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many.
|
||||
|
||||
The List View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view.
|
||||
|
||||
<Banner type="info">
|
||||
**Note:** Only [Collections](../configuration/collections) have a List View.
|
||||
[Globals](../configuration/globals) do not have a List View as they are single
|
||||
**Note:** Only [Collections](../collections/overview) have a List View.
|
||||
[Globals](../globals/overview) do not have a List View as they are single
|
||||
documents.
|
||||
</Banner>
|
||||
|
||||
@@ -90,11 +90,11 @@ The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforelist). |
|
||||
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforelisttable). |
|
||||
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterlist). |
|
||||
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterlisttable). |
|
||||
| `Description` | A component to render a description of the Collection. [More details](#description). |
|
||||
| `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforeList). |
|
||||
| `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforeListTable). |
|
||||
| `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterList). |
|
||||
| `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterListTable). |
|
||||
| `Description` | A component to render a description of the Collection. [More details](#Description). |
|
||||
|
||||
### beforeList
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ powerful Admin UI.
|
||||
| **`name`** \* | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
|
||||
| **`collection`** \* | The `slug`s having the relationship field or an array of collection slugs. |
|
||||
| **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections |
|
||||
| **`orderable`** | If true, enables custom ordering and joined documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) for efficient reordering. |
|
||||
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
|
||||
@@ -28,7 +28,7 @@ Then, you could configure two different runner strategies:
|
||||
|
||||
As mentioned above, you can queue jobs, but the jobs won't run unless a worker picks up your jobs and runs them. This can be done in four ways:
|
||||
|
||||
### Cron jobs
|
||||
#### Cron jobs
|
||||
|
||||
You can use the `jobs.autoRun` property to configure cron jobs:
|
||||
|
||||
@@ -63,7 +63,7 @@ export default buildConfig({
|
||||
and should not be used on serverless platforms like Vercel.
|
||||
</Banner>
|
||||
|
||||
### Endpoint
|
||||
#### Endpoint
|
||||
|
||||
You can execute jobs by making a fetch request to the `/api/payload-jobs/run` endpoint:
|
||||
|
||||
@@ -130,7 +130,7 @@ This works because Vercel automatically makes the `CRON_SECRET` environment vari
|
||||
|
||||
After the project is deployed to Vercel, the Vercel Cron job will automatically trigger the `/api/payload-jobs/run` endpoint in the specified schedule, running the queued jobs in the background.
|
||||
|
||||
### Local API
|
||||
#### Local API
|
||||
|
||||
If you want to process jobs programmatically from your server-side code, you can use the Local API:
|
||||
|
||||
@@ -156,7 +156,7 @@ const results = await payload.jobs.runByID({
|
||||
})
|
||||
```
|
||||
|
||||
### Bin script
|
||||
#### Bin script
|
||||
|
||||
Finally, you can process jobs via the bin script that comes with Payload out of the box.
|
||||
|
||||
@@ -169,76 +169,3 @@ In addition, the bin script allows you to pass a `--cron` flag to the `jobs:run`
|
||||
```sh
|
||||
npx payload jobs:run --cron "*/5 * * * *"
|
||||
```
|
||||
|
||||
## Processing Order
|
||||
|
||||
By default, jobs are processed first in, first out (FIFO). This means that the first job added to the queue will be the first one processed. However, you can also configure the order in which jobs are processed.
|
||||
|
||||
### Jobs Configuration
|
||||
|
||||
You can configure the order in which jobs are processed in the jobs configuration by passing the `processingOrder` property. This mimics the Payload [sort](../queries/sort) property that's used for functionality such as `payload.find()`.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// your tasks here
|
||||
],
|
||||
processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also set this on a queue-by-queue basis:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// your tasks here
|
||||
],
|
||||
processingOrder: {
|
||||
default: 'createdAt', // FIFO
|
||||
queues: {
|
||||
nightly: '-createdAt', // LIFO
|
||||
myQueue: '-createdAt', // LIFO
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
If you need even more control over the processing order, you can pass a function that returns the processing order - this function will be called every time a queue starts processing jobs.
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
// Other configurations...
|
||||
jobs: {
|
||||
tasks: [
|
||||
// your tasks here
|
||||
],
|
||||
processingOrder: ({ queue }) => {
|
||||
if (queue === 'myQueue') {
|
||||
return '-createdAt' // LIFO
|
||||
}
|
||||
return 'createdAt' // FIFO
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Local API
|
||||
|
||||
You can configure the order in which jobs are processed in the `payload.jobs.queue` method by passing the `processingOrder` property.
|
||||
|
||||
```ts
|
||||
const createdJob = await payload.jobs.queue({
|
||||
workflow: 'createPostAndUpdate',
|
||||
input: {
|
||||
title: 'my title',
|
||||
},
|
||||
processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO
|
||||
})
|
||||
```
|
||||
|
||||
@@ -55,9 +55,18 @@ Because _**you**_ are in complete control of who can do what with your data, you
|
||||
wield that power responsibly before deploying to Production.
|
||||
|
||||
<Banner type="error">
|
||||
**By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.**
|
||||
**
|
||||
By default, all Access Control functions require that a user is successfully logged in to
|
||||
Payload to create, read, update, or delete data.
|
||||
**
|
||||
But, if you allow public user registration, for example, you will want to make sure that your
|
||||
access control functions are more strict - permitting
|
||||
|
||||
But, if you allow public user registration, for example, you will want to make sure that your access control functions are more strict - permitting **only appropriate users** to perform appropriate actions.
|
||||
**
|
||||
only appropriate users
|
||||
**
|
||||
|
||||
to perform appropriate actions.
|
||||
|
||||
</Banner>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
// Your richtext data here
|
||||
const data: SerializedEditorState = {}
|
||||
|
||||
const markdown = convertLexicalToMarkdown({
|
||||
const html = convertLexicalToMarkdown({
|
||||
data,
|
||||
editorConfig: await editorConfigFactory.default({
|
||||
config, // <= make sure you have access to your Payload Config
|
||||
@@ -101,7 +101,7 @@ import {
|
||||
editorConfigFactory,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
const lexicalJSON = convertMarkdownToLexical({
|
||||
const html = convertMarkdownToLexical({
|
||||
editorConfig: await editorConfigFactory.default({
|
||||
config, // <= make sure you have access to your Payload Config
|
||||
}),
|
||||
|
||||
@@ -409,7 +409,7 @@ Explore the APIs available through ClientFeature to add the specific functionali
|
||||
|
||||
### Adding a client feature to the server feature
|
||||
|
||||
Inside of your server feature, you can provide an [import path](/docs/custom-components/overview#component-paths) to the client feature like this:
|
||||
Inside of your server feature, you can provide an [import path](/docs/admin/custom-components/overview#component-paths) to the client feature like this:
|
||||
|
||||
```ts
|
||||
import { createServerFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -87,7 +87,6 @@
|
||||
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
|
||||
"script:gen-templates": "pnpm --filter scripts gen-templates",
|
||||
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
|
||||
"script:license-check": "pnpm --filter scripts license-check",
|
||||
"script:list-published": "pnpm --filter releaser list-published",
|
||||
"script:pack": "pnpm --filter scripts pack-all-to-dest",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -273,7 +273,6 @@ export function mongooseAdapter({
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'mongoose',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: 'text',
|
||||
init: adapter,
|
||||
|
||||
@@ -81,19 +81,7 @@ export async function parseParams({
|
||||
[searchParam.path]: searchParam.value,
|
||||
})
|
||||
} else {
|
||||
if (result[searchParam.path]) {
|
||||
if (!result.$and) {
|
||||
result.$and = []
|
||||
}
|
||||
|
||||
result.$and.push({ [searchParam.path]: result[searchParam.path] })
|
||||
result.$and.push({
|
||||
[searchParam.path]: searchParam.value,
|
||||
})
|
||||
delete result[searchParam.path]
|
||||
} else {
|
||||
result[searchParam.path] = searchParam.value
|
||||
}
|
||||
result[searchParam.path] = searchParam.value
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value ?? {}, {
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { BaseJob, UpdateJobs, Where } from 'payload'
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildSortParam } from './queries/buildSortParam.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
@@ -12,11 +11,8 @@ import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateJobs: UpdateJobs = async function updateMany(
|
||||
this: MongooseAdapter,
|
||||
{ id, data, limit, req, returning, sort: sortArg, where: whereArg },
|
||||
{ id, data, limit, req, returning, where: whereArg },
|
||||
) {
|
||||
if (!(data?.log as object[])?.length) {
|
||||
delete data.log
|
||||
}
|
||||
const where = id ? { id: { equals: id } } : (whereArg as Where)
|
||||
|
||||
const { collectionConfig, Model } = getCollection({
|
||||
@@ -24,14 +20,6 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
collectionSlug: 'payload-jobs',
|
||||
})
|
||||
|
||||
const sort: Record<string, unknown> | undefined = buildSortParam({
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
sort: sortArg || collectionConfig.defaultSort,
|
||||
timestamps: true,
|
||||
})
|
||||
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
lean: true,
|
||||
new: true,
|
||||
@@ -63,7 +51,7 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
const documentsToUpdate = await Model.find(
|
||||
query,
|
||||
{},
|
||||
{ ...options, limit, projection: { _id: 1 }, sort },
|
||||
{ ...options, limit, projection: { _id: 1 } },
|
||||
)
|
||||
if (documentsToUpdate.length === 0) {
|
||||
return null
|
||||
@@ -78,14 +66,7 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
return null
|
||||
}
|
||||
|
||||
result = await Model.find(
|
||||
query,
|
||||
{},
|
||||
{
|
||||
...options,
|
||||
sort,
|
||||
},
|
||||
)
|
||||
result = await Model.find(query, {}, options)
|
||||
}
|
||||
} catch (error) {
|
||||
handleError({ collection: collectionConfig.slug, error, req })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts",
|
||||
"default": "./src/exports/types-deprecated.ts"
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -102,9 +102,9 @@
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts",
|
||||
"default": "./dist/exports/types-deprecated.js"
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type {
|
||||
Args as _Args,
|
||||
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
|
||||
PostgresAdapter as _PostgresAdapter,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-postgres` instead
|
||||
*/
|
||||
export type Args = _Args
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-postgres` instead
|
||||
*/
|
||||
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-postgres` instead
|
||||
*/
|
||||
export type PostgresAdapter = _PostgresAdapter
|
||||
@@ -208,18 +208,12 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'postgres',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
Args as PostgresAdapterArgs,
|
||||
GeneratedDatabaseSchema,
|
||||
PostgresAdapter,
|
||||
} from './types.js'
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
|
||||
export { geometryColumn } from '@payloadcms/drizzle/postgres'
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"types": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"require": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts"
|
||||
"import": "./src/types.ts",
|
||||
"require": "./src/types.ts",
|
||||
"types": "./src/types.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -99,9 +99,9 @@
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"require": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts"
|
||||
"import": "./dist/types.js",
|
||||
"require": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SQLiteSelect } from 'drizzle-orm/sqlite-core'
|
||||
import type { ChainedMethods } from '@payloadcms/drizzle/types'
|
||||
|
||||
import { chainMethods } from '@payloadcms/drizzle'
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { CountDistinct, SQLiteAdapter } from './types.js'
|
||||
@@ -19,25 +20,30 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
return Number(countResult[0]?.count)
|
||||
}
|
||||
|
||||
let query: SQLiteSelect = db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1)
|
||||
.$dynamic()
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
query = query.leftJoin(table, condition)
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
// When we have any joins, we need to count each individual ID only once.
|
||||
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
|
||||
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
|
||||
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
|
||||
const countResult = await query
|
||||
const countResult = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
|
||||
return Number(countResult[0]?.count)
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import type {
|
||||
Args as _Args,
|
||||
CountDistinct as _CountDistinct,
|
||||
DeleteWhere as _DeleteWhere,
|
||||
DropDatabase as _DropDatabase,
|
||||
Execute as _Execute,
|
||||
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
|
||||
GenericColumns as _GenericColumns,
|
||||
GenericRelation as _GenericRelation,
|
||||
GenericTable as _GenericTable,
|
||||
IDType as _IDType,
|
||||
Insert as _Insert,
|
||||
MigrateDownArgs as _MigrateDownArgs,
|
||||
MigrateUpArgs as _MigrateUpArgs,
|
||||
SQLiteAdapter as _SQLiteAdapter,
|
||||
SQLiteSchemaHook as _SQLiteSchemaHook,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type SQLiteAdapter = _SQLiteAdapter
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type Args = _Args
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type CountDistinct = _CountDistinct
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type DeleteWhere = _DeleteWhere
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type DropDatabase = _DropDatabase
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type Execute<T> = _Execute<T>
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GenericColumns = _GenericColumns
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GenericRelation = _GenericRelation
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type GenericTable = _GenericTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type IDType = _IDType
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type Insert = _Insert
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type MigrateDownArgs = _MigrateDownArgs
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type MigrateUpArgs = _MigrateUpArgs
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-sqlite` instead
|
||||
*/
|
||||
export type SQLiteSchemaHook = _SQLiteSchemaHook
|
||||
@@ -58,6 +58,10 @@ import { init } from './init.js'
|
||||
import { insert } from './insert.js'
|
||||
import { requireDrizzleKit } from './requireDrizzleKit.js'
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
|
||||
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
|
||||
export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
@@ -193,32 +197,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'sqlite',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo deprecate /types subpath export in 4.0
|
||||
*/
|
||||
export type {
|
||||
Args as SQLiteAdapterArgs,
|
||||
CountDistinct,
|
||||
DeleteWhere,
|
||||
DropDatabase,
|
||||
Execute,
|
||||
GeneratedDatabaseSchema,
|
||||
GenericColumns,
|
||||
GenericRelation,
|
||||
GenericTable,
|
||||
IDType,
|
||||
Insert,
|
||||
MigrateDownArgs,
|
||||
MigrateUpArgs,
|
||||
SQLiteAdapter,
|
||||
SQLiteSchemaHook,
|
||||
} from './types.js'
|
||||
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts",
|
||||
"default": "./src/exports/types-deprecated.ts"
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./src/exports/migration-utils.ts",
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -103,9 +103,9 @@
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts",
|
||||
"default": "./dist/exports/types-deprecated.js"
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
},
|
||||
"./migration-utils": {
|
||||
"import": "./dist/exports/migration-utils.js",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type {
|
||||
Args as _Args,
|
||||
GeneratedDatabaseSchema as _GeneratedDatabaseSchema,
|
||||
VercelPostgresAdapter as _VercelPostgresAdapter,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
|
||||
*/
|
||||
export type Args = _Args
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
|
||||
*/
|
||||
export type GeneratedDatabaseSchema = _GeneratedDatabaseSchema
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/db-vercel-postgres` instead
|
||||
*/
|
||||
export type VercelPostgresAdapter = _VercelPostgresAdapter
|
||||
@@ -205,21 +205,12 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'postgres',
|
||||
allowIDOnCreate,
|
||||
defaultIDType: payloadIDType,
|
||||
init: adapter,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo deprecate /types subpath export in 4.0
|
||||
*/
|
||||
export type {
|
||||
Args as VercelPostgresAdapterArgs,
|
||||
GeneratedDatabaseSchema,
|
||||
VercelPostgresAdapter,
|
||||
} from './types.js'
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/drizzle/postgres'
|
||||
export { geometryColumn } from '@payloadcms/drizzle/postgres'
|
||||
export { sql } from 'drizzle-orm'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -30,13 +30,13 @@
|
||||
"default": "./src/exports/postgres.ts"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./src/exports/types-deprecated.ts",
|
||||
"types": "./src/exports/types-deprecated.ts",
|
||||
"default": "./src/exports/types-deprecated.ts"
|
||||
"import": "./src/types.ts",
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"types": "./src/types.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"mock.js"
|
||||
@@ -81,9 +81,9 @@
|
||||
"default": "./dist/exports/postgres.js"
|
||||
},
|
||||
"./types": {
|
||||
"import": "./dist/exports/types-deprecated.js",
|
||||
"types": "./dist/exports/types-deprecated.d.ts",
|
||||
"default": "./dist/exports/types-deprecated.js"
|
||||
"import": "./dist/types.js",
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteOne: DeleteOne = async function deleteOne(
|
||||
this: DrizzleAdapter,
|
||||
{ collection: collectionSlug, req, returning, select, where: whereArg },
|
||||
{ collection: collectionSlug, req, select, where: whereArg, returning },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
@@ -32,9 +32,9 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: [{ args: [1], method: 'limit' }],
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) => query.limit(1),
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
@@ -59,10 +59,6 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
docToDelete = await db.query[tableName].findFirst(findManyArgs)
|
||||
}
|
||||
|
||||
if (!docToDelete) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result =
|
||||
returning === false
|
||||
? null
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import type {
|
||||
BaseRawColumn as _BaseRawColumn,
|
||||
BuildDrizzleTable as _BuildDrizzleTable,
|
||||
BuildQueryJoinAliases as _BuildQueryJoinAliases,
|
||||
ChainedMethods as _ChainedMethods,
|
||||
ColumnToCodeConverter as _ColumnToCodeConverter,
|
||||
CountDistinct as _CountDistinct,
|
||||
CreateJSONQueryArgs as _CreateJSONQueryArgs,
|
||||
DeleteWhere as _DeleteWhere,
|
||||
DrizzleAdapter as _DrizzleAdapter,
|
||||
DrizzleTransaction as _DrizzleTransaction,
|
||||
DropDatabase as _DropDatabase,
|
||||
EnumRawColumn as _EnumRawColumn,
|
||||
Execute as _Execute,
|
||||
GenericColumn as _GenericColumn,
|
||||
GenericColumns as _GenericColumns,
|
||||
GenericPgColumn as _GenericPgColumn,
|
||||
GenericRelation as _GenericRelation,
|
||||
GenericTable as _GenericTable,
|
||||
IDType as _IDType,
|
||||
Insert as _Insert,
|
||||
IntegerRawColumn as _IntegerRawColumn,
|
||||
Migration as _Migration,
|
||||
PostgresDB as _PostgresDB,
|
||||
RawColumn as _RawColumn,
|
||||
RawForeignKey as _RawForeignKey,
|
||||
RawIndex as _RawIndex,
|
||||
RawRelation as _RawRelation,
|
||||
RawTable as _RawTable,
|
||||
RelationMap as _RelationMap,
|
||||
RequireDrizzleKit as _RequireDrizzleKit,
|
||||
SetColumnID as _SetColumnID,
|
||||
SQLiteDB as _SQLiteDB,
|
||||
TimestampRawColumn as _TimestampRawColumn,
|
||||
TransactionPg as _TransactionPg,
|
||||
TransactionSQLite as _TransactionSQLite,
|
||||
UUIDRawColumn as _UUIDRawColumn,
|
||||
VectorRawColumn as _VectorRawColumn,
|
||||
} from '../types.js'
|
||||
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type BaseRawColumn = _BaseRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type BuildDrizzleTable = _BuildDrizzleTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type BuildQueryJoinAliases = _BuildQueryJoinAliases
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type ChainedMethods = _ChainedMethods
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type ColumnToCodeConverter = _ColumnToCodeConverter
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type CountDistinct = _CountDistinct
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type CreateJSONQueryArgs = _CreateJSONQueryArgs
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DeleteWhere = _DeleteWhere
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DrizzleAdapter = _DrizzleAdapter
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DrizzleTransaction = _DrizzleTransaction
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type DropDatabase = _DropDatabase
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type EnumRawColumn = _EnumRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type Execute<T> = _Execute<T>
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericColumn = _GenericColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericColumns<T> = _GenericColumns<T>
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericPgColumn = _GenericPgColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericRelation = _GenericRelation
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type GenericTable = _GenericTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type IDType = _IDType
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type Insert = _Insert
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type IntegerRawColumn = _IntegerRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type Migration = _Migration
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type PostgresDB = _PostgresDB
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawColumn = _RawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawForeignKey = _RawForeignKey
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawIndex = _RawIndex
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawRelation = _RawRelation
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RawTable = _RawTable
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RelationMap = _RelationMap
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type RequireDrizzleKit = _RequireDrizzleKit
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type SetColumnID = _SetColumnID
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type SQLiteDB = _SQLiteDB
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type TimestampRawColumn = _TimestampRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type TransactionPg = _TransactionPg
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type TransactionSQLite = _TransactionSQLite
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type UUIDRawColumn = _UUIDRawColumn
|
||||
/**
|
||||
* @deprecated - import from `@payloadcms/drizzle` instead
|
||||
*/
|
||||
export type VectorRawColumn = _VectorRawColumn
|
||||
@@ -1,6 +1,3 @@
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
|
||||
*/
|
||||
export type ChainedMethods = {
|
||||
args: unknown[]
|
||||
method: string
|
||||
@@ -10,8 +7,6 @@ export type ChainedMethods = {
|
||||
* Call and returning methods that would normally be chained together but cannot be because of control logic
|
||||
* @param methods
|
||||
* @param query
|
||||
*
|
||||
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
|
||||
*/
|
||||
const chainMethods = <T>({ methods, query }: { methods: ChainedMethods; query: T }): T => {
|
||||
return methods.reduce((query, { args, method }) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FindArgs, FlattenedField, TypeWithID } from 'payload'
|
||||
import { inArray } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
import type { ChainedMethods } from './chainMethods.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { selectDistinct } from '../queries/selectDistinct.js'
|
||||
@@ -61,6 +62,15 @@ export const findMany = async function find({
|
||||
const orderedIDMap: Record<number | string, number> = {}
|
||||
let orderedIDs: (number | string)[]
|
||||
|
||||
const selectDistinctMethods: ChainedMethods = []
|
||||
|
||||
if (orderBy) {
|
||||
selectDistinctMethods.push({
|
||||
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||
method: 'orderBy',
|
||||
})
|
||||
}
|
||||
|
||||
const findManyArgs = buildFindManyArgs({
|
||||
adapter,
|
||||
collectionSlug,
|
||||
@@ -74,16 +84,15 @@ export const findMany = async function find({
|
||||
tableName,
|
||||
versions,
|
||||
})
|
||||
|
||||
selectDistinctMethods.push({ args: [offset], method: 'offset' })
|
||||
selectDistinctMethods.push({ args: [limit], method: 'limit' })
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter,
|
||||
chainedMethods: selectDistinctMethods,
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) => {
|
||||
if (orderBy) {
|
||||
query = query.orderBy(() => orderBy.map(({ column, order }) => order(column)))
|
||||
}
|
||||
return query.offset(offset).limit(limit)
|
||||
},
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { SQLiteSelect, SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
||||
import type { SQLiteSelectBase } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { and, asc, count, desc, eq, or, sql } from 'drizzle-orm'
|
||||
import {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../types.js'
|
||||
import type { Result } from './buildFindManyArgs.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
@@ -25,6 +25,7 @@ import { operatorMap } from '../queries/operatorMap.js'
|
||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||
import { jsonAggBuildObject } from '../utilities/json.js'
|
||||
import { rawConstraint } from '../utilities/rawConstraint.js'
|
||||
import { chainMethods } from './chainMethods.js'
|
||||
|
||||
const flattenAllWherePaths = (where: Where, paths: string[]) => {
|
||||
for (const k in where) {
|
||||
@@ -611,6 +612,34 @@ export const traverseFields = ({
|
||||
where: joinQueryWhere,
|
||||
})
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
joins.forEach(({ type, condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: type ?? 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
if (page && limit !== 0) {
|
||||
const offset = (page - 1) * limit - 1
|
||||
if (offset > 0) {
|
||||
chainedMethods.push({
|
||||
args: [offset],
|
||||
method: 'offset',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (limit !== 0) {
|
||||
chainedMethods.push({
|
||||
args: [limit],
|
||||
method: 'limit',
|
||||
})
|
||||
}
|
||||
|
||||
const db = adapter.drizzle as LibSQLDatabase
|
||||
|
||||
for (let key in selectFields) {
|
||||
const val = selectFields[key]
|
||||
|
||||
@@ -625,29 +654,14 @@ export const traverseFields = ({
|
||||
selectFields.parent = newAliasTable.parent
|
||||
}
|
||||
|
||||
let query: SQLiteSelect = db
|
||||
.select(selectFields as any)
|
||||
.from(newAliasTable)
|
||||
.where(subQueryWhere)
|
||||
.orderBy(() => orderBy.map(({ column, order }) => order(column)))
|
||||
.$dynamic()
|
||||
|
||||
joins.forEach(({ type, condition, table }) => {
|
||||
query = query[type ?? 'leftJoin'](table, condition)
|
||||
})
|
||||
|
||||
if (page && limit !== 0) {
|
||||
const offset = (page - 1) * limit - 1
|
||||
if (offset > 0) {
|
||||
query = query.offset(offset)
|
||||
}
|
||||
}
|
||||
|
||||
if (limit !== 0) {
|
||||
query = query.limit(limit)
|
||||
}
|
||||
|
||||
const subQuery = query.as(subQueryAlias)
|
||||
const subQuery = chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select(selectFields as any)
|
||||
.from(newAliasTable)
|
||||
.where(subQueryWhere)
|
||||
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
|
||||
}).as(subQueryAlias)
|
||||
|
||||
if (shouldCount) {
|
||||
currentArgs.extras[`${columnName}_count`] = sql`${db
|
||||
|
||||
@@ -23,55 +23,14 @@ export { migrateFresh } from './migrateFresh.js'
|
||||
export { migrateRefresh } from './migrateRefresh.js'
|
||||
export { migrateReset } from './migrateReset.js'
|
||||
export { migrateStatus } from './migrateStatus.js'
|
||||
export { default as buildQuery } from './queries/buildQuery.js'
|
||||
export { operatorMap } from './queries/operatorMap.js'
|
||||
export type { Operators } from './queries/operatorMap.js'
|
||||
export { parseParams } from './queries/parseParams.js'
|
||||
export { queryDrafts } from './queryDrafts.js'
|
||||
export { buildDrizzleRelations } from './schema/buildDrizzleRelations.js'
|
||||
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 type {
|
||||
BaseRawColumn,
|
||||
BuildDrizzleTable,
|
||||
BuildQueryJoinAliases,
|
||||
ChainedMethods,
|
||||
ColumnToCodeConverter,
|
||||
CountDistinct,
|
||||
CreateJSONQueryArgs,
|
||||
DeleteWhere,
|
||||
DrizzleAdapter,
|
||||
DrizzleTransaction,
|
||||
DropDatabase,
|
||||
EnumRawColumn,
|
||||
Execute,
|
||||
GenericColumn,
|
||||
GenericColumns,
|
||||
GenericPgColumn,
|
||||
GenericRelation,
|
||||
GenericTable,
|
||||
IDType,
|
||||
Insert,
|
||||
IntegerRawColumn,
|
||||
Migration,
|
||||
PostgresDB,
|
||||
RawColumn,
|
||||
RawForeignKey,
|
||||
RawIndex,
|
||||
RawRelation,
|
||||
RawTable,
|
||||
RelationMap,
|
||||
RequireDrizzleKit,
|
||||
SetColumnID,
|
||||
SQLiteDB,
|
||||
TimestampRawColumn,
|
||||
TransactionPg,
|
||||
TransactionSQLite,
|
||||
UUIDRawColumn,
|
||||
VectorRawColumn,
|
||||
} from './types.js'
|
||||
export { updateGlobal } from './updateGlobal.js'
|
||||
export { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
export { updateJobs } from './updateJobs.js'
|
||||
|
||||
@@ -50,8 +50,7 @@ export async function migrateDown(this: DrizzleAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this, db)
|
||||
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function migrateRefresh(this: DrizzleAdapter) {
|
||||
msg: `Migrated down: ${migration.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this, db)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
collection: 'payload-migrations',
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function migrateReset(this: DrizzleAdapter): Promise<void> {
|
||||
msg: `Migrated down: ${migrationFile.name} (${Date.now() - start}ms)`,
|
||||
})
|
||||
|
||||
const tableExists = await migrationTableExists(this, db)
|
||||
const tableExists = await migrationTableExists(this)
|
||||
if (tableExists) {
|
||||
await payload.delete({
|
||||
id: migration.id,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { ChainedMethods } from '../types.js'
|
||||
import type { BasePostgresAdapter, CountDistinct } from './types.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
|
||||
export const countDistinct: CountDistinct = async function countDistinct(
|
||||
this: BasePostgresAdapter,
|
||||
{ db, joins, tableName, where },
|
||||
@@ -19,25 +20,30 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
return Number(countResult[0].count)
|
||||
}
|
||||
|
||||
let query = db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1)
|
||||
.$dynamic()
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
query = query.leftJoin(table as PgTableWithColumns<any>, condition)
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
// When we have any joins, we need to count each individual ID only once.
|
||||
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
|
||||
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
|
||||
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
|
||||
const countResult = await query
|
||||
const countResult = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
|
||||
return Number(countResult[0].count)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type Args = {
|
||||
aliasTable?: Table
|
||||
fields: FlattenedField[]
|
||||
joins: BuildQueryJoinAliases
|
||||
locale?: string
|
||||
locale: string
|
||||
parentIsLocalized: boolean
|
||||
selectFields: Record<string, GenericColumn>
|
||||
selectLocale?: boolean
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { QueryPromise, SQL } from 'drizzle-orm'
|
||||
import type { PgSelect } from 'drizzle-orm/pg-core'
|
||||
import type { SQLiteColumn, SQLiteSelect } from 'drizzle-orm/sqlite-core'
|
||||
import type { SQLiteColumn } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import type { ChainedMethods } from '../find/chainMethods.js'
|
||||
import type {
|
||||
DrizzleAdapter,
|
||||
DrizzleTransaction,
|
||||
@@ -12,11 +12,13 @@ import type {
|
||||
} from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
chainedMethods?: ChainedMethods
|
||||
db: DrizzleAdapter['drizzle'] | DrizzleTransaction
|
||||
joins: BuildQueryJoinAliases
|
||||
query?: (args: { query: SQLiteSelect }) => SQLiteSelect
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
where: SQL
|
||||
@@ -27,40 +29,42 @@ type Args = {
|
||||
*/
|
||||
export const selectDistinct = ({
|
||||
adapter,
|
||||
chainedMethods = [],
|
||||
db,
|
||||
joins,
|
||||
query: queryModifier = ({ query }) => query,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
}: Args): QueryPromise<{ id: number | string }[] & Record<string, GenericColumn>> => {
|
||||
if (Object.keys(joins).length > 0) {
|
||||
let query: SQLiteSelect
|
||||
if (where) {
|
||||
chainedMethods.push({ args: [where], method: 'where' })
|
||||
}
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
chainedMethods.push({
|
||||
args: [table, condition],
|
||||
method: 'leftJoin',
|
||||
})
|
||||
})
|
||||
|
||||
let query
|
||||
const table = adapter.tables[tableName]
|
||||
|
||||
if (adapter.name === 'postgres') {
|
||||
query = (db as TransactionPg)
|
||||
.selectDistinct(selectFields as Record<string, GenericPgColumn>)
|
||||
.from(table)
|
||||
.$dynamic() as unknown as SQLiteSelect
|
||||
}
|
||||
if (adapter.name === 'sqlite') {
|
||||
query = (db as TransactionSQLite)
|
||||
.selectDistinct(selectFields as Record<string, SQLiteColumn>)
|
||||
.from(table)
|
||||
.$dynamic()
|
||||
}
|
||||
|
||||
if (where) {
|
||||
query = query.where(where)
|
||||
}
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
query = query.leftJoin(table, condition)
|
||||
})
|
||||
|
||||
return queryModifier({
|
||||
return chainMethods({
|
||||
methods: chainedMethods,
|
||||
query,
|
||||
}) as unknown as QueryPromise<{ id: number | string }[] & Record<string, GenericColumn>>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,11 @@ import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
|
||||
import type { SQLiteRaw } from 'drizzle-orm/sqlite-core/query-builders/raw'
|
||||
import type { QueryResult } from 'pg'
|
||||
|
||||
import type { ChainedMethods } from './find/chainMethods.js'
|
||||
import type { Operators } from './queries/operatorMap.js'
|
||||
|
||||
export { ChainedMethods }
|
||||
|
||||
export type PostgresDB = NodePgDatabase<Record<string, unknown>>
|
||||
|
||||
export type SQLiteDB = LibSQLDatabase<
|
||||
@@ -374,8 +377,3 @@ export type RelationMap = Map<
|
||||
type: 'many' | 'one'
|
||||
}
|
||||
>
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0. Use query + $dynamic() instead: https://orm.drizzle.team/docs/dynamic-query-building
|
||||
*/
|
||||
export type { ChainedMethods } from './find/chainMethods.js'
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { UpdateJobs, Where } from 'payload'
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { BaseJob, UpdateJobs, Where } from 'payload'
|
||||
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
import type { ChainedMethods, DrizzleAdapter } from './types.js'
|
||||
|
||||
import { chainMethods } from './find/chainMethods.js'
|
||||
import { findMany } from './find/findMany.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { transform } from './transform/read/index.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
@@ -12,9 +17,6 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
this: DrizzleAdapter,
|
||||
{ id, data, limit: limitArg, req, returning, sort: sortArg, where: whereArg },
|
||||
) {
|
||||
if (!(data?.log as object[])?.length) {
|
||||
delete data.log
|
||||
}
|
||||
const whereToUse: Where = id ? { id: { equals: id } } : whereArg
|
||||
const limit = id ? 1 : limitArg
|
||||
|
||||
@@ -23,6 +25,128 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const sort = sortArg !== undefined && sortArg !== null ? sortArg : collection.defaultSort
|
||||
|
||||
const dataKeys = Object.keys(data)
|
||||
// The initial update is when all jobs are being updated to processing and fetched
|
||||
const isInitialUpdate = dataKeys.length === 1 && dataKeys[0] === 'processing'
|
||||
|
||||
if (isInitialUpdate) {
|
||||
// Performance optimization for the initial update - this needs to happen as quickly as possible
|
||||
const _db = db as LibSQLDatabase
|
||||
|
||||
const rowToInsert: {
|
||||
id?: number | string
|
||||
processing: boolean
|
||||
} = data as { processing: boolean }
|
||||
|
||||
const { orderBy, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.flattenedFields,
|
||||
sort,
|
||||
tableName,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const table = this.tables[tableName]
|
||||
const jobsLogTable = this.tables['payload_jobs_log']
|
||||
|
||||
let idsToUpdate: (number | string)[] = []
|
||||
let docsToUpdate: BaseJob[] = []
|
||||
|
||||
// Fetch all jobs that should be updated. This can't be done in the update query, as
|
||||
// 1) we need to join the logs table to get the logs for each job
|
||||
// 2) postgres doesn't support limit on update queries
|
||||
const jobsQuery = _db
|
||||
.select({
|
||||
id: table.id,
|
||||
})
|
||||
.from(table)
|
||||
.where(where)
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
if (typeof limit === 'number' && limit > 0) {
|
||||
chainedMethods.push({
|
||||
args: [limit],
|
||||
method: 'limit',
|
||||
})
|
||||
}
|
||||
|
||||
if (orderBy) {
|
||||
chainedMethods.push({
|
||||
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||
method: 'orderBy',
|
||||
})
|
||||
}
|
||||
|
||||
docsToUpdate = (await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: jobsQuery,
|
||||
})) as BaseJob[]
|
||||
|
||||
idsToUpdate = docsToUpdate?.map((job) => job.id)
|
||||
|
||||
// Now fetch all log entries for these jobs
|
||||
if (idsToUpdate.length) {
|
||||
const logsQuery = _db
|
||||
.select({
|
||||
id: jobsLogTable.id,
|
||||
completedAt: jobsLogTable.completedAt,
|
||||
error: jobsLogTable.error,
|
||||
executedAt: jobsLogTable.executedAt,
|
||||
input: jobsLogTable.input,
|
||||
output: jobsLogTable.output,
|
||||
parentID: jobsLogTable._parentID,
|
||||
state: jobsLogTable.state,
|
||||
taskID: jobsLogTable.taskID,
|
||||
taskSlug: jobsLogTable.taskSlug,
|
||||
})
|
||||
.from(jobsLogTable)
|
||||
.where(inArray(jobsLogTable._parentID, idsToUpdate))
|
||||
|
||||
const logs = await logsQuery
|
||||
|
||||
// Group logs by parent ID
|
||||
const logsByParentId = logs.reduce(
|
||||
(acc, log) => {
|
||||
const parentId = log.parentID
|
||||
if (!acc[parentId]) {
|
||||
acc[parentId] = []
|
||||
}
|
||||
acc[parentId].push(log)
|
||||
return acc
|
||||
},
|
||||
{} as Record<number | string, any[]>,
|
||||
)
|
||||
|
||||
// Attach logs to their respective jobs
|
||||
for (const job of docsToUpdate) {
|
||||
job.log = logsByParentId[job.id] || []
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the actual update
|
||||
const query = _db
|
||||
.update(table)
|
||||
.set(rowToInsert)
|
||||
.where(inArray(table.id, idsToUpdate))
|
||||
.returning()
|
||||
|
||||
const updatedJobs = (await query) as BaseJob[]
|
||||
|
||||
return updatedJobs.map((row) => {
|
||||
// Attach logs to the updated job
|
||||
row.log = docsToUpdate.find((job) => job.id === row.id)?.log || []
|
||||
|
||||
return transform<BaseJob>({
|
||||
adapter: this,
|
||||
config: this.payload.config,
|
||||
data: row,
|
||||
fields: collection.flattenedFields,
|
||||
joinQuery: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const jobs = await findMany({
|
||||
adapter: this,
|
||||
collectionSlug: 'payload-jobs',
|
||||
@@ -38,7 +162,7 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
return []
|
||||
}
|
||||
|
||||
const results = []
|
||||
const results: BaseJob[] = []
|
||||
|
||||
// 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 job of jobs.docs) {
|
||||
@@ -47,7 +171,7 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
...data,
|
||||
}
|
||||
|
||||
const result = await upsertRow({
|
||||
const result = await upsertRow<BaseJob>({
|
||||
id: job.id,
|
||||
adapter: this,
|
||||
data: updateData,
|
||||
@@ -58,7 +182,6 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
req,
|
||||
tableName,
|
||||
})
|
||||
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { UpdateMany } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
import type { ChainedMethods, DrizzleAdapter } from './types.js'
|
||||
|
||||
import { chainMethods } from './find/chainMethods.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
@@ -44,10 +45,16 @@ export const updateMany: UpdateMany = async function updateMany(
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: orderBy
|
||||
? [
|
||||
{
|
||||
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||
method: 'orderBy',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) =>
|
||||
orderBy ? query.orderBy(() => orderBy.map(({ column, order }) => order(column))) : query,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
@@ -62,17 +69,28 @@ export const updateMany: UpdateMany = async function updateMany(
|
||||
|
||||
const table = this.tables[tableName]
|
||||
|
||||
let query = _db.select({ id: table.id }).from(table).where(where).$dynamic()
|
||||
const query = _db.select({ id: table.id }).from(table).where(where)
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
if (typeof limit === 'number' && limit > 0) {
|
||||
query = query.limit(limit)
|
||||
chainedMethods.push({
|
||||
args: [limit],
|
||||
method: 'limit',
|
||||
})
|
||||
}
|
||||
|
||||
if (orderBy) {
|
||||
query = query.orderBy(() => orderBy.map(({ column, order }) => order(column)))
|
||||
chainedMethods.push({
|
||||
args: [() => orderBy.map(({ column, order }) => order(column))],
|
||||
method: 'orderBy',
|
||||
})
|
||||
}
|
||||
|
||||
const docsToUpdate = await query
|
||||
const docsToUpdate = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query,
|
||||
})
|
||||
|
||||
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
// selectDistinct will only return if there are joins
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
chainedMethods: [{ args: [1], method: 'limit' }],
|
||||
db,
|
||||
joins,
|
||||
query: ({ query }) => query.limit(1),
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
|
||||
@@ -423,7 +423,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
path: fieldName,
|
||||
},
|
||||
],
|
||||
req,
|
||||
},
|
||||
req?.t,
|
||||
)
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
|
||||
import type { DrizzleAdapter, PostgresDB } from '../types.js'
|
||||
|
||||
export const migrationTableExists = async (
|
||||
adapter: DrizzleAdapter,
|
||||
db?: LibSQLDatabase | PostgresDB,
|
||||
): Promise<boolean> => {
|
||||
export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boolean> => {
|
||||
let statement
|
||||
|
||||
if (adapter.name === 'postgres') {
|
||||
@@ -25,7 +20,7 @@ export const migrationTableExists = async (
|
||||
}
|
||||
|
||||
const result = await adapter.execute({
|
||||
drizzle: db ?? adapter.drizzle,
|
||||
drizzle: adapter.drizzle,
|
||||
raw: statement,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'
|
||||
import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLObjectType } from 'graphql'
|
||||
|
||||
export const buildPaginatedListType = (name, docType) =>
|
||||
new GraphQLObjectType({
|
||||
name,
|
||||
fields: {
|
||||
docs: {
|
||||
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(docType))),
|
||||
type: new GraphQLList(docType),
|
||||
},
|
||||
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
hasPrevPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
limit: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
nextPage: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
hasPrevPage: { type: GraphQLBoolean },
|
||||
limit: { type: GraphQLInt },
|
||||
nextPage: { type: GraphQLInt },
|
||||
offset: { type: GraphQLInt },
|
||||
page: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
pagingCounter: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
prevPage: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
totalDocs: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
totalPages: { type: new GraphQLNonNull(GraphQLInt) },
|
||||
page: { type: GraphQLInt },
|
||||
pagingCounter: { type: GraphQLInt },
|
||||
prevPage: { type: GraphQLInt },
|
||||
totalDocs: { type: GraphQLInt },
|
||||
totalPages: { type: GraphQLInt },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -348,15 +348,11 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
name: joinName,
|
||||
fields: {
|
||||
docs: {
|
||||
type: new GraphQLNonNull(
|
||||
Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: new GraphQLList(
|
||||
new GraphQLNonNull(graphqlResult.collections[field.collection].graphQL.type),
|
||||
),
|
||||
),
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
|
||||
},
|
||||
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
@@ -383,8 +379,6 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
const { limit, page, sort, where } = args
|
||||
const { req } = context
|
||||
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
const fullWhere = combineQueries(where, {
|
||||
[field.on]: { equals: parent._id ?? parent.id },
|
||||
})
|
||||
@@ -396,7 +390,6 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
return await req.payload.find({
|
||||
collection,
|
||||
depth: 0,
|
||||
draft,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
limit,
|
||||
locale: req.locale,
|
||||
@@ -432,7 +425,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
...objectTypeConfig,
|
||||
[formatName(field.name)]: formattedNameResolver({
|
||||
type: withNullableType({
|
||||
type: field?.hasMany === true ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
type: field?.hasMany === true ? new GraphQLList(type) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
@@ -860,10 +853,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
|
||||
...objectTypeConfig,
|
||||
[formatName(field.name)]: formattedNameResolver({
|
||||
type: withNullableType({
|
||||
type:
|
||||
field.hasMany === true
|
||||
? new GraphQLList(new GraphQLNonNull(GraphQLString))
|
||||
: GraphQLString,
|
||||
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import type { FieldSchemaJSON } from 'payload'
|
||||
|
||||
import type { LivePreviewMessageEvent } from './types.js'
|
||||
|
||||
import { isLivePreviewEvent } from './isLivePreviewEvent.js'
|
||||
import { mergeData } from './mergeData.js'
|
||||
|
||||
const _payloadLivePreview: {
|
||||
fieldSchema: FieldSchemaJSON | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
previousData: any
|
||||
} = {
|
||||
const _payloadLivePreview = {
|
||||
/**
|
||||
* For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
|
||||
* We need to cache this value so that it can be used across subsequent messages
|
||||
@@ -24,7 +18,7 @@ const _payloadLivePreview: {
|
||||
previousData: undefined,
|
||||
}
|
||||
|
||||
export const handleMessage = async <T extends Record<string, any>>(args: {
|
||||
export const handleMessage = async <T>(args: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
event: LivePreviewMessageEvent<T>
|
||||
|
||||
@@ -4,15 +4,7 @@ import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
const defaultRequestHandler = ({
|
||||
apiPath,
|
||||
endpoint,
|
||||
serverURL,
|
||||
}: {
|
||||
apiPath: string
|
||||
endpoint: string
|
||||
serverURL: string
|
||||
}) => {
|
||||
const defaultRequestHandler = ({ apiPath, endpoint, serverURL }) => {
|
||||
const url = `${serverURL}${apiPath}/${endpoint}`
|
||||
return fetch(url, {
|
||||
credentials: 'include',
|
||||
@@ -27,7 +19,7 @@ const defaultRequestHandler = ({
|
||||
// Instead, we keep track of the old locale ourselves and trigger a re-population when it changes
|
||||
let prevLocale: string | undefined
|
||||
|
||||
export const mergeData = async <T extends Record<string, any>>(args: {
|
||||
export const mergeData = async <T>(args: {
|
||||
apiRoute?: string
|
||||
collectionPopulationRequestHandler?: ({
|
||||
apiPath,
|
||||
@@ -94,7 +86,7 @@ export const mergeData = async <T extends Record<string, any>>(args: {
|
||||
|
||||
if (res?.docs?.length > 0) {
|
||||
res.docs.forEach((doc) => {
|
||||
populationsByCollection[collection]?.forEach((population) => {
|
||||
populationsByCollection[collection].forEach((population) => {
|
||||
if (population.id === doc.id) {
|
||||
population.ref[population.accessor] = doc
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { handleMessage } from './handleMessage.js'
|
||||
|
||||
export const subscribe = <T extends Record<string, any>>(args: {
|
||||
export const subscribe = <T>(args: {
|
||||
apiRoute?: string
|
||||
callback: (data: T) => void
|
||||
depth?: number
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import type { DocumentEvent, FieldSchemaJSON } from 'payload'
|
||||
import type { DocumentEvent } from 'payload'
|
||||
import type { fieldSchemaToJSON } from 'payload/shared'
|
||||
|
||||
import type { PopulationsByCollection } from './types.js'
|
||||
|
||||
import { traverseRichText } from './traverseRichText.js'
|
||||
|
||||
export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
export const traverseFields = <T>(args: {
|
||||
externallyUpdatedRelationship?: DocumentEvent
|
||||
fieldSchema: FieldSchemaJSON
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: T
|
||||
localeChanged: boolean
|
||||
populationsByCollection: PopulationsByCollection
|
||||
result: Record<string, any>
|
||||
result: T
|
||||
}): void => {
|
||||
const {
|
||||
externallyUpdatedRelationship,
|
||||
@@ -47,7 +48,7 @@ export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingRow,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
@@ -63,7 +64,7 @@ export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
case 'blocks':
|
||||
if (Array.isArray(incomingData[fieldName])) {
|
||||
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
|
||||
const incomingBlockJSON = fieldSchema.blocks?.[incomingBlock.blockType]
|
||||
const incomingBlockJSON = fieldSchema.blocks[incomingBlock.blockType]
|
||||
|
||||
if (!result[fieldName]) {
|
||||
result[fieldName] = []
|
||||
@@ -81,7 +82,7 @@ export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: incomingBlockJSON!.fields!,
|
||||
fieldSchema: incomingBlockJSON.fields,
|
||||
incomingData: incomingBlock,
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
@@ -105,7 +106,7 @@ export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
|
||||
traverseFields({
|
||||
externallyUpdatedRelationship,
|
||||
fieldSchema: fieldSchema.fields!,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
localeChanged,
|
||||
populationsByCollection,
|
||||
@@ -165,11 +166,11 @@ export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
incomingRelation === externallyUpdatedRelationship?.id
|
||||
|
||||
if (hasChanged || hasUpdated || localeChanged) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
ref: result[fieldName],
|
||||
@@ -264,11 +265,11 @@ export const traverseFields = <T extends Record<string, any>>(args: {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
if (!populationsByCollection[fieldSchema.relationTo!]) {
|
||||
populationsByCollection[fieldSchema.relationTo!] = []
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo!]?.push({
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
ref: result as Record<string, unknown>,
|
||||
|
||||
@@ -79,7 +79,7 @@ export const traverseRichText = ({
|
||||
populationsByCollection[incomingData.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[incomingData.relationTo]?.push({
|
||||
populationsByCollection[incomingData.relationTo].push({
|
||||
id:
|
||||
incomingData[key] && typeof incomingData[key] === 'object'
|
||||
? incomingData[key].id
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"strict": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
"references": [{ "path": "../payload" }]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { getSafeRedirect } from './getSafeRedirect'
|
||||
|
||||
const fallback = '/admin' // default fallback if the input is unsafe or invalid
|
||||
|
||||
describe('getSafeRedirect', () => {
|
||||
// Valid - safe redirect paths
|
||||
it.each([['/dashboard'], ['/admin/settings'], ['/projects?id=123'], ['/hello-world']])(
|
||||
'should allow safe relative path: %s',
|
||||
(input) => {
|
||||
// If the input is a clean relative path, it should be returned as-is
|
||||
expect(getSafeRedirect(input, fallback)).toBe(input)
|
||||
},
|
||||
)
|
||||
|
||||
// Invalid types or empty inputs
|
||||
it.each(['', null, undefined, 123, {}, []])(
|
||||
'should fallback on invalid or non-string input: %s',
|
||||
(input) => {
|
||||
// If the input is not a valid string, it should return the fallback
|
||||
expect(getSafeRedirect(input as any, fallback)).toBe(fallback)
|
||||
},
|
||||
)
|
||||
|
||||
// Unsafe redirect patterns
|
||||
it.each([
|
||||
'//example.com', // protocol-relative URL
|
||||
'/javascript:alert(1)', // JavaScript scheme
|
||||
'/JavaScript:alert(1)', // case-insensitive JavaScript
|
||||
'/http://unknown.com', // disguised external redirect
|
||||
'/https://unknown.com', // disguised external redirect
|
||||
'/%2Funknown.com', // encoded slash — could resolve to //
|
||||
'/\\/unknown.com', // escaped slash
|
||||
'/\\\\unknown.com', // double escaped slashes
|
||||
'/\\unknown.com', // single escaped slash
|
||||
'%2F%2Funknown.com', // fully encoded protocol-relative path
|
||||
'%2Fjavascript:alert(1)', // encoded JavaScript scheme
|
||||
])('should block unsafe redirect: %s', (input) => {
|
||||
// All of these should return the fallback because they’re unsafe
|
||||
expect(getSafeRedirect(input, fallback)).toBe(fallback)
|
||||
})
|
||||
|
||||
// Input with extra spaces should still be properly handled
|
||||
it('should trim whitespace before evaluating', () => {
|
||||
// A valid path with surrounding spaces should still be accepted
|
||||
expect(getSafeRedirect(' /dashboard ', fallback)).toBe('/dashboard')
|
||||
|
||||
// An unsafe path with spaces should still be rejected
|
||||
expect(getSafeRedirect(' //example.com ', fallback)).toBe(fallback)
|
||||
})
|
||||
|
||||
// If decoding the input fails (e.g., invalid percent encoding), it should not crash
|
||||
it('should return fallback on invalid encoding', () => {
|
||||
expect(getSafeRedirect('%E0%A4%A', fallback)).toBe(fallback)
|
||||
})
|
||||
})
|
||||
@@ -6,25 +6,14 @@ export const getSafeRedirect = (
|
||||
return fallback
|
||||
}
|
||||
|
||||
// Normalize and decode the path
|
||||
let redirectPath: string
|
||||
try {
|
||||
redirectPath = decodeURIComponent(redirectParam.trim())
|
||||
} catch {
|
||||
return fallback // invalid encoding
|
||||
}
|
||||
// Ensures that any leading or trailing whitespace doesn’t affect the checks
|
||||
const redirectPath = redirectParam.trim()
|
||||
|
||||
const isSafeRedirect =
|
||||
// Must start with a single forward slash (e.g., "/admin")
|
||||
redirectPath.startsWith('/') &&
|
||||
// Prevent protocol-relative URLs (e.g., "//example.com")
|
||||
// Prevent protocol-relative URLs (e.g., "//evil.com")
|
||||
!redirectPath.startsWith('//') &&
|
||||
// Prevent encoded slashes that could resolve to protocol-relative
|
||||
!redirectPath.startsWith('/%2F') &&
|
||||
// Prevent backslash-based escape attempts (e.g., "/\\/example.com", "/\\\\example.com", "/\\example.com")
|
||||
!redirectPath.startsWith('/\\/') &&
|
||||
!redirectPath.startsWith('/\\\\') &&
|
||||
!redirectPath.startsWith('/\\') &&
|
||||
// Prevent javascript-based schemes (e.g., "/javascript:alert(1)")
|
||||
!redirectPath.toLowerCase().startsWith('/javascript:') &&
|
||||
// Prevent attempts to redirect to full URLs using "/http:" or "/https:"
|
||||
|
||||
@@ -195,7 +195,6 @@ export const renderListView = async (
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
i18n: req.i18n,
|
||||
orderableFieldName: collectionConfig.orderable === true ? '_order' : undefined,
|
||||
payload,
|
||||
useAsTitle: collectionConfig.admin.useAsTitle,
|
||||
})
|
||||
@@ -260,7 +259,6 @@ export const renderListView = async (
|
||||
defaultSort={sort}
|
||||
listPreferences={listPreferences}
|
||||
modifySearchParams={!isInDrawer}
|
||||
orderableFieldName={collectionConfig.orderable === true ? '_order' : undefined}
|
||||
>
|
||||
{RenderServerComponent({
|
||||
clientProps: {
|
||||
|
||||
@@ -83,10 +83,10 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
current.set('localeCodes', JSON.stringify(selectedLocales.map((locale) => locale.value)))
|
||||
}
|
||||
|
||||
if (modifiedOnly === false) {
|
||||
current.set('modifiedOnly', 'false')
|
||||
} else {
|
||||
if (!modifiedOnly) {
|
||||
current.delete('modifiedOnly')
|
||||
} else {
|
||||
current.set('modifiedOnly', 'true')
|
||||
}
|
||||
|
||||
const search = current.toString()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { ChevronIcon, FieldDiffLabel, Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { ChevronIcon, Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { fieldIsArrayType, fieldIsBlockType } from 'payload/shared'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Label from '../Label/index.js'
|
||||
import './index.scss'
|
||||
import { countChangedFields, countChangedFieldsInRows } from '../utilities/countChangedFields.js'
|
||||
|
||||
@@ -99,7 +100,7 @@ export const DiffCollapser: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<FieldDiffLabel>
|
||||
<Label>
|
||||
<button
|
||||
aria-label={isCollapsed ? 'Expand' : 'Collapse'}
|
||||
className={`${baseClass}__toggle-button`}
|
||||
@@ -114,7 +115,7 @@ export const DiffCollapser: React.FC<Props> = ({
|
||||
{t('version:changedFieldsCount', { count: changeCount })}
|
||||
</Pill>
|
||||
)}
|
||||
</FieldDiffLabel>
|
||||
</Label>
|
||||
<div className={contentClassNames}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'field-diff-label'
|
||||
|
||||
export const FieldDiffLabel: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
|
||||
const Label: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
|
||||
<div className={baseClass}>{children}</div>
|
||||
)
|
||||
|
||||
export default Label
|
||||
@@ -1,23 +1,22 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
BaseVersionField,
|
||||
ClientField,
|
||||
ClientFieldSchemaMap,
|
||||
Field,
|
||||
FieldDiffClientProps,
|
||||
FieldDiffServerProps,
|
||||
FieldTypes,
|
||||
FlattenedBlock,
|
||||
PayloadComponent,
|
||||
PayloadRequest,
|
||||
SanitizedFieldPermissions,
|
||||
VersionField,
|
||||
} from 'payload'
|
||||
import type { DiffMethod } from 'react-diff-viewer-continued'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { dequal } from 'dequal/lite'
|
||||
import {
|
||||
type BaseVersionField,
|
||||
type ClientField,
|
||||
type ClientFieldSchemaMap,
|
||||
type Field,
|
||||
type FieldDiffClientProps,
|
||||
type FieldDiffServerProps,
|
||||
type FieldTypes,
|
||||
type FlattenedBlock,
|
||||
MissingEditorProp,
|
||||
type PayloadComponent,
|
||||
type PayloadRequest,
|
||||
type SanitizedFieldPermissions,
|
||||
type VersionField,
|
||||
} from 'payload'
|
||||
import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared'
|
||||
|
||||
import { diffMethods } from './fields/diffMethods.js'
|
||||
@@ -239,24 +238,7 @@ const buildVersionField = ({
|
||||
return null
|
||||
}
|
||||
|
||||
let CustomComponent = customDiffComponents?.[field.type]
|
||||
if (field?.type === 'richText') {
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
if (field.editor.CellComponent) {
|
||||
CustomComponent = field.editor.DiffComponent
|
||||
}
|
||||
}
|
||||
if (field?.admin?.components?.Diff) {
|
||||
CustomComponent = field.admin.components.Diff
|
||||
}
|
||||
|
||||
const CustomComponent = field?.admin?.components?.Diff ?? customDiffComponents?.[field.type]
|
||||
const DefaultComponent = diffComponents?.[field.type]
|
||||
|
||||
const baseVersionField: BaseVersionField = {
|
||||
|
||||
@@ -7,11 +7,12 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { FieldDiffLabel, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import React from 'react'
|
||||
import ReactDiffViewer from 'react-diff-viewer-continued'
|
||||
|
||||
import Label from '../../Label/index.js'
|
||||
import './index.scss'
|
||||
import { diffStyles } from '../styles.js'
|
||||
|
||||
@@ -168,10 +169,10 @@ export const Relationship: RelationshipFieldDiffClientComponent = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<FieldDiffLabel>
|
||||
<Label>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{getTranslation(label, i18n)}
|
||||
</FieldDiffLabel>
|
||||
</Label>
|
||||
<ReactDiffViewer
|
||||
hideLineNumbers
|
||||
newValue={versionToRender}
|
||||
|
||||
@@ -3,9 +3,10 @@ import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Option, SelectField, SelectFieldDiffClientComponent } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import Label from '../../Label/index.js'
|
||||
import './index.scss'
|
||||
import { diffStyles } from '../styles.js'
|
||||
import { DiffViewer } from './DiffViewer/index.js'
|
||||
@@ -102,10 +103,10 @@ export const Select: SelectFieldDiffClientComponent = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<FieldDiffLabel>
|
||||
<Label>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{'label' in field && getTranslation(field.label || '', i18n)}
|
||||
</FieldDiffLabel>
|
||||
</Label>
|
||||
<DiffViewer
|
||||
comparisonToRender={comparisonToRender}
|
||||
diffMethod={diffMethod}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import type { TextFieldDiffClientComponent } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { FieldDiffLabel, useTranslation } from '@payloadcms/ui'
|
||||
import { useTranslation } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import Label from '../../Label/index.js'
|
||||
import './index.scss'
|
||||
import { diffStyles } from '../styles.js'
|
||||
import { DiffViewer } from './DiffViewer/index.js'
|
||||
@@ -33,12 +34,12 @@ export const Text: TextFieldDiffClientComponent = ({
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<FieldDiffLabel>
|
||||
<Label>
|
||||
{locale && <span className={`${baseClass}__locale-label`}>{locale}</span>}
|
||||
{'label' in field &&
|
||||
typeof field.label !== 'function' &&
|
||||
getTranslation(field.label || '', i18n)}
|
||||
</FieldDiffLabel>
|
||||
</Label>
|
||||
<DiffViewer
|
||||
comparisonToRender={comparisonToRender}
|
||||
diffMethod={diffMethod}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { ReactDiffViewerStylesOverride } from 'react-diff-viewer-continued'
|
||||
|
||||
export const diffStyles: ReactDiffViewerStylesOverride = {
|
||||
export const diffStyles = {
|
||||
diffContainer: {
|
||||
minWidth: 'unset',
|
||||
},
|
||||
@@ -28,11 +26,4 @@ export const diffStyles: ReactDiffViewerStylesOverride = {
|
||||
wordRemovedBackground: 'var(--theme-error-200)',
|
||||
},
|
||||
},
|
||||
wordAdded: {
|
||||
color: 'var(--theme-success-600)',
|
||||
},
|
||||
wordRemoved: {
|
||||
color: 'var(--theme-error-600)',
|
||||
textDecorationLine: 'line-through',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function VersionView(props: DocumentViewServerProps) {
|
||||
|
||||
const comparisonVersionIDFromParams: string = searchParams.compareValue as string
|
||||
|
||||
const modifiedOnly: boolean = searchParams.modifiedOnly === 'false' ? false : true
|
||||
const modifiedOnly: boolean = searchParams.modifiedOnly === 'true'
|
||||
|
||||
const { localization } = config
|
||||
|
||||
|
||||
@@ -193,7 +193,6 @@ export async function VersionsView(props: DocumentViewServerProps) {
|
||||
defaultLimit={limitToUse}
|
||||
defaultSort={sort as string}
|
||||
modifySearchParams
|
||||
orderableFieldName={collectionConfig?.orderable === true ? '_order' : undefined}
|
||||
>
|
||||
<VersionsViewClient
|
||||
baseClass={baseClass}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.32.0",
|
||||
"version": "3.30.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -5,17 +5,12 @@ import type { JSONSchema4 } from 'json-schema'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
|
||||
import type { ValidationFieldError } from '../errors/ValidationError.js'
|
||||
import type {
|
||||
FieldAffectingData,
|
||||
RichTextField,
|
||||
RichTextFieldClient,
|
||||
Validate,
|
||||
} from '../fields/config/types.js'
|
||||
import type { FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { RequestContext } from '../index.js'
|
||||
import type { JsonObject, PayloadRequest, PopulateType } from '../types/index.js'
|
||||
import type { RichTextFieldClientProps, RichTextFieldServerProps } from './fields/RichText.js'
|
||||
import type { FieldDiffClientProps, FieldDiffServerProps, FieldSchemaMap } from './types.js'
|
||||
import type { RichTextFieldClientProps } from './fields/RichText.js'
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
|
||||
export type AfterReadRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
@@ -253,15 +248,7 @@ export type RichTextAdapter<
|
||||
ExtraFieldProperties = any,
|
||||
> = {
|
||||
CellComponent: PayloadComponent<never>
|
||||
/**
|
||||
* Component that will be displayed in the version diff view.
|
||||
* If not provided, richtext content will be diffed as JSON.
|
||||
*/
|
||||
DiffComponent?: PayloadComponent<
|
||||
FieldDiffServerProps<RichTextField, RichTextFieldClient>,
|
||||
FieldDiffClientProps<RichTextFieldClient>
|
||||
>
|
||||
FieldComponent: PayloadComponent<RichTextFieldServerProps, RichTextFieldClientProps>
|
||||
FieldComponent: PayloadComponent<never, RichTextFieldClientProps>
|
||||
} & RichTextAdapterBase<Value, AdapterProps, ExtraFieldProperties>
|
||||
|
||||
export type RichTextAdapterProvider<
|
||||
|
||||
@@ -13,12 +13,8 @@ export type Data = {
|
||||
export type Row = {
|
||||
blockType?: string
|
||||
collapsed?: boolean
|
||||
customComponents?: {
|
||||
RowLabel?: React.ReactNode
|
||||
}
|
||||
id: string
|
||||
isLoading?: boolean
|
||||
lastRenderedPath?: string
|
||||
}
|
||||
|
||||
export type FilterOptionsResult = {
|
||||
@@ -38,6 +34,7 @@ export type FieldState = {
|
||||
Error?: React.ReactNode
|
||||
Field?: React.ReactNode
|
||||
Label?: React.ReactNode
|
||||
RowLabels?: React.ReactNode[]
|
||||
}
|
||||
disableFormData?: boolean
|
||||
errorMessage?: string
|
||||
@@ -49,29 +46,9 @@ export type FieldState = {
|
||||
fieldSchema?: Field
|
||||
filterOptions?: FilterOptionsResult
|
||||
initialValue?: unknown
|
||||
/**
|
||||
* The path of the field when its custom components were last rendered.
|
||||
* This is used to denote if a field has been rendered, and if so,
|
||||
* what path it was rendered under last.
|
||||
*
|
||||
* If this path is undefined, or, if it is different
|
||||
* from the current path of a given field, the field's components will be re-rendered.
|
||||
*/
|
||||
lastRenderedPath?: string
|
||||
passesCondition?: boolean
|
||||
requiresRender?: boolean
|
||||
rows?: Row[]
|
||||
/**
|
||||
* The `serverPropsToIgnore` obj is used to prevent the various properties from being overridden across form state requests.
|
||||
* This can happen when queueing a form state request with `requiresRender: true` while the another is already processing.
|
||||
* For example:
|
||||
* 1. One "add row" action will set `requiresRender: true` and dispatch a form state request
|
||||
* 2. Another "add row" action will set `requiresRender: true` and queue a form state request
|
||||
* 3. The first request will return with `requiresRender: false`
|
||||
* 4. The second request will be dispatched with `requiresRender: false` but should be `true`
|
||||
* To fix this, only merge the `requiresRender` property if the previous state has not set it to `true`.
|
||||
* See the `mergeServerFormState` function for implementation details.
|
||||
*/
|
||||
serverPropsToIgnore?: Array<keyof FieldState>
|
||||
valid?: boolean
|
||||
validate?: Validate
|
||||
value?: unknown
|
||||
@@ -106,13 +83,6 @@ export type BuildFormStateArgs = {
|
||||
*/
|
||||
language?: keyof SupportedLanguages
|
||||
locale?: string
|
||||
/**
|
||||
* If true, will not render RSCs and instead return a simple string in their place.
|
||||
* This is useful for environments that lack RSC support, such as Jest.
|
||||
* Form state can still be built, but any server components will be omitted.
|
||||
* @default false
|
||||
*/
|
||||
mockRSCs?: boolean
|
||||
operation?: 'create' | 'update'
|
||||
/*
|
||||
If true, will render field components within their state object
|
||||
|
||||
@@ -60,7 +60,6 @@ export type BuildTableStateArgs = {
|
||||
columns?: ColumnPreference[]
|
||||
docs?: PaginatedDocs['docs']
|
||||
enableRowSelections?: boolean
|
||||
orderableFieldName: string
|
||||
parent?: {
|
||||
collectionSlug: CollectionSlug
|
||||
id: number | string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
import fs from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import process from 'node:process'
|
||||
|
||||
import type { PayloadComponent, SanitizedConfig } from '../../config/types.js'
|
||||
@@ -147,7 +147,7 @@ ${mapKeys.join(',\n')}
|
||||
|
||||
if (!force) {
|
||||
// Read current import map and check in the IMPORTS if there are any new imports. If not, don't write the file.
|
||||
const currentImportMap = await fs.readFile(importMapFilePath, 'utf-8')
|
||||
const currentImportMap = await fs.promises.readFile(importMapFilePath, 'utf-8')
|
||||
|
||||
if (currentImportMap?.trim() === importMapOutputFile?.trim()) {
|
||||
if (log) {
|
||||
@@ -161,5 +161,5 @@ ${mapKeys.join(',\n')}
|
||||
console.log('Writing import map to', importMapFilePath)
|
||||
}
|
||||
|
||||
await fs.writeFile(importMapFilePath, importMapOutputFile)
|
||||
await fs.promises.writeFile(importMapFilePath, importMapOutputFile)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,8 @@ export function iterateConfig({
|
||||
}
|
||||
|
||||
if (config?.admin?.dependencies) {
|
||||
for (const dependency of Object.values(config.admin.dependencies)) {
|
||||
for (const key in config.admin.dependencies) {
|
||||
const dependency = config.admin.dependencies[key]
|
||||
addToImportMap(dependency.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
import fs from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { compile } from 'json-schema-to-typescript'
|
||||
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
@@ -58,7 +58,7 @@ export async function generateTypes(
|
||||
|
||||
// Diff the compiled types against the existing types file
|
||||
try {
|
||||
const existingTypes = await fs.readFile(outputFile, 'utf-8')
|
||||
const existingTypes = fs.readFileSync(outputFile, 'utf-8')
|
||||
|
||||
if (compiled === existingTypes) {
|
||||
return
|
||||
@@ -67,7 +67,7 @@ export async function generateTypes(
|
||||
// swallow err
|
||||
}
|
||||
|
||||
await fs.writeFile(outputFile, compiled)
|
||||
fs.writeFileSync(outputFile, compiled)
|
||||
if (shouldLog) {
|
||||
logger.info(`Types written to ${outputFile}`)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
|
||||
import type { Config, SanitizedConfig } from '../../config/types.js'
|
||||
import type {
|
||||
CollectionConfig,
|
||||
|
||||
@@ -507,17 +507,6 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
|
||||
duration: number
|
||||
}
|
||||
| false
|
||||
/**
|
||||
* If true, enables custom ordering for the collection, and documents in the listView can be reordered via drag and drop.
|
||||
* New documents are inserted at the end of the list according to this parameter.
|
||||
*
|
||||
* Under the hood, a field with {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing|fractional indexing} is used to optimize inserts and reorderings.
|
||||
*
|
||||
* @default false
|
||||
*
|
||||
* @experimental There may be frequent breaking changes to this API
|
||||
*/
|
||||
orderable?: boolean
|
||||
slug: string
|
||||
/**
|
||||
* Add `createdAt` and `updatedAt` fields
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* THIS FILE IS COPIED FROM:
|
||||
* https://github.com/rocicorp/fractional-indexing/blob/main/src/index.js
|
||||
*
|
||||
* I AM NOT INSTALLING THAT LIBRARY BECAUSE JEST COMPLAINS ABOUT THE ESM MODULE AND THE TESTS FAIL.
|
||||
* DO NOT MODIFY IT
|
||||
*/
|
||||
|
||||
// License: CC0 (no rights reserved).
|
||||
|
||||
// This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
|
||||
|
||||
export const BASE_62_DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
|
||||
// `a` may be empty string, `b` is null or non-empty string.
|
||||
// `a < b` lexicographically if `b` is non-null.
|
||||
// no trailing zeros allowed.
|
||||
// digits is a string such as '0123456789' for base 10. Digits must be in
|
||||
// ascending character code order!
|
||||
/**
|
||||
* @param {string} a
|
||||
* @param {string | null | undefined} b
|
||||
* @param {string} digits
|
||||
* @returns {string}
|
||||
*/
|
||||
function midpoint(a, b, digits) {
|
||||
const zero = digits[0]
|
||||
if (b != null && a >= b) {
|
||||
throw new Error(a + ' >= ' + b)
|
||||
}
|
||||
if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) {
|
||||
throw new Error('trailing zero')
|
||||
}
|
||||
if (b) {
|
||||
// remove longest common prefix. pad `a` with 0s as we
|
||||
// go. note that we don't need to pad `b`, because it can't
|
||||
// end before `a` while traversing the common prefix.
|
||||
let n = 0
|
||||
while ((a[n] || zero) === b[n]) {
|
||||
n++
|
||||
}
|
||||
if (n > 0) {
|
||||
return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits)
|
||||
}
|
||||
}
|
||||
// first digits (or lack of digit) are different
|
||||
const digitA = a ? digits.indexOf(a[0]) : 0
|
||||
const digitB = b != null ? digits.indexOf(b[0]) : digits.length
|
||||
if (digitB - digitA > 1) {
|
||||
const midDigit = Math.round(0.5 * (digitA + digitB))
|
||||
return digits[midDigit]
|
||||
} else {
|
||||
// first digits are consecutive
|
||||
if (b && b.length > 1) {
|
||||
return b.slice(0, 1)
|
||||
} else {
|
||||
// `b` is null or has length 1 (a single digit).
|
||||
// the first digit of `a` is the previous digit to `b`,
|
||||
// or 9 if `b` is null.
|
||||
// given, for example, midpoint('49', '5'), return
|
||||
// '4' + midpoint('9', null), which will become
|
||||
// '4' + '9' + midpoint('', null), which is '495'
|
||||
return digits[digitA] + midpoint(a.slice(1), null, digits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} int
|
||||
* @return {void}
|
||||
*/
|
||||
|
||||
function validateInteger(int) {
|
||||
if (int.length !== getIntegerLength(int[0])) {
|
||||
throw new Error('invalid integer part of order key: ' + int)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} head
|
||||
* @return {number}
|
||||
*/
|
||||
|
||||
function getIntegerLength(head) {
|
||||
if (head >= 'a' && head <= 'z') {
|
||||
return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2
|
||||
} else if (head >= 'A' && head <= 'Z') {
|
||||
return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2
|
||||
} else {
|
||||
throw new Error('invalid order key head: ' + head)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
function getIntegerPart(key) {
|
||||
const integerPartLength = getIntegerLength(key[0])
|
||||
if (integerPartLength > key.length) {
|
||||
throw new Error('invalid order key: ' + key)
|
||||
}
|
||||
return key.slice(0, integerPartLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} digits
|
||||
* @return {void}
|
||||
*/
|
||||
|
||||
function validateOrderKey(key, digits) {
|
||||
if (key === 'A' + digits[0].repeat(26)) {
|
||||
throw new Error('invalid order key: ' + key)
|
||||
}
|
||||
// getIntegerPart will throw if the first character is bad,
|
||||
// or the key is too short. we'd call it to check these things
|
||||
// even if we didn't need the result
|
||||
const i = getIntegerPart(key)
|
||||
const f = key.slice(i.length)
|
||||
if (f.slice(-1) === digits[0]) {
|
||||
throw new Error('invalid order key: ' + key)
|
||||
}
|
||||
}
|
||||
|
||||
// note that this may return null, as there is a largest integer
|
||||
/**
|
||||
* @param {string} x
|
||||
* @param {string} digits
|
||||
* @return {string | null}
|
||||
*/
|
||||
function incrementInteger(x, digits) {
|
||||
validateInteger(x)
|
||||
const [head, ...digs] = x.split('')
|
||||
let carry = true
|
||||
for (let i = digs.length - 1; carry && i >= 0; i--) {
|
||||
const d = digits.indexOf(digs[i]) + 1
|
||||
if (d === digits.length) {
|
||||
digs[i] = digits[0]
|
||||
} else {
|
||||
digs[i] = digits[d]
|
||||
carry = false
|
||||
}
|
||||
}
|
||||
if (carry) {
|
||||
if (head === 'Z') {
|
||||
return 'a' + digits[0]
|
||||
}
|
||||
if (head === 'z') {
|
||||
return null
|
||||
}
|
||||
const h = String.fromCharCode(head.charCodeAt(0) + 1)
|
||||
if (h > 'a') {
|
||||
digs.push(digits[0])
|
||||
} else {
|
||||
digs.pop()
|
||||
}
|
||||
return h + digs.join('')
|
||||
} else {
|
||||
return head + digs.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// note that this may return null, as there is a smallest integer
|
||||
/**
|
||||
* @param {string} x
|
||||
* @param {string} digits
|
||||
* @return {string | null}
|
||||
*/
|
||||
|
||||
function decrementInteger(x, digits) {
|
||||
validateInteger(x)
|
||||
const [head, ...digs] = x.split('')
|
||||
let borrow = true
|
||||
for (let i = digs.length - 1; borrow && i >= 0; i--) {
|
||||
const d = digits.indexOf(digs[i]) - 1
|
||||
if (d === -1) {
|
||||
digs[i] = digits.slice(-1)
|
||||
} else {
|
||||
digs[i] = digits[d]
|
||||
borrow = false
|
||||
}
|
||||
}
|
||||
if (borrow) {
|
||||
if (head === 'a') {
|
||||
return 'Z' + digits.slice(-1)
|
||||
}
|
||||
if (head === 'A') {
|
||||
return null
|
||||
}
|
||||
const h = String.fromCharCode(head.charCodeAt(0) - 1)
|
||||
if (h < 'Z') {
|
||||
digs.push(digits.slice(-1))
|
||||
} else {
|
||||
digs.pop()
|
||||
}
|
||||
return h + digs.join('')
|
||||
} else {
|
||||
return head + digs.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// `a` is an order key or null (START).
|
||||
// `b` is an order key or null (END).
|
||||
// `a < b` lexicographically if both are non-null.
|
||||
// digits is a string such as '0123456789' for base 10. Digits must be in
|
||||
// ascending character code order!
|
||||
/**
|
||||
* @param {string | null | undefined} a
|
||||
* @param {string | null | undefined} b
|
||||
* @param {string=} digits
|
||||
* @return {string}
|
||||
*/
|
||||
export function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
||||
if (a != null) {
|
||||
validateOrderKey(a, digits)
|
||||
}
|
||||
if (b != null) {
|
||||
validateOrderKey(b, digits)
|
||||
}
|
||||
if (a != null && b != null && a >= b) {
|
||||
throw new Error(a + ' >= ' + b)
|
||||
}
|
||||
if (a == null) {
|
||||
if (b == null) {
|
||||
return 'a' + digits[0]
|
||||
}
|
||||
|
||||
const ib = getIntegerPart(b)
|
||||
const fb = b.slice(ib.length)
|
||||
if (ib === 'A' + digits[0].repeat(26)) {
|
||||
return ib + midpoint('', fb, digits)
|
||||
}
|
||||
if (ib < b) {
|
||||
return ib
|
||||
}
|
||||
const res = decrementInteger(ib, digits)
|
||||
if (res == null) {
|
||||
throw new Error('cannot decrement any more')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
if (b == null) {
|
||||
const ia = getIntegerPart(a)
|
||||
const fa = a.slice(ia.length)
|
||||
const i = incrementInteger(ia, digits)
|
||||
return i == null ? ia + midpoint(fa, null, digits) : i
|
||||
}
|
||||
|
||||
const ia = getIntegerPart(a)
|
||||
const fa = a.slice(ia.length)
|
||||
const ib = getIntegerPart(b)
|
||||
const fb = b.slice(ib.length)
|
||||
if (ia === ib) {
|
||||
return ia + midpoint(fa, fb, digits)
|
||||
}
|
||||
const i = incrementInteger(ia, digits)
|
||||
if (i == null) {
|
||||
throw new Error('cannot increment any more')
|
||||
}
|
||||
if (i < b) {
|
||||
return i
|
||||
}
|
||||
return ia + midpoint(fa, null, digits)
|
||||
}
|
||||
|
||||
/**
|
||||
* same preconditions as generateKeysBetween.
|
||||
* n >= 0.
|
||||
* Returns an array of n distinct keys in sorted order.
|
||||
* If a and b are both null, returns [a0, a1, ...]
|
||||
* If one or the other is null, returns consecutive "integer"
|
||||
* keys. Otherwise, returns relatively short keys between
|
||||
* a and b.
|
||||
* @param {string | null | undefined} a
|
||||
* @param {string | null | undefined} b
|
||||
* @param {number} n
|
||||
* @param {string} digits
|
||||
* @return {string[]}
|
||||
*/
|
||||
export function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
|
||||
if (n === 0) {
|
||||
return []
|
||||
}
|
||||
if (n === 1) {
|
||||
return [generateKeyBetween(a, b, digits)]
|
||||
}
|
||||
if (b == null) {
|
||||
let c = generateKeyBetween(a, b, digits)
|
||||
const result = [c]
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
c = generateKeyBetween(c, b, digits)
|
||||
result.push(c)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if (a == null) {
|
||||
let c = generateKeyBetween(a, b, digits)
|
||||
const result = [c]
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
c = generateKeyBetween(a, c, digits)
|
||||
result.push(c)
|
||||
}
|
||||
result.reverse()
|
||||
return result
|
||||
}
|
||||
const mid = Math.floor(n / 2)
|
||||
const c = generateKeyBetween(a, b, digits)
|
||||
return [
|
||||
...generateNKeysBetween(a, c, mid, digits),
|
||||
c,
|
||||
...generateNKeysBetween(c, b, n - mid - 1, digits),
|
||||
]
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
import type { BeforeChangeHook, CollectionConfig } from '../../collections/config/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { Endpoint, PayloadHandler, SanitizedConfig } from '../types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { traverseFields } from '../../utilities/traverseFields.js'
|
||||
import { generateKeyBetween, generateNKeysBetween } from './fractional-indexing.js'
|
||||
|
||||
/**
|
||||
* This function creates:
|
||||
* - N fields per collection, named `_order` or `_<collection>_<joinField>_order`
|
||||
* - 1 hook per collection
|
||||
* - 1 endpoint per app
|
||||
*
|
||||
* Also, if collection.defaultSort or joinField.defaultSort is not set, it will be set to the orderable field.
|
||||
*/
|
||||
export const setupOrderable = (config: SanitizedConfig) => {
|
||||
const fieldsToAdd = new Map<CollectionConfig, string[]>()
|
||||
|
||||
config.collections.forEach((collection) => {
|
||||
if (collection.orderable) {
|
||||
const currentFields = fieldsToAdd.get(collection) || []
|
||||
fieldsToAdd.set(collection, [...currentFields, '_order'])
|
||||
collection.defaultSort = collection.defaultSort ?? '_order'
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
callback: ({ field, parentRef, ref }) => {
|
||||
if (field.type === 'array' || field.type === 'blocks') {
|
||||
return false
|
||||
}
|
||||
if (field.type === 'group' || field.type === 'tab') {
|
||||
// @ts-expect-error ref is untyped
|
||||
const parentPrefix = parentRef?.prefix ? `${parentRef.prefix}_` : ''
|
||||
// @ts-expect-error ref is untyped
|
||||
ref.prefix = `${parentPrefix}${field.name}`
|
||||
}
|
||||
if (field.type === 'join' && field.orderable === true) {
|
||||
if (Array.isArray(field.collection)) {
|
||||
throw new Error('Orderable joins must target a single collection')
|
||||
}
|
||||
const relationshipCollection = config.collections.find((c) => c.slug === field.collection)
|
||||
if (!relationshipCollection) {
|
||||
return false
|
||||
}
|
||||
field.defaultSort = field.defaultSort ?? `_${field.collection}_${field.name}_order`
|
||||
const currentFields = fieldsToAdd.get(relationshipCollection) || []
|
||||
// @ts-expect-error ref is untyped
|
||||
const prefix = parentRef?.prefix ? `${parentRef.prefix}_` : ''
|
||||
fieldsToAdd.set(relationshipCollection, [
|
||||
...currentFields,
|
||||
`_${field.collection}_${prefix}${field.name}_order`,
|
||||
])
|
||||
}
|
||||
},
|
||||
fields: collection.fields,
|
||||
})
|
||||
})
|
||||
|
||||
Array.from(fieldsToAdd.entries()).forEach(([collection, orderableFields]) => {
|
||||
addOrderableFieldsAndHook(collection, orderableFields)
|
||||
})
|
||||
|
||||
if (fieldsToAdd.size > 0) {
|
||||
addOrderableEndpoint(config)
|
||||
}
|
||||
}
|
||||
|
||||
export const addOrderableFieldsAndHook = (
|
||||
collection: CollectionConfig,
|
||||
orderableFieldNames: string[],
|
||||
) => {
|
||||
// 1. Add field
|
||||
orderableFieldNames.forEach((orderableFieldName) => {
|
||||
const orderField: Field = {
|
||||
name: orderableFieldName,
|
||||
type: 'text',
|
||||
admin: {
|
||||
disableBulkEdit: true,
|
||||
disabled: true,
|
||||
disableListColumn: true,
|
||||
disableListFilter: true,
|
||||
hidden: true,
|
||||
readOnly: true,
|
||||
},
|
||||
index: true,
|
||||
required: true,
|
||||
// override the schema to make order fields optional for payload.create()
|
||||
typescriptSchema: [
|
||||
() => ({
|
||||
type: 'string',
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
unique: true,
|
||||
}
|
||||
|
||||
collection.fields.unshift(orderField)
|
||||
})
|
||||
|
||||
// 2. Add hook
|
||||
if (!collection.hooks) {
|
||||
collection.hooks = {}
|
||||
}
|
||||
if (!collection.hooks.beforeChange) {
|
||||
collection.hooks.beforeChange = []
|
||||
}
|
||||
|
||||
const orderBeforeChangeHook: BeforeChangeHook = async ({ data, operation, req }) => {
|
||||
// Only set _order on create, not on update (unless explicitly provided)
|
||||
if (operation === 'create') {
|
||||
for (const orderableFieldName of orderableFieldNames) {
|
||||
if (!data[orderableFieldName]) {
|
||||
const lastDoc = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
select: { [orderableFieldName]: true },
|
||||
sort: `-${orderableFieldName}`,
|
||||
})
|
||||
|
||||
const lastOrderValue = lastDoc.docs[0]?.[orderableFieldName] || null
|
||||
data[orderableFieldName] = generateKeyBetween(lastOrderValue, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
collection.hooks.beforeChange.push(orderBeforeChangeHook)
|
||||
}
|
||||
|
||||
/**
|
||||
* The body of the reorder endpoint.
|
||||
* @internal
|
||||
*/
|
||||
export type OrderableEndpointBody = {
|
||||
collectionSlug: string
|
||||
docsToMove: string[]
|
||||
newKeyWillBe: 'greater' | 'less'
|
||||
orderableFieldName: string
|
||||
target: {
|
||||
id: string
|
||||
key: string
|
||||
}
|
||||
}
|
||||
|
||||
export const addOrderableEndpoint = (config: SanitizedConfig) => {
|
||||
// 3. Add endpoint
|
||||
const reorderHandler: PayloadHandler = async (req) => {
|
||||
const body = (await req.json?.()) as OrderableEndpointBody
|
||||
|
||||
const { collectionSlug, docsToMove, newKeyWillBe, orderableFieldName, target } = body
|
||||
|
||||
if (!Array.isArray(docsToMove) || docsToMove.length === 0) {
|
||||
return new Response(JSON.stringify({ error: 'docsToMove must be a non-empty array' }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
if (
|
||||
typeof target !== 'object' ||
|
||||
typeof target.id !== 'string' ||
|
||||
typeof target.key !== 'string'
|
||||
) {
|
||||
return new Response(JSON.stringify({ error: 'target must be an object with id and key' }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
if (newKeyWillBe !== 'greater' && newKeyWillBe !== 'less') {
|
||||
return new Response(JSON.stringify({ error: 'newKeyWillBe must be "greater" or "less"' }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
const collection = config.collections.find((c) => c.slug === collectionSlug)
|
||||
if (!collection) {
|
||||
return new Response(JSON.stringify({ error: `Collection ${collectionSlug} not found` }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
if (typeof orderableFieldName !== 'string') {
|
||||
return new Response(JSON.stringify({ error: 'orderableFieldName must be a string' }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
// Prevent reordering if user doesn't have editing permissions
|
||||
if (collection.access?.update) {
|
||||
await executeAccess(
|
||||
{
|
||||
// Currently only one doc can be moved at a time. We should review this if we want to allow
|
||||
// multiple docs to be moved at once in the future.
|
||||
id: docsToMove[0],
|
||||
data: {},
|
||||
req,
|
||||
},
|
||||
collection.access.update,
|
||||
)
|
||||
}
|
||||
|
||||
const targetId = target.id
|
||||
let targetKey = target.key
|
||||
|
||||
// If targetKey = pending, we need to find its current key.
|
||||
// This can only happen if the user reorders rows quickly with a slow connection.
|
||||
if (targetKey === 'pending') {
|
||||
const beforeDoc = await req.payload.findByID({
|
||||
id: targetId,
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
select: { [orderableFieldName]: true },
|
||||
})
|
||||
targetKey = beforeDoc?.[orderableFieldName] || null
|
||||
}
|
||||
|
||||
// The reason the endpoint does not receive this docId as an argument is that there
|
||||
// are situations where the user may not see or know what the next or previous one is. For
|
||||
// example, access control restrictions, if docBefore is the last one on the page, etc.
|
||||
const adjacentDoc = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
select: { [orderableFieldName]: true },
|
||||
sort: newKeyWillBe === 'greater' ? orderableFieldName : `-${orderableFieldName}`,
|
||||
where: {
|
||||
[orderableFieldName]: {
|
||||
[newKeyWillBe === 'greater' ? 'greater_than' : 'less_than']: targetKey,
|
||||
},
|
||||
},
|
||||
})
|
||||
const adjacentDocKey = adjacentDoc.docs?.[0]?.[orderableFieldName] || null
|
||||
|
||||
// Currently N (= docsToMove.length) is always 1. Maybe in the future we will
|
||||
// allow dragging and reordering multiple documents at once via the UI.
|
||||
const orderValues =
|
||||
newKeyWillBe === 'greater'
|
||||
? generateNKeysBetween(targetKey, adjacentDocKey, docsToMove.length)
|
||||
: generateNKeysBetween(adjacentDocKey, targetKey, docsToMove.length)
|
||||
|
||||
// Update each document with its new order value
|
||||
for (const [index, id] of docsToMove.entries()) {
|
||||
await req.payload.update({
|
||||
id,
|
||||
collection: collection.slug,
|
||||
data: {
|
||||
[orderableFieldName]: orderValues[index],
|
||||
},
|
||||
depth: 0,
|
||||
req,
|
||||
select: { id: true },
|
||||
})
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ orderValues, success: true }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
|
||||
const reorderEndpoint: Endpoint = {
|
||||
handler: reorderHandler,
|
||||
method: 'post',
|
||||
path: '/reorder',
|
||||
}
|
||||
|
||||
if (!config.endpoints) {
|
||||
config.endpoints = []
|
||||
}
|
||||
config.endpoints.push(reorderEndpoint)
|
||||
}
|
||||
@@ -36,7 +36,6 @@ import { getDefaultJobsCollection, jobsCollectionSlug } from '../queues/config/i
|
||||
import { flattenBlock } from '../utilities/flattenAllFields.js'
|
||||
import { getSchedulePublishTask } from '../versions/schedule/job.js'
|
||||
import { addDefaultsToConfig } from './defaults.js'
|
||||
import { setupOrderable } from './orderable/index.js'
|
||||
|
||||
const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig> => {
|
||||
const sanitizedConfig = { ...configToSanitize }
|
||||
@@ -109,9 +108,6 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
|
||||
|
||||
const config: Partial<SanitizedConfig> = sanitizeAdminConfig(configWithDefaults)
|
||||
|
||||
// Add orderable fields
|
||||
setupOrderable(config as SanitizedConfig)
|
||||
|
||||
if (!config.endpoints) {
|
||||
config.endpoints = []
|
||||
}
|
||||
|
||||
@@ -793,7 +793,9 @@ export type Config = {
|
||||
/** Global date format that will be used for all dates in the Admin panel. Any valid date-fns format pattern can be used. */
|
||||
dateFormat?: string
|
||||
/**
|
||||
* Each entry in this map generates an entry in the importMap.
|
||||
* Each entry in this map generates an entry in the importMap,
|
||||
* as well as an entry in the componentMap if the type of the
|
||||
* dependency is 'component'
|
||||
*/
|
||||
dependencies?: AdminDependencies
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { BaseJob, DatabaseAdapter } from '../index.js'
|
||||
import type { UpdateJobs } from './types.js'
|
||||
|
||||
import { jobsCollectionSlug } from '../queues/config/index.js'
|
||||
import { sanitizeUpdateData } from '../queues/utilities/sanitizeUpdateData.js'
|
||||
|
||||
export const defaultUpdateJobs: UpdateJobs = async function updateMany(
|
||||
this: DatabaseAdapter,
|
||||
@@ -41,7 +42,7 @@ export const defaultUpdateJobs: UpdateJobs = async function updateMany(
|
||||
const updatedJob = await this.updateOne({
|
||||
id: job.id,
|
||||
collection: jobsCollectionSlug,
|
||||
data: updateData,
|
||||
data: sanitizeUpdateData({ data: updateData }),
|
||||
req,
|
||||
returning,
|
||||
})
|
||||
|
||||
@@ -642,12 +642,6 @@ export type DatabaseAdapterResult<T = BaseDatabaseAdapter> = {
|
||||
allowIDOnCreate?: boolean
|
||||
defaultIDType: 'number' | 'text'
|
||||
init: (args: { payload: Payload }) => T
|
||||
/**
|
||||
* The name of the database adapter. For example, "postgres" or "mongoose".
|
||||
*
|
||||
* @todo make required in 4.0
|
||||
*/
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type DBIdentifierName =
|
||||
|
||||
@@ -4,7 +4,6 @@ import { en } from '@payloadcms/translations/languages/en'
|
||||
import { status as httpStatus } from 'http-status'
|
||||
|
||||
import type { LabelFunction, StaticLabel } from '../config/types.js'
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
|
||||
import { APIError } from './APIError.js'
|
||||
|
||||
@@ -29,10 +28,6 @@ export class ValidationError extends APIError<{
|
||||
errors: ValidationFieldError[]
|
||||
global?: string
|
||||
id?: number | string
|
||||
/**
|
||||
* req needs to be passed through (if you have one) in order to resolve label functions that may be part of the errors array
|
||||
*/
|
||||
req?: Partial<PayloadRequest>
|
||||
},
|
||||
t?: TFunction,
|
||||
) {
|
||||
@@ -42,36 +37,8 @@ export class ValidationError extends APIError<{
|
||||
? en.translations.error.followingFieldsInvalid_one
|
||||
: en.translations.error.followingFieldsInvalid_other
|
||||
|
||||
const req = results.req
|
||||
// delete to avoid logging the whole req
|
||||
delete results['req']
|
||||
|
||||
super(
|
||||
`${message} ${results.errors
|
||||
.map((f) => {
|
||||
if (f.label) {
|
||||
if (typeof f.label === 'function') {
|
||||
if (!req || !req.i18n || !req.t) {
|
||||
return f.path
|
||||
}
|
||||
|
||||
return f.label({ i18n: req.i18n, t: req.t })
|
||||
}
|
||||
|
||||
if (typeof f.label === 'object') {
|
||||
if (req?.i18n?.language) {
|
||||
return f.label[req.i18n.language]
|
||||
}
|
||||
|
||||
return f.label[Object.keys(f.label)[0]]
|
||||
}
|
||||
|
||||
return f.label
|
||||
}
|
||||
|
||||
return f.path
|
||||
})
|
||||
.join(', ')}`,
|
||||
`${message} ${results.errors.map((f) => f.label || f.path).join(', ')}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
results,
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ import type {
|
||||
EmailFieldLabelServerComponent,
|
||||
FieldDescriptionClientProps,
|
||||
FieldDescriptionServerProps,
|
||||
FieldDiffClientProps,
|
||||
FieldDiffClientComponent,
|
||||
FieldDiffServerProps,
|
||||
GroupFieldClientProps,
|
||||
GroupFieldLabelClientComponent,
|
||||
@@ -326,7 +326,7 @@ type Admin = {
|
||||
components?: {
|
||||
Cell?: PayloadComponent<DefaultServerCellComponentProps, DefaultCellComponentProps>
|
||||
Description?: PayloadComponent<FieldDescriptionServerProps, FieldDescriptionClientProps>
|
||||
Diff?: PayloadComponent<FieldDiffServerProps, FieldDiffClientProps>
|
||||
Diff?: PayloadComponent<FieldDiffServerProps, FieldDiffClientComponent>
|
||||
Field?: PayloadComponent<FieldClientComponent | FieldServerComponent>
|
||||
/**
|
||||
* The Filter component has to be a client component
|
||||
@@ -1549,17 +1549,6 @@ export type JoinField = {
|
||||
* A string for the field in the collection being joined to.
|
||||
*/
|
||||
on: string
|
||||
/**
|
||||
* If true, enables custom ordering for the collection with the relationship, and joined documents can be reordered via drag and drop.
|
||||
* New documents are inserted at the end of the list according to this parameter.
|
||||
*
|
||||
* Under the hood, a field with {@link https://observablehq.com/@dgreensp/implementing-fractional-indexing|fractional indexing} is used to optimize inserts and reorderings.
|
||||
*
|
||||
* @default false
|
||||
*
|
||||
* @experimental There may be frequent breaking changes to this API
|
||||
*/
|
||||
orderable?: boolean
|
||||
sanitizedMany?: JoinField[]
|
||||
type: 'join'
|
||||
validate?: never
|
||||
@@ -1573,15 +1562,7 @@ export type JoinFieldClient = {
|
||||
} & { targetField: Pick<RelationshipFieldClient, 'relationTo'> } & FieldBaseClient &
|
||||
Pick<
|
||||
JoinField,
|
||||
| 'collection'
|
||||
| 'defaultLimit'
|
||||
| 'defaultSort'
|
||||
| 'index'
|
||||
| 'maxDepth'
|
||||
| 'on'
|
||||
| 'orderable'
|
||||
| 'type'
|
||||
| 'where'
|
||||
'collection' | 'defaultLimit' | 'defaultSort' | 'index' | 'maxDepth' | 'on' | 'type' | 'where'
|
||||
>
|
||||
|
||||
export type FlattenedBlock = {
|
||||
|
||||
@@ -77,7 +77,6 @@ export const beforeChange = async <T extends JsonObject>({
|
||||
collection: collection?.slug,
|
||||
errors,
|
||||
global: global?.slug,
|
||||
req,
|
||||
},
|
||||
req.t,
|
||||
)
|
||||
|
||||
@@ -65,11 +65,8 @@ import type {
|
||||
} from './types/index.js'
|
||||
import type { TraverseFieldsCallback } from './utilities/traverseFields.js'
|
||||
export type * from './admin/types.js'
|
||||
import type { SupportedLanguages } from '@payloadcms/translations'
|
||||
|
||||
import { Cron } from 'croner'
|
||||
|
||||
import type { ClientConfig } from './config/client.js'
|
||||
import type { TypeWithVersion } from './versions/types.js'
|
||||
|
||||
import { decrypt, encrypt } from './auth/crypto.js'
|
||||
@@ -868,12 +865,10 @@ export const reload = async (
|
||||
}
|
||||
|
||||
await payload.db.init()
|
||||
|
||||
if (payload.db.connect) {
|
||||
await payload.db.connect({ hotReload: true })
|
||||
}
|
||||
|
||||
global._payload_clientConfigs = {} as Record<keyof SupportedLanguages, ClientConfig>
|
||||
global._payload_clientConfig = null
|
||||
global._payload_schemaMap = null
|
||||
global._payload_clientSchemaMap = null
|
||||
global._payload_doNotCacheClientConfig = true // This will help refreshing the client config cache more reliably. If you remove this, please test HMR + client config refreshing (do new fields appear in the document?)
|
||||
@@ -1090,7 +1085,6 @@ export {
|
||||
} from './config/client.js'
|
||||
|
||||
export { defaults } from './config/defaults.js'
|
||||
export { type OrderableEndpointBody } from './config/orderable/index.js'
|
||||
export { sanitizeConfig } from './config/sanitize.js'
|
||||
export type * from './config/types.js'
|
||||
export { combineQueries } from './database/combineQueries.js'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CollectionConfig } from '../../../index.js'
|
||||
import type { Payload, PayloadRequest, Sort } from '../../../types/index.js'
|
||||
import type { RunJobsArgs } from '../../operations/runJobs/index.js'
|
||||
import type { Payload, PayloadRequest } from '../../../types/index.js'
|
||||
import type { TaskConfig } from './taskTypes.js'
|
||||
import type { WorkflowConfig } from './workflowTypes.js'
|
||||
|
||||
@@ -81,22 +80,6 @@ export type JobsConfig = {
|
||||
* a new collection.
|
||||
*/
|
||||
jobsCollectionOverrides?: (args: { defaultJobsCollection: CollectionConfig }) => CollectionConfig
|
||||
/**
|
||||
* Adjust the job processing order using a Payload sort string. This can be set globally or per queue.
|
||||
*
|
||||
* FIFO would equal `createdAt` and LIFO would equal `-createdAt`.
|
||||
*
|
||||
* @default all jobs for all queues will be executed in FIFO order.
|
||||
*/
|
||||
processingOrder?:
|
||||
| ((args: RunJobsArgs) => Promise<Sort> | Sort)
|
||||
| {
|
||||
default?: Sort
|
||||
queues: {
|
||||
[queue: string]: Sort
|
||||
}
|
||||
}
|
||||
| Sort
|
||||
/**
|
||||
* By default, the job system uses direct database calls for optimal performance.
|
||||
* If you added custom hooks to your jobs collection, you can set this to true to
|
||||
|
||||
@@ -18,7 +18,7 @@ export type JobLog = {
|
||||
/**
|
||||
* ID added by the array field when the log is saved in the database
|
||||
*/
|
||||
id: string
|
||||
id?: string
|
||||
input?: Record<string, any>
|
||||
output?: Record<string, any>
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type Payload,
|
||||
type PayloadRequest,
|
||||
type RunningJob,
|
||||
type Sort,
|
||||
type TypedJobs,
|
||||
type Where,
|
||||
} from '../index.js'
|
||||
@@ -100,19 +99,8 @@ export const getJobsLocalAPI = (payload: Payload) => ({
|
||||
run: async (args?: {
|
||||
limit?: number
|
||||
overrideAccess?: boolean
|
||||
/**
|
||||
* Adjust the job processing order using a Payload sort string.
|
||||
*
|
||||
* FIFO would equal `createdAt` and LIFO would equal `-createdAt`.
|
||||
*/
|
||||
processingOrder?: Sort
|
||||
queue?: string
|
||||
req?: PayloadRequest
|
||||
/**
|
||||
* By default, jobs are run in parallel.
|
||||
* If you want to run them in sequence, set this to true.
|
||||
*/
|
||||
sequential?: boolean
|
||||
where?: Where
|
||||
}): Promise<ReturnType<typeof runJobs>> => {
|
||||
const newReq: PayloadRequest = args?.req ?? (await createLocalReq({}, payload))
|
||||
@@ -120,10 +108,8 @@ export const getJobsLocalAPI = (payload: Payload) => ({
|
||||
return await runJobs({
|
||||
limit: args?.limit,
|
||||
overrideAccess: args?.overrideAccess !== false,
|
||||
processingOrder: args?.processingOrder,
|
||||
queue: args?.queue,
|
||||
req: newReq,
|
||||
sequential: args?.sequential,
|
||||
where: args?.where,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-strict-ignore
|
||||
import type { PayloadRequest, Sort, Where } from '../../../types/index.js'
|
||||
import type { PaginatedDocs } from '../../../database/types.js'
|
||||
import type { PayloadRequest, Where } from '../../../types/index.js'
|
||||
import type { WorkflowJSON } from '../../config/types/workflowJSONTypes.js'
|
||||
import type {
|
||||
BaseJob,
|
||||
@@ -25,21 +26,8 @@ export type RunJobsArgs = {
|
||||
id?: number | string
|
||||
limit?: number
|
||||
overrideAccess?: boolean
|
||||
/**
|
||||
* Adjust the job processing order
|
||||
*
|
||||
* FIFO would equal `createdAt` and LIFO would equal `-createdAt`.
|
||||
*
|
||||
* @default all jobs for all queues will be executed in FIFO order.
|
||||
*/
|
||||
processingOrder?: Sort
|
||||
queue?: string
|
||||
req: PayloadRequest
|
||||
/**
|
||||
* By default, jobs are run in parallel.
|
||||
* If you want to run them in sequence, set this to true.
|
||||
*/
|
||||
sequential?: boolean
|
||||
where?: Where
|
||||
}
|
||||
|
||||
@@ -55,18 +43,14 @@ export type RunJobsResult = {
|
||||
remainingJobsFromQueried: number
|
||||
}
|
||||
|
||||
export const runJobs = async (args: RunJobsArgs): Promise<RunJobsResult> => {
|
||||
const {
|
||||
id,
|
||||
limit = 10,
|
||||
overrideAccess,
|
||||
processingOrder,
|
||||
queue,
|
||||
req,
|
||||
sequential,
|
||||
where: whereFromProps,
|
||||
} = args
|
||||
|
||||
export const runJobs = async ({
|
||||
id,
|
||||
limit = 10,
|
||||
overrideAccess,
|
||||
queue,
|
||||
req,
|
||||
where: whereFromProps,
|
||||
}: RunJobsArgs): Promise<RunJobsResult> => {
|
||||
if (!overrideAccess) {
|
||||
const hasAccess = await req.payload.config.jobs.access.run({ req })
|
||||
if (!hasAccess) {
|
||||
@@ -140,21 +124,6 @@ export const runJobs = async (args: RunJobsArgs): Promise<RunJobsResult> => {
|
||||
}),
|
||||
]
|
||||
} else {
|
||||
let defaultProcessingOrder: Sort =
|
||||
req.payload.collections[jobsCollectionSlug].config.defaultSort ?? 'createdAt'
|
||||
|
||||
const processingOrderConfig = req.payload.config.jobs?.processingOrder
|
||||
if (typeof processingOrderConfig === 'function') {
|
||||
defaultProcessingOrder = await processingOrderConfig(args)
|
||||
} else if (typeof processingOrderConfig === 'object' && !Array.isArray(processingOrderConfig)) {
|
||||
if (queue && processingOrderConfig.queues && processingOrderConfig.queues[queue]) {
|
||||
defaultProcessingOrder = processingOrderConfig.queues[queue]
|
||||
} else if (processingOrderConfig.default) {
|
||||
defaultProcessingOrder = processingOrderConfig.default
|
||||
}
|
||||
} else if (typeof processingOrderConfig === 'string') {
|
||||
defaultProcessingOrder = processingOrderConfig
|
||||
}
|
||||
const updatedDocs = await updateJobs({
|
||||
data: {
|
||||
processing: true,
|
||||
@@ -164,7 +133,6 @@ export const runJobs = async (args: RunJobsArgs): Promise<RunJobsResult> => {
|
||||
limit,
|
||||
req,
|
||||
returning: true,
|
||||
sort: processingOrder ?? defaultProcessingOrder,
|
||||
where,
|
||||
})
|
||||
|
||||
@@ -207,7 +175,7 @@ export const runJobs = async (args: RunJobsArgs): Promise<RunJobsResult> => {
|
||||
? []
|
||||
: undefined
|
||||
|
||||
const runSingleJob = async (job) => {
|
||||
const jobPromises = jobsQuery.docs.map(async (job) => {
|
||||
if (!job.workflowSlug && !job.taskSlug) {
|
||||
throw new Error('Job must have either a workflowSlug or a taskSlug')
|
||||
}
|
||||
@@ -289,20 +257,9 @@ export const runJobs = async (args: RunJobsArgs): Promise<RunJobsResult> => {
|
||||
|
||||
return { id: job.id, result }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let resultsArray: { id: number | string; result: RunJobResult }[] = []
|
||||
if (sequential) {
|
||||
for (const job of jobsQuery.docs) {
|
||||
const result = await runSingleJob(job)
|
||||
if (result !== null) {
|
||||
resultsArray.push(result)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const jobPromises = jobsQuery.docs.map(runSingleJob)
|
||||
resultsArray = await Promise.all(jobPromises)
|
||||
}
|
||||
const resultsArray = await Promise.all(jobPromises)
|
||||
|
||||
if (jobsToDelete && jobsToDelete.length > 0) {
|
||||
try {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user