Compare commits

..

2 Commits

Author SHA1 Message Date
Alessio Gravili
16170b84f1 remove diff 2025-03-25 12:44:52 -06:00
Alessio Gravili
300cb0d7e7 perf(drizzle): 35x faster initial update when running jobs 2025-03-25 12:32:03 -06:00
320 changed files with 4760 additions and 19362 deletions

View File

@@ -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

View File

@@ -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. |

View File

@@ -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: {

View File

@@ -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

View File

@@ -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._

View File

@@ -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

View File

@@ -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. |

View File

@@ -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
})
```

View File

@@ -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>

View File

@@ -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
}),

View File

@@ -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'

View File

@@ -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",

View File

@@ -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": {

View File

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

View File

@@ -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": {

View File

@@ -273,7 +273,6 @@ export function mongooseAdapter({
}
return {
name: 'mongoose',
allowIDOnCreate,
defaultIDType: 'text',
init: adapter,

View File

@@ -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 ?? {}, {

View File

@@ -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 })

View File

@@ -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",

View File

@@ -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

View File

@@ -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'

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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'

View File

@@ -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",

View File

@@ -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

View File

@@ -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'

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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 }) => {

View File

@@ -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,

View File

@@ -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

View File

@@ -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'

View File

@@ -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,

View File

@@ -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',

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -19,7 +19,7 @@ type Args = {
aliasTable?: Table
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale?: string
locale: string
parentIsLocalized: boolean
selectFields: Record<string, GenericColumn>
selectLocale?: boolean

View File

@@ -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>>
})
}
}

View File

@@ -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'

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -423,7 +423,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
path: fieldName,
},
],
req,
},
req?.t,
)

View File

@@ -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,
})

View File

@@ -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": {

View File

@@ -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": {

View File

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

View File

@@ -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 },
},
})

View File

@@ -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,

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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

View File

@@ -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>,

View File

@@ -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

View File

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

View File

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

View File

@@ -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 theyre 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)
})
})

View File

@@ -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 doesnt 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:"

View File

@@ -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: {

View File

@@ -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()

View File

@@ -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>
)

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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',
},
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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<

View File

@@ -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

View File

@@ -60,7 +60,6 @@ export type BuildTableStateArgs = {
columns?: ColumnPreference[]
docs?: PaginatedDocs['docs']
enableRowSelections?: boolean
orderableFieldName: string
parent?: {
collectionSlug: CollectionSlug
id: number | string

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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}`)
}

View File

@@ -1,5 +1,4 @@
// @ts-strict-ignore
import type { Config, SanitizedConfig } from '../../config/types.js'
import type {
CollectionConfig,

View File

@@ -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

View File

@@ -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),
]
}

View File

@@ -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)
}

View File

@@ -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 = []
}

View File

@@ -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
/**

View File

@@ -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,
})

View File

@@ -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 =

View File

@@ -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,
)

View File

@@ -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 = {

View File

@@ -77,7 +77,6 @@ export const beforeChange = async <T extends JsonObject>({
collection: collection?.slug,
errors,
global: global?.slug,
req,
},
req.t,
)

View File

@@ -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'

View File

@@ -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

View File

@@ -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>
/**

View File

@@ -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,
})
},

View File

@@ -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