164 Commits

Author SHA1 Message Date
Jessica Rynkar e4742054db fix: stricter input validation (#15868)
Misc input validation improvements, sanitizing path segments in both SQL
and JSON queries, standardizing the processing of column and JSON paths
across different adapters, and making adjustments to traversal and alias
generation to align behavior across components.
2026-03-16 10:48:24 +00:00
Sasha dc049fe00f fix: run sanitizeWhereQuery for join query access result (#15891)
This PR properly runs `sanitizeWhereQuery` for joined collections which
is important specifically for virtual relationship fields path
transformation.

Fixes https://github.com/payloadcms/payload/issues/15332
2026-03-10 17:04:33 -04:00
Jessica Rynkar 08226db60c fix: throw error for unknown query operators (#15739)
### What
Ensures unknown query operators are properly rejected during query
validation.

### Why
Previously, unrecognized operators in where clauses were silently
ignored. This could lead to unexpected behavior. Query validation should
fail-closed and only accept known operators.

### How
Added an `else` in validateQueryPaths.ts to push an error for any
operator not in the valid operator set.

Test added to `joins > int`
2026-02-24 14:54:50 +00:00
Sasha 0935824011 feat(db-*): add customID arg to db.create (#15653)
This PR adds `customID` argument to `payload.db.create` which can be
used to ensure that a document is created with the provided ID, for
example:
```ts
payload.db.create({ collection: 'posts', customID: 'ce98d6c4-c3ab-45de-9dfc-bf33d94cc941', data: { } })
```
This does not require to have a custom ID field in `posts` collection,
as the following:
```ts
payload.db.create({ collection: 'posts', data: { customID: 'ce98d6c4-c3ab-45de-9dfc-bf33d94cc941' } })
```
Can be used only when you define a custom ID field
2026-02-17 20:45:13 +02:00
Sasha 1b7b13d178 feat(drizzle): predefined migration for blocksAsJSON: true (#15257)
This PR adds ability to migrate an existing project to use the
`blocksAsJSON` postgres/sqlite property.
https://github.com/payloadcms/payload/pull/12750
Usage: `pnpm payload migrate:create --file
@payloadcms/db-postgres/blocks-as-json`.

Example of a generated migration for the website template:
```ts
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'

import { getBlocksToJsonMigrator } from '@payloadcms/db-postgres/migration-utils'
import { fileURLToPath } from 'url'
import path from 'path'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

// Configure migration options (optional)
const BATCH_SIZE = 100 // Number of entities to process per batch
const TEMP_FOLDER = path.resolve(dirname, '.payload-blocks-migration') // Folder path to store migration batch


export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
  
  const migrator = getBlocksToJsonMigrator(payload)
  migrator.setTempFolder(TEMP_FOLDER)
  await migrator.collectAndSaveEntitiesToBatches(req, { batchSize: BATCH_SIZE })
  
  await db.execute(sql`
   ALTER TABLE "pages_blocks_cta_links" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "pages_blocks_cta" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "pages_blocks_content_columns" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "pages_blocks_content" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "pages_blocks_media_block" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "pages_blocks_archive" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "pages_blocks_form_block" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "_pages_v_blocks_cta_links" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "_pages_v_blocks_cta" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "_pages_v_blocks_content_columns" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "_pages_v_blocks_content" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "_pages_v_blocks_media_block" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "_pages_v_blocks_archive" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "_pages_v_blocks_form_block" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_checkbox" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_country" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_email" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_message" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_number" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_select_options" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_select" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_state" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_text" DISABLE ROW LEVEL SECURITY;
  ALTER TABLE "forms_blocks_textarea" DISABLE ROW LEVEL SECURITY;
  DROP TABLE "pages_blocks_cta_links" CASCADE;
  DROP TABLE "pages_blocks_cta" CASCADE;
  DROP TABLE "pages_blocks_content_columns" CASCADE;
  DROP TABLE "pages_blocks_content" CASCADE;
  DROP TABLE "pages_blocks_media_block" CASCADE;
  DROP TABLE "pages_blocks_archive" CASCADE;
  DROP TABLE "pages_blocks_form_block" CASCADE;
  DROP TABLE "_pages_v_blocks_cta_links" CASCADE;
  DROP TABLE "_pages_v_blocks_cta" CASCADE;
  DROP TABLE "_pages_v_blocks_content_columns" CASCADE;
  DROP TABLE "_pages_v_blocks_content" CASCADE;
  DROP TABLE "_pages_v_blocks_media_block" CASCADE;
  DROP TABLE "_pages_v_blocks_archive" CASCADE;
  DROP TABLE "_pages_v_blocks_form_block" CASCADE;
  DROP TABLE "forms_blocks_checkbox" CASCADE;
  DROP TABLE "forms_blocks_country" CASCADE;
  DROP TABLE "forms_blocks_email" CASCADE;
  DROP TABLE "forms_blocks_message" CASCADE;
  DROP TABLE "forms_blocks_number" CASCADE;
  DROP TABLE "forms_blocks_select_options" CASCADE;
  DROP TABLE "forms_blocks_select" CASCADE;
  DROP TABLE "forms_blocks_state" CASCADE;
  DROP TABLE "forms_blocks_text" CASCADE;
  DROP TABLE "forms_blocks_textarea" CASCADE;
  ALTER TABLE "pages_rels" DROP CONSTRAINT "pages_rels_categories_fk";
  
  ALTER TABLE "_pages_v_rels" DROP CONSTRAINT "_pages_v_rels_categories_fk";
  
  DROP INDEX "pages_rels_categories_id_idx";
  DROP INDEX "_pages_v_rels_categories_id_idx";
  ALTER TABLE "pages" ADD COLUMN "layout" jsonb;
  ALTER TABLE "_pages_v" ADD COLUMN "version_layout" jsonb;
  ALTER TABLE "forms" ADD COLUMN "fields" jsonb;
  ALTER TABLE "pages_rels" DROP COLUMN "categories_id";
  ALTER TABLE "_pages_v_rels" DROP COLUMN "categories_id";
  DROP TYPE "public"."enum_pages_blocks_cta_links_link_type";
  DROP TYPE "public"."enum_pages_blocks_cta_links_link_appearance";
  DROP TYPE "public"."enum_pages_blocks_content_columns_size";
  DROP TYPE "public"."enum_pages_blocks_content_columns_link_type";
  DROP TYPE "public"."enum_pages_blocks_content_columns_link_appearance";
  DROP TYPE "public"."enum_pages_blocks_archive_populate_by";
  DROP TYPE "public"."enum_pages_blocks_archive_relation_to";
  DROP TYPE "public"."enum__pages_v_blocks_cta_links_link_type";
  DROP TYPE "public"."enum__pages_v_blocks_cta_links_link_appearance";
  DROP TYPE "public"."enum__pages_v_blocks_content_columns_size";
  DROP TYPE "public"."enum__pages_v_blocks_content_columns_link_type";
  DROP TYPE "public"."enum__pages_v_blocks_content_columns_link_appearance";
  DROP TYPE "public"."enum__pages_v_blocks_archive_populate_by";
  DROP TYPE "public"."enum__pages_v_blocks_archive_relation_to";`)
  payload.logger.info("Executed blocks to JSON migration statements.")
  
  await migrator.migrateEntitiesFromTempFolder(req, { clearBatches: true })
    
}

export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
    // Migration code
}
```

The migration when created will automatically update the payload config
file (if one is found) to insert `blocksAsJSON: true`.
Additionally, extends the predefined migration API with `dynamic` to
dynamically build the migration file.
2026-01-23 15:38:50 -05:00
Jarrod Flesch d77af00614 feat: adds experimental option localizeStatus and allows unpublish per-locale functionality (#14667)
## Localized Status (Experimental) 

This PR introduces a new **experimental** option that allows each locale
to track and manage its own publication status independently, for
collection docs and globals.

### Configuration
To enable this feature, you need **two** configurations:

1. Enable the experimental flag in your `payload.config`:

```ts
experimental: {
  localizeStatus: true, // default: `false`
}
```

2. Enable it on specific collections/globals:
```ts
export const Posts: CollectionConfig = {
  slug: 'posts',
  versions: {
    drafts: {
      localizeStatus: true, // default: false
    },
  },
  // ...
}
```

## Key Changes

When enabled:

- **Per-locale status tracking** - Each locale maintains its own
published/draft status
- **Independent publishing** - Publish or unpublish individual locales
without affecting others
- **Locale-aware UI** - Admin panel shows status for the currently
active locale
- **Localized version history** - Versions list reflects the current
locale's status

## Improved Behavior
- Creating a document in one locale sets that locale to the specified
status and other locales default to draft
- You can publish/unpublish specific locales independently
- Collection list views show status for the currently active locale
- Document edit view displays the current locale's status

## Migration Required (If you already have version data)

> If this is a new project then you only need to enable the flags. If
you have existing data you will want to continue with the migration
guide below.

⚠️ Breaking Change: When `localizeStatus` is enabled, existing `_status`
fields will need to be migrated from strings to locale objects.

### ➡️ Step 1
Before doing anything, **take a backup of your current database**.

### ➡️ Step 2
Stop your dev server if it is running

### ➡️  Step 3
**Create migration file**
```ts
// run the following to create a blank
// migration file named `localize_status`
payload migrate:create localize_status
```

**Add migration code**

🔵 **PostgreSQL / SQLite**:
```ts
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-postgres'
import { sql } from '@payloadcms/db-postgres'
import { localizeStatus } from 'payload/migrations'

export async function up({ db, payload }: MigrateUpArgs): Promise<void> {
  await localizeStatus.up({
    collectionSlug: 'posts', // 👈 Change to your collection
    db,
    payload,
    sql,
  })
}

export async function down({ db, payload }: MigrateDownArgs): Promise<void> {
  await localizeStatus.down({
    collectionSlug: 'posts', // 👈 Change to your collection  
    db,
    payload,
    sql,
  })
}
```

🟢  **MongoDB**:
```ts
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-mongodb'
import { localizeStatus } from 'payload/migrations'

export async function up({ payload }: MigrateUpArgs): Promise<void> {
  await localizeStatus.up({
    collectionSlug: 'posts', // 👈 Change to your collection
    payload,
  })
}

export async function down({ payload }: MigrateDownArgs): Promise<void> {
  await localizeStatus.down({
    collectionSlug: 'posts', // 👈 Change to your collection
    payload,
  })
}
```

### ➡️ Step 4
Run the migration
```ts
payload migrate
```

### ➡️ Step 5
Set `localizeStatus: true` 

**Payload Config**
```ts
// payload config file
experimental: {
  localizeStatus: true, // <-- add this
}
```

**Collection Config**
```ts
// collection you want to migrate
export const Posts: CollectionConfig = {
  slug: 'posts',
  versions: {
    drafts: {
      localizeStatus: true, // <-- add this
    },
  },
  // ...
}
```

### ➡️ Step 6
Run `pnpm dev` and test out the feature.
  
## Related PRs in this feature

- [feat: adds versions.drafts.localizeStatus and allows unpublish
per‑locale #14667](https://github.com/payloadcms/payload/pull/14667)
- [feat(ui): experimental localize metadata UI
#14699](https://github.com/payloadcms/payload/pull/14699)
- [chore: localize status migration work
#14862](https://github.com/payloadcms/payload/pull/14862)

---------

Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
Co-authored-by: Jessica Rynkar <67977755+jessrynkar@users.noreply.github.com>
2026-01-16 18:35:16 +00:00
German Jablonski ea15f0039a chore: deprecate skip parameter from db-adapters (#15183)
`skip` is an internal API. It is used only by the database adapters and
is not publicly exposed through the local API, only when accessing the
database directly. It does exactly the same thing as `page`, but with
different semantics. In this PR I am deprecating it because it adds
nothing but complexity. It is used only once in the entire repository
and conflicts with the `page` parameter.
2026-01-13 13:42:15 -05:00
Tobias Odendahl 92da9faade feat: add bulkOperations.singleTransaction config option (#14387)
## Summary

Adds a `bulkOperationsSingleTransaction` config option to the MongoDB
adapter to handle database transaction limitations when processing large
numbers of documents in bulk operations.

## Problem

Some databases (e.g., DocumentDB, Cosmos DB) have transaction
limitations when working with large datasets. DocumentDB throws an error
when a transaction attempts to use a cursor for more than 100 documents:
`Feature not supported: cursor within transaction. Try increasing the
batchsize.`

## Solution

- Adds `bulkOperationsSingleTransaction` boolean option to MongoDB
adapter (`@payloadcms/db-mongodb`)
- Added to `BaseDatabaseAdapter` interface for cross-adapter
compatibility
- Included in `compatibilityOptions` for DocumentDB and Cosmos DB
(default: `true`)
- When enabled, bulk update and delete operations process documents
sequentially in separate transactions
- When disabled (default: `false`), maintains current behavior with
parallel processing in a single transaction
- Fully backward compatible - no changes to existing behavior unless
explicitly configured

## Changes

- **MongoDB Adapter**: Add `bulkOperationsSingleTransaction` config
option
- **Base Adapter Interface**: Add optional property to
`BaseDatabaseAdapter`
- **Compatibility Options**: Enable by default for DocumentDB and Cosmos
DB
- **Core Operations**: Modify `updateOperation` and `deleteOperation` to
support per-document transactions
- **Documentation**: Add to MongoDB adapter options documentation
- **Tests**: Add integration tests for both update and delete operations

## Testing

Tested with both `bulkOperationsSingleTransaction: true` and `false` to
ensure:
- Documents are correctly updated/deleted in both modes
- Sequential processing works without transaction conflicts
- Parallel processing (default) maintains optimal performance
2026-01-13 09:13:56 -05:00
Alessio Gravili cd87ab4517 fix: warning during Next.js build "the request of a dependency is an expression" (#15007)
Fixes https://github.com/payloadcms/payload/issues/15006

https://github.com/payloadcms/payload/pull/14337 introduced a next build
warning, because the `eval` around `await import` statements has been
removed.

Unlike jest, vitest is fine with it. But during Next.js build, the
bundler will throw a `"Critical dependency: the request of a dependency
is an expression"` warning.

This PR fixes this warning by adding back the eval statement around the
import. It does not add back the conditional require statement - that
part was only necessary to appease jest.

For vitest, we have to directly call `await import()`. Using eval in
vitest will throw an `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING` error.

## Preventing this regression in the future

This PR adds a comment above the eval explaining why it's needed.
Previously this was missing, and we only had a comment explaining why
the `require` was needed.

Additionally, the reason this was not caught in CI was because warnings
during next build did not fail the build script. This PR adds an
additional check in `build-template-with-local-pkgs.ts` that throws an
error if a warning was detected during next build. You can find the CI
failure here:
https://github.com/payloadcms/payload/actions/runs/20402288354/job/58626594247?pr=15007
(CI run from commit that added this check, before the commit that fixed
it)

Now, if this ever happens again, our CI will fail and prevent a
regression.

## More Tests

This PR adds additional database and job queue tests that verify that
the job queue handler and migration file imports work correctly.
2025-12-23 11:52:54 -05:00
Sasha 4e45432592 test: migrate to vitest (#14337)
Continuation of https://github.com/payloadcms/payload/pull/10200

* For VSCode - you'll need to install the official plugin
https://marketplace.visualstudio.com/items?itemName=vitest.explorer
* Every test suite now explicitly imports the test runner functions like
`describe`, `expect`, instead of using it from `global`. While `vitest`
supports globals with https://vitest.dev/config/#globals, the main
benefit of not doing this is that we don't conflict with test runners
that have the same functions, e.g `playwright` and `tstyche`.
* Now we have only 1 test config file that uses
[projects](https://vitest.dev/guide/projects) for unit and integration
tests

---------

Co-authored-by: Alessio Gravili <github@gravili.net>
2025-12-19 16:07:14 -05:00
Alessio Gravili 0d9ec910a9 perf: up to 5000% faster permissions calculation (#14631)
In large projects, calculating the `permissions` object is one of the
slowest parts of Payload. This PR completely rewrites the permission
calculation system to make it significantly faster and more reliable.

The permissions object is calculated multiple times throughout Payload's
lifecycle: for the entire config every time we load or navigate to any
admin panel page, on every API endpoint call, and as part of query path
validation when calling any Payload operation.
This is done by `getAccessResults` without any document data, and in
large configs can be one of the slowest operations, noticeably slowing
down admin panel navigation.

After this rewrite, **speed improvements in permissions calculation
without data range from 5.3% (access-control test suite) to 74% (fields
test suite - our largest config)**. The larger the config, the more
noticeable these improvements become. These performance gains apply to
both the Payload Admin Panel and API requests.

---

The `permissions` object can also be calculated WITH document data,
which triggers evaluation of `Where` conditions in
collection/global-level access control and may fetch the document to
pass to access control functions.

This has been heavily optimized with **speed improvements for
permissions calculation with data ranging between 75% and 5138%** (if I
craft a collection config that excessively uses aspects this PR
improves) in the benchmarks. This optimization affects:

- Loading any document (collection/global edit view or drawer)
- Saving a document
- Loading the list view with query presets applied
- Bulk upload initialization (runs twice)

These improvements are most noticeable when navigating _to_ documents in
the admin panel.

## What's Fixed

In addition to performance improvements, the new function is now
cleaner, easier to understand and works more reliably. I specifically
wanna call out two issues that were fixed:

**Field access control data bug**: Added a new e2e test that was
previously failing - when saving a document, the data object passed to
field-level update access control functions had the wrong shape (or in
some cases did not exist at all). This caused fields to show incorrect
`readOnly` states after save. The new implementation properly passes
document data through all access control checks.

**Where query validation**: The old implementation sometimes skipped
validating `Where` queries for collections when `req.data` existed,
potentially granting incorrect access. The new version always validates
where queries correctly when `fetchData: true`.

**Global Where queries:**: Where queries were _never_ executed for
globals. Instead, [we were just checking if the global existed in
general](https://github.com/payloadcms/payload/blob/main/packages/payload/src/utilities/getEntityPolicies.ts#L59).
This PR ensures that `Where` queries returned from global access control
are respected.


## What's Faster (and Why)

**Where query caching:** When multiple operations return identical where
queries (common pattern), we now cache the result. Instead of 3-4
separate DB calls, we make 1 call and reuse it.

**Parallel execution:** All `Where` query evaluations and async access
control functions (for both collections and fields) now execute
concurrently with maximum parallelism, rather than sequentially.

**Optimized DB operations**: When evaluating returned `Where` queries,
we now use direct database `db.count()` queries instead of
`payload.find()` operations. This is much faster (especially on
Postgres).

**Synchronous tree traversal**: The entire field permission tree is now
built synchronously with all async work collected and executed in
parallel at the end, rather than cascading await calls through each
nesting level. This eliminates sequential bottlenecks in deeply nested
field structures.

## Benchmarks

### Admin Panel

- Fields test suite with access control added
- 1000 blocks added to the blocks collection. This is not as excessive
as you would expect:
- most Payload apps do not run on an M3 Max. CPU/DB is often much, much
slower
  - this is all local, with a local DB
- a lot of projects accumulate a huge amount of blocks if you multiply
blocks x block fields. They can definitely reach 1000 total blocks
- each block is simple - only one text field per block. In real
projects, blocks are usually a lot more complex

**Before:**


https://github.com/user-attachments/assets/37b55f02-6dbc-4005-9da6-d7cd4bfdc925

**After:**


https://github.com/user-attachments/assets/8dd796dd-66ed-4d32-a688-27b4169577c6

Branch:
https://github.com/payloadcms/payload/tree/fix/update-field-access-control-after-save-benchmarks

### Modified Access-Control Test Suite

`cd test && pnpm payload run access-control/benchmark-permissions.ts`

```md
📊 Benchmark 1: getAccessResults (all collections + globals)
──────────────────────────────────────────────────────────────────────
┌─────────┬───────────────────┬─────────┬───────────────────┬──────────┐
│ (index) │ Task Name         │ ops/sec │ Average Time (ms) │ Margin   │
├─────────┼───────────────────┼─────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (optimized)' │ '16.91' │ '59203.986'       │ '±0.52%' │
│ 1       │ 'OLD (previous)'  │ '16.07' │ '62323.953'       │ '±0.57%' │
└─────────┴───────────────────┴─────────┴───────────────────┴──────────┘
  ⚡ Speedup: +5.3% 🚀
  📊 DB Calls per operation (across all collections + globals):
     NEW: 0.0 total (0.0 data, 0.0 where)
     OLD: 0.0 total (0.0 data, 0.0 where)

📊 Benchmark 2: docAccessOperation (with fetchData)
──────────────────────────────────────────────────────────────────────
  Collection: where-cache-same (same where queries)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (with cache)' │ '2272.81' │ '442.319'         │ '±0.11%' │
│ 1       │ 'OLD (no cache)'   │ '1019.72' │ '983.347'         │ '±0.12%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +122.9% 🚀
  📊 DB Calls per operation:
     NEW: 2.0 total (1.0 data, 1.0 where)
     OLD: 4.0 total (1.0 data, 3.0 where)

📊 Benchmark 3: docAccessOperation (with data passed)
──────────────────────────────────────────────────────────────────────
  Collection: where-cache-same (same where queries, no DB fetch)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (with cache)' │ '4945.43' │ '203.252'         │ '±0.11%' │
│ 1       │ 'OLD (no cache)'   │ '1023.75' │ '979.898'         │ '±0.14%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +383.1% 🚀
  📊 DB Calls per operation:
     NEW: 1.0 total (0.0 data, 1.0 where)
     OLD: 4.0 total (1.0 data, 3.0 where)

📊 Benchmark 4: docAccessOperation (unique where queries)
──────────────────────────────────────────────────────────────────────
  Collection: where-cache-unique (unique where queries per operation)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (parallel)'   │ '1693.67' │ '599.091'         │ '±0.29%' │
│ 1       │ 'OLD (sequential)' │ '970.37'  │ '1036.268'        │ '±0.20%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +74.5% 🚀
  📊 DB Calls per operation:
     NEW: 4.0 total (1.0 data, 3.0 where)
     OLD: 4.0 total (1.0 data, 3.0 where)

📊 Benchmark 5: Complex Collection (async access, nested blocks, field access)
──────────────────────────────────────────────────────────────────────
  Collection: complex-content (stress test)

┌─────────┬────────────────────┬──────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec  │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼──────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (optimized)'  │ '133.17' │ '7683.346'        │ '±0.83%' │
│ 1       │ 'OLD (sequential)' │ '72.86'  │ '13901.616'       │ '±0.82%' │
└─────────┴────────────────────┴──────────┴───────────────────┴──────────┘
  ⚡ Speedup: +82.8% 🚀
  📊 DB Calls per operation:
     NEW: 2.0 total (1.0 data, 1.0 where)
     OLD: 2.0 total (1.0 data, 1.0 where)

📊 Benchmark 6: Sync-Heavy Collection (same where, many sync field access)
──────────────────────────────────────────────────────────────────────
  Collection: sync-heavy (where cache + field access)

┌─────────┬────────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name          │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼────────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (with cache)' │ '3607.72' │ '279.559'         │ '±0.15%' │
│ 1       │ 'OLD (no cache)'   │ '68.87'   │ '15136.557'       │ '±1.44%' │
└─────────┴────────────────────┴───────────┴───────────────────┴──────────┘
  ⚡ Speedup: +5138.5% 🚀
  📊 DB Calls per operation:
     NEW: 1.0 total (0.0 data, 1.0 where)
     OLD: 5.0 total (1.0 data, 4.0 where)

══════════════════════════════════════════════════════════════════════
📈 Summary:
══════════════════════════════════════════════════════════════════════
  1. getAccessResults:                        (see above)
  2. docAccessOperation (with fetchData):     (see above)
  3. docAccessOperation (with data passed):   (see above)
  4. docAccessOperation (unique where):       (see above)
  5. Complex collection (async + nested):     (see above)
  6. Sync-heavy (where cache + fields):       (see above)
══════════════════════════════════════════════════════════════════════
```

### Fields Test Suite

`cd test && pnpm payload run fields/benchmark-getAccessResults.ts`

```md
📊 Benchmark: getAccessResults (all fields test collections + globals)
──────────────────────────────────────────────────────────────────────
┌─────────┬───────────────────┬───────────┬───────────────────┬──────────┐
│ (index) │ Task Name         │ ops/sec   │ Average Time (ms) │ Margin   │
├─────────┼───────────────────┼───────────┼───────────────────┼──────────┤
│ 0       │ 'NEW (optimized)' │ '2104.06' │ '478.382'         │ '±0.18%' │
│ 1       │ 'OLD (previous)'  │ '1208.14' │ '834.974'         │ '±0.21%' │
└─────────┴───────────────────┴───────────┴───────────────────┴──────────┘

  ⚡ Speedup: +74.2% 🚀
  📊 DB Calls per operation (across all collections + globals):
     NEW: 0.0 total (0.0 data, 0.0 where)
     OLD: 0.0 total (0.0 data, 0.0 where)
```
2025-11-20 11:06:12 -08:00
Jessica Rynkar fd7c94c027 chore: getLocalizedPaths should filter fields with locale-like names (#14661)
### What?
Ensures `getLocalizedPaths` doesn't falsely detect fields with
locale-like names as a localized field.

### Why?
In the function we are determining if the next segment is a locale based
on whether the segment of the field path matches a locale:
```ts
const nextSegmentIsLocale = localizationConfig && localizationConfig.localeCodes.includes(nextSegment)
```

If you have locale `en` and a nested field with `name: 'en'`, the
function will mistake it for a locale key regardless of whether the
field is localized.

### How?
Uses the existing `fieldShouldBeLocalized` function to check whether the
field is localized in addition to the other checks.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-11-18 15:44:27 -05:00
Sasha 059185f88f fix(db-*): findMigrationDir in projects without src folder (#14381)
Fixes https://github.com/payloadcms/payload/issues/14379

Additionally, removes duplication of `findMigrationDir` between DB
adapters, deprecates the export from `@payloadcms/drizzle` and adds unit
testing to this function.
2025-10-28 18:57:26 +00:00
Jarrod Flesch c2dcd122e1 chore: extract snapshot logic, correct types in version ops (#14290)
Adds a `createSnapshot` to improve readability in the save versions
file.

Corrects update/create version types for collections and globals.
2025-10-22 13:21:35 -04:00
Dan Ribbens 9ceee8ea3c chore: ci changes to add compatibility for mongodb alternates (#13898)
- Adds documentdb and cosmosdb to CI test matrix
- Adds missing generateDatabaseAdapter configs
- Adjust generateDatabaseAdapter firestore config to make it pure to the
compatibilityOptions we provide as an export

Creating as draft because I expect a few tests to fail based on previous
comments.

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2025-10-06 16:48:02 -04:00
Dan Ribbens 456e3d6459 revert: "feat: adds new experimental.localizeStatus option (#13207)" (#13928)
This reverts commit 0f6d748365.

# Conflicts:
#	docs/configuration/overview.mdx
#	docs/experimental/overview.mdx
#	packages/ui/src/elements/Status/index.tsx
2025-09-25 15:14:19 +00:00
Jessica Rynkar 0f6d748365 feat: adds new experimental.localizeStatus option (#13207)
### What?
Adds a new `experimental.localizeStatus` config option, set to `false`
by default. When `true`, the admin panel will display the document
status based on the *current locale* instead of the _latest_ overall
status. Also updates the edit view to only show a `changed` status when
`autosave` is enabled.

### Why?
Showing the status for the current locale is more accurate and useful in
multi-locale setups. This update will become default behavior, able to
be opted in by setting `experimental.localizeStatus: true` in the
Payload config. This option will become depreciated in V4.

### How?
When `localizeStatus` is `true`, we store the localized status in a new
`localeStatus` field group within version data. The admin panel then
reads from this field to display the correct status for the current
locale.

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-09-03 10:27:08 +01:00
Jarrod Flesch 73ba4d1bb9 fix: unable to query versions on latest key (#13512)
Fixes https://github.com/payloadcms/payload/issues/13455

https://github.com/payloadcms/payload/pull/13297 Fixed a scoping issue,
but exposed a new issue where querying versioned documents by the
`latest` key would fail. This PR fixes the newly discoverable issue.
2025-08-19 11:42:02 -07:00
Jarrod Flesch 8d84352ee9 fix(next): catch list filter errors, prevent list view crash (#13297)
Catches list filter errors and prevents the list view from crashing when
attempting to search on fields the user does not have access to. Instead
just shows the default "no results found" message.
2025-07-29 11:30:07 -04:00
Sasha 75385de01f fix: filtering by polymorphic relationships inside other fields (#13265)
Previously, filtering by a polymorphic relationship inside an array /
group (unless the `name` is `version`) / tab caused `QueryError: The
following path cannot be queried:`.
2025-07-25 09:10:21 -04:00
Alessio Gravili c08b2aea89 feat: scheduling jobs (#12863)
Adds a new `schedule` property to workflow and task configs that can be
used to have Payload automatically _queue_ jobs following a certain
_schedule_.

Docs:
https://payloadcms.com/docs/dynamic/jobs-queue/schedules?branch=feat/schedule-jobs

## API Example

```ts
export default buildConfig({
  // ...
  jobs: {
    // ...
    scheduler: 'manual', // Or `cron` if you're not using serverless. If `manual` is used, then user needs to set up running /api/payload-jobs/handleSchedules or payload.jobs.handleSchedules in regular intervals
    tasks: [
      {
        schedule: [
          {
            cron: '* * * * * *',
            queue: 'autorunSecond',
            // Hooks are optional
            hooks: {
              // Not an array, as providing and calling `defaultBeforeSchedule` would be more error-prone if this was an array
              beforeSchedule: async (args) => {
                // Handles verifying that there are no jobs already scheduled or processing.
                // You can override this behavior by not calling defaultBeforeSchedule, e.g. if you wanted
                // to allow a maximum of 3 scheduled jobs in the queue instead of 1, or add any additional conditions
                const result = await args.defaultBeforeSchedule(args)
                return {
                  ...result,
                  input: {
                    message: 'This task runs every second',
                  },
                }
              },
              afterSchedule: async (args) => {
                await args.defaultAfterSchedule(args) // Handles updating the payload-jobs-stats global
                args.req.payload.logger.info(
                  'EverySecond task scheduled: ' +
                  (args.status === 'success' ? args.job.id : 'skipped or failed to schedule'),
                )
              },
            },
          },
        ],
        slug: 'EverySecond',
        inputSchema: [
          {
            name: 'message',
            type: 'text',
            required: true,
          },
        ],
        handler: ({ input, req }) => {
          req.payload.logger.info(input.message)
          return {
            output: {},
          }
        },
      }
    ]
  }
})
```

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210495300843759
2025-07-18 06:48:27 -04:00
Sasha a20b43624b feat: add findDistinct operation (#13102)
Adds a new operation findDistinct that can give you distinct values of a
field for a given collection
Example:
Assume you have a collection posts with multiple documents, and some of
them share the same title:
```js
// Example dataset (some titles appear multiple times)
[
  { title: 'title-1' },
  { title: 'title-2' },
  { title: 'title-1' },
  { title: 'title-3' },
  { title: 'title-2' },
  { title: 'title-4' },
  { title: 'title-5' },
  { title: 'title-6' },
  { title: 'title-7' },
  { title: 'title-8' },
  { title: 'title-9' },
]
```
You can now retrieve all unique title values using findDistinct:
```js
const result = await payload.findDistinct({
  collection: 'posts',
  field: 'title',
})

console.log(result.values)
// Output:
// [
//   'title-1',
//   'title-2',
//   'title-3',
//   'title-4',
//   'title-5',
//   'title-6',
//   'title-7',
//   'title-8',
//   'title-9'
// ]
```
You can also limit the number of distinct results:
```js
const limitedResult = await payload.findDistinct({
  collection: 'posts',
  field: 'title',
  sortOrder: 'desc',
  limit: 3,
})

console.log(limitedResult.values)
// Output:
// [
//   'title-1',
//   'title-2',
//   'title-3'
// ]
```

You can also pass a `where` query to filter the documents.
2025-07-16 17:18:14 -04:00
Alessio Gravili 053192c488 refactor: changed default exports to named exports in payload package (#12871)
This changes all remaining default exports to named exports in the
payload package and removes all unnecessary internal-only barrel export
files. => Less lines of code, less eslint warnings

![Screenshot 2025-06-19 at 14 02
23@2x](https://github.com/user-attachments/assets/bcbe2394-07b5-49b4-86c7-30243679bb61)
2025-06-24 04:38:02 +00:00
Sasha bc9b501e28 fix: querying virtual fields deeply with draft: true (#12868)
Fixes an issue when querying deeply new relationship virtual fields with
`draft: true`. Changes the method for `where` sanitization, before it
was done in `validateSearchParam` which didn't work with versions
properly, now there's a separate `sanitizeWhereQuery` function that does
this.
2025-06-23 22:18:49 -04:00
Alessio Gravili 84cb2b5819 refactor: simplify job type (#12816)
Previously, there were multiple ways to type a running job:
- `GeneratedTypes['payload-jobs']` - only works in an installed project
- is `any` in monorepo
- `BaseJob` - works everywhere, but does not incorporate generated types
which may include type for custom fields added to the jobs collection
- `RunningJob<>` - more accurate version of `BaseJob`, but same problem

This PR deprecated all those types in favor of a new `Job` type.
Benefits:
- Works in both monorepo and installed projects. If no generated types
exist, it will automatically fall back to `BaseJob`
- Comes with an optional generic that can be used to narrow down
`job.input` based on the task / workflow slug. No need to use a separate
type helper like `RunningJob<>`

With this new type, I was able to replace every usage of
`GeneratedTypes['payload-jobs']`, `BaseJob` and `RunningJob<>` with the
simple `Job` type.

Additionally, this PR simplifies some of the logic used to run jobs
2025-06-16 16:15:56 -04:00
Sasha 9943b3508d fix: filtering joins in where by ID (#12804)
Fixes https://github.com/payloadcms/payload/issues/12768

Example:
```
const found_1 = await payload.find({
  collection: 'categories',
  where: { 'relatedPosts.id': { equals: post.id } },
})
```
or
```
const found_2 = await payload.find({
  collection: 'categories',
  where: { relatedPosts: { equals: post.id } },
})
```
2025-06-13 14:13:17 -04:00
Germán Jabloñski 53f8838830 chore: migrate to TypeScript strict in Payload package - #4/4 (#12733)
Important: An intentional effort is being made during migration to not
modify runtime behavior. This implies that there will be several
assertions, non-null assertions, and @ts-expect-error. This philosophy
applies only to migrating old code to TypeScript strict, not to writing
new code. For a more detailed justification for this reasoning, see
#11840 (comment).

In this PR, instead of following the approach of migrating a subset of
files, I'm migrating all files by disabling specific rules. The first
commits are named after the rule being disabled.

With this PR, the migration of the payload package is complete 🚀
2025-06-09 20:50:17 +00:00
Sasha 9fbc3f6453 fix: proper globals max versions clean up (#12611)
Fixes https://github.com/payloadcms/payload/issues/11879
2025-06-09 14:38:07 -04:00
Elliot DeNolf 48218bccb5 chore: fix lint warnings for default exports, unused imports, unused err in catch (#12666)
Fix various lint warnings in payload package. 

387 warnings -> 215 warnings

- Migrate (most) default exports to named
- Remove unused imports
- Rename unused errors in catch statements to `ignore`
2025-06-04 10:15:59 -04:00
Germán Jabloñski 6ec21a53ff chore: migrate to TypeScript strict in Payload package (enable strictNullChecks) - #3 (#12586)
Important: An intentional effort is being made during migration to not
modify runtime behavior. This implies that there will be several
assertions, non-null assertions, and @ts-expect-error. This philosophy
applies only to migrating old code to TypeScript strict, not to writing
new code. For a more detailed justification for this reasoning,
https://github.com/payloadcms/payload/pull/11840#discussion_r2021975897.

In this PR, instead of following the approach of migrating a subset of
files, I'm migrating all files by disabling a specific rule. In this
case, `strictNullChecks`.

`strictNullChecks` is a good rule to start the migration with because
it's easy to silence with non-null assertions or optional chainings.
Additionally, almost all ts strict errors are due to this rule.

This PR improves 200+ files, leaving only 68 remaining to migrate to
strict mode in the payload package.
2025-06-03 14:43:37 +00:00
Sasha 2b40e0f21f feat: polymorphic join querying by fields that don't exist in every collection (#12648)
This PR makes it possible to do polymorphic join querying by fields that
don't exist in all collections specified in `field.collection`, for
example:
```
const result = await payload.find({
  collection: 'payload-folders',
  joins: {
    documentsAndFolders: {
      where: {
        and: [
          {
            relationTo: {
              in: ['folderPoly1', 'folderPoly2'],
            },
          },
          {
            folderPoly2Title: { // this field exists only in the folderPoly2 collection, before it'd throw a query error.
              equals: 'Poly 2 Title',
            },
          },
        ],
      },
    },
  },
})
```

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-06-03 00:48:07 +03:00
Sasha 1b1e36e2df fix(db-*): migrate:reset executes in a wrong order (#12445)
fixes https://github.com/payloadcms/payload/issues/12442
2025-05-22 13:30:29 -04:00
Sasha 1c99f46e4f feat: queriable / sortable / useAsTitle virtual fields linked with a relationship field (#11805)
This PR adds an ability to specify a virtual field in this way
```js
{
  slug: 'posts',
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
  ],
},
{
  slug: 'virtual-relations',
  fields: [
    {
      name: 'postTitle',
      type: 'text',
      virtual: 'post.title',
    },
    {
      name: 'post',
      type: 'relationship',
      relationTo: 'posts',
    },
  ],
},
```

Then, every time you query `virtual-relations`, `postTitle` will be
automatically populated (even if using `depth: 0`) on the db level. This
field also, unlike `virtual: true` is available for querying / sorting /
`useAsTitle`.

Also, the field can be deeply nested to 2 or more relationships, for
example:
```
{
  name: 'postCategoryTitle',
  type: 'text',
  virtual: 'post.category.title',
},
```

Where the current collection has `post` - a relationship to `posts`, the
collection `posts` has `category` that's a relationship to `categories`
and finally `categories` has `title`.
2025-04-16 15:46:18 -04:00
Sasha 466dcd7189 feat: support where querying by join fields (#12075)
### What?
This PR adds support for `where` querying by the join field (don't
confuse with `where` querying of related docs via `joins.where`)

Previously, this didn't work:
```
const categories = await payload.find({
  collection: 'categories',
  where: { 'relatedPosts.title': { equals: 'my-title' } },
})
```

### Why?
This is crucial for bi-directional relationships, can be used for access
control.

### How?
Implements `where` handling for join fields the same as we do for
relationships. In MongoDB it's not as efficient as it can be, the old PR
that improves it and can be updated later is here
https://github.com/payloadcms/payload/pull/8858

Fixes https://github.com/payloadcms/payload/discussions/9683
2025-04-10 15:30:40 -04:00
Sasha b9ffbc6994 fix: querying by polymorphic join field relationTo with overrideAccess: false (#11999)
Previously, querying by polymorphic joins `relationTo` with
`overrideAccess: false` caused an error:
```
QueryError: The following paths cannot be queried: relationTo
```

As this field actually doesn't exist in the schema. Now, under condition
that the query comes from a polymorphic join we skip checking
`relationTo` field access.
2025-04-07 20:19:43 +00:00
Alessio Gravili 9a1c3cf4cc fix: support parallel job queue tasks (#11917)
This adds support for running multiple job queue tasks in parallel
within the same workflow while preventing conflicts. Previously, this
would have caused the following issues:
- Job log entries get lost - the final job log is incomplete, despite
all tasks having been executed
- Write conflicts in postgres, leading to unique constraint violation
errors

The solution involves handling job log data updates in a way that avoids
overwriting, and ensuring the final update reflects the latest job log
data. Each job log entry now initializes its own ID, so a given job log
entry’s ID remains the same across multiple, parallel task executions.

## Postgres

In Postgres, we need to enable transactions for the
`payload.db.updateJobs` operation; otherwise, two tasks updating the
same job in parallel can conflict. This happens because Postgres handles
array rows by deleting them all, then re-inserting (rather than
upserting). The rows are stored in a separate table, and the following
scenario can occur:

Op 1: deletes all job log rows
Op 2: deletes all job log rows
Op 1: inserts 200 job log rows
Op 2: insert the same 200 job log rows again => `error: “duplicate key
value violates unique constraint "payload_jobs_log_pkey”`

Because transactions were not used, the rows inserted by Op 1
immediately became visible to Op 2, causing the conflict. Enabling
transactions fixes this. In theory, it can still happen if Op 1 commits
before Op 2 starts inserting (due to the read committed isolation
level), but it should occur far less frequently.

Alongside this change, we should consider inserting the rows using an
upsert (update on conflict), which will get rid of this error
completely. That way, if the insertion of Op 1 is visible to Op 2, Op 2
will simply overwrite it, rather than erroring. Individual job entries
are immutable and job entries cannot be deleted, thus this shouldn't
corrupt any data.

## Mongo

In Mongo, the issue is addressed by ensuring that log row deletions
caused due to different log states in concurrent operations are not
merged back to the client job log, and by making sure the final update
includes all job logs.

There is no duplicate key error in Mongo because the array log resides
in the same document and duplicates are simply upserted. We cannot use
transactions in Mongo, as it appears to lock the document in a way that
prevents reliable parallel updates, leading to:

`MongoServerError: WriteConflict error: this operation conflicted with
another operation. Please retry your operation or multi-document
transaction`
2025-03-31 13:06:05 -06:00
Alessio Gravili a083d47368 feat(db-*): return database name to unsanitized config (#11913)
You can access the database name from `sanitizedConfig.db.name`. But
currently, it' not possible to access the db name from the unsanitized
config.

Plugins only have access to the unsanitized config. This change allows
db adapters to return the db name early, which will allow plugins to
conditionally initialize db-specific functionality
2025-03-31 12:57:17 -06:00
Alessio Gravili a5c3aa0e4f perf: reduce job queue db calls (#11846)
Continuation of #11489. This adds a new, optional `updateJobs` db
adapter method that reduces the amount of database calls for the jobs
queue.

## MongoDB

### Previous: running a set of 50 queued jobs
- 1x db.find (= 1x `Model.paginate`)
- 50x db.updateOne (= 50x `Model.findOneAndUpdate`)

### Now: running a set of 50 queued jobs
- 1x db.updateJobs (= 1x `Model.find` and 1x `Model.updateMany`)

**=> 51 db round trips before, 2 db round trips after**


### Previous: upon task completion
- 1x db.find (= 1x `Model.paginate`)
- 1x db.updateOne (= 1x `Model.findOneAndUpdate`)

### Now: upon task completion
- 1x db.updateJobs (= 1x `Model.findOneAndUpdate`)


**=> 2 db round trips before, 1 db round trip after**


## Drizzle (e.g. Postgres)

### running a set of 50 queued jobs
 - 1x db.query[tablename].findMany
 - 50x db.select 
 - 50x upsertRow
 
This is unaffected by this PR and will be addressed in a future PR
2025-03-25 18:09:52 +00:00
Sasha 1b2b6a1b15 fix: respect draft: true when querying docs for the join field (#11763)
Previously, if you were querying a collection that has a join field with
`draft: true`, and the join field's collection also has
`versions.drafts: true` our db adapter would still query the original
SQL table / mongodb collection instead of the versions one which isn't
quite right since we respect `draft: true` when populating relationships
2025-03-24 09:49:30 -04:00
Alessio Gravili e96d3c87e2 feat(db-*): support sort in db.updateMany (#11768)
This adds support for `sort` in `payload.db.updateMany`.

## Example

```ts
const updatedDocs = await payload.db.updateMany({
  collection: 'posts',
  data: {
    title: 'updated',
  },
  limit: 5,
  sort: '-numberField', // <= new
  where: {
    id: {
      exists: true,
    },
  },
})
```
2025-03-19 10:47:58 -06:00
Sasha f442d22237 feat(db-*): allow to thread id to create operation data without custom IDs (#11709)
Fixes https://github.com/payloadcms/payload/issues/6884

Adds a new flag `acceptIDOnCreate` that allows you to thread your own
`id` to `payload.create` `data`, for example:

```ts
// doc created with id 1
const doc = await payload.create({ collection: 'posts', data: {id: 1, title: "my title"}})
```

```ts
import { Types } from 'mongoose'
const id = new Types.ObjectId().toHexString()
const doc = await payload.create({ collection: 'posts', data: {id, title: "my title"}})
```
2025-03-17 23:48:35 -04:00
Alessio Gravili 6a3d58bb32 feat(db-*): support limit in db.updateMany (#11488)
This PR adds a new `limit` property to `payload.db.updateMany`. This functionality is required for [migrating our job system to use faster, direct db adapter calls](https://github.com/payloadcms/payload/pull/11489)
2025-03-03 05:32:57 +00:00
Dmitrii Kuzmin c828e336ee fix: excludes index files from migration files filtering (#10722) 2025-02-28 19:39:44 -05:00
Alessio Gravili 41c7413f59 feat(db-*): add updateMany method to database adapter (#11441)
This PR adds a new `payload.db.updateMany` method, which is a more performant way to update multiple documents compared to using `payload.update`.
2025-02-27 20:30:17 -07:00
Alessio Gravili c21dac1b53 perf(db-*): add option to disable returning modified documents in db methods (#11393)
This PR adds a new `returning` option to various db adapter methods. Setting it to `false` where the return value is not used will lead to performance gains, as we don't have to do additional db calls to fetch the updated document and then sanitize it.
2025-02-27 17:40:22 -05:00
Alessio Gravili 7922d66181 fix(db-mongodb): properly handle document notfound cases for update and delete operations (#11267)
In the `findOne` db operation, we return `null` if the document was not
found.

For single-document delete and update operations, if the document you
wanted to update is not found, the following runtime error is thrown
instead: `Cannot read properties of null (reading '_id')`.

This PR correctly handles these cases and returns `null` from the db
method, just like the `findOne` operation.
2025-02-19 01:19:59 +00:00
Sasha 6d36a28cdc feat: join field across many collections (#10919)
This feature allows you to specify `collection` for the join field as
array.
This can be useful for example to describe relationship linking like
this:
```ts
{
  slug: 'folders',
  fields: [
    {
      type: 'join',
      on: 'folder',
      collection: ['files', 'documents', 'folders'],
      name: 'children',
    },
    {
      type: 'relationship',
      relationTo: 'folders',
      name: 'folder',
    },
  ],
},
{
  slug: 'files',
  upload: true,
  fields: [
    {
      type: 'relationship',
      relationTo: 'folders',
      name: 'folder',
    },
  ],
},
{
  slug: 'documents',
  fields: [
    {
      type: 'relationship',
      relationTo: 'folders',
      name: 'folder',
    },
  ],
},
```

Documents and files can be placed to folders and folders themselves can
be nested to other folders (root folders just have `folder` as `null`).

Output type of `Folder`:
```ts
export interface Folder {
  id: string;
  children?: {
    docs?:
      | (
          | {
              relationTo?: 'files';
              value: string | File;
            }
          | {
              relationTo?: 'documents';
              value: string | Document;
            }
          | {
              relationTo?: 'folders';
              value: string | Folder;
            }
        )[]
      | null;
    hasNextPage?: boolean | null;
  } | null;
  folder?: (string | null) | Folder;
  updatedAt: string;
  createdAt: string;
}
```

While you could instead have many join fields (for example
`childrenFolders`, `childrenFiles`) etc - this doesn't allow you to
sort/filter and paginate things across many collections, which isn't
trivial. With SQL we use `UNION ALL` query to achieve that.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-02-18 21:53:45 +02:00
Alessio Gravili 313ff047df perf: optimize permissions calculation with lots of blocks (#11236)
This PR optimizes permissions calculation for block references, by
calculating them only once per block reference config, instead of once
every single time the blocks are referenced.

This will lead to significant performance improvements in Payload
Configs with a lot of duplicative block references, as permissions are
calculated every time you navigate from page to page.

# Benchmarks

Tested using `pnpm dev benchmark-blocks`.

## Before - ~ 6 seconds


https://github.com/user-attachments/assets/85cac698-3120-414f-91d3-608a404a3a5f


## After - ~ 2 seconds


https://github.com/user-attachments/assets/0c3642f6-6001-41ae-a7cd-f30b24362e9b
2025-02-18 11:16:04 -05:00
Sasha 847d8d824f feat: add page query parameter for joins (#10998)
Currently, the join field outputs to its result `hasNextPage: boolean`
and have the `limit` query parameter but lacks `page` which can be
useful. This PR adds it.
2025-02-18 16:47:09 +02:00
Alessio Gravili d49de7bdf8 perf: do not populate globals when calculating permissions, cleanup getEntityPolicies (#11237)
Previously, we forgot to add `depth: 0` to our `findGlobal` call in `getEntityPolicies`. This PR adds `depth: 0` which will be faster.

It also cleans up the `getEntityPolicies` function in general by adding missing types, JSDocs and improving code readability.

This was part of https://github.com/payloadcms/payload/pull/11236 and has been extracted into this separate PR, to make it easier to review
2025-02-17 22:31:27 +00:00