feat: add find to payloadDataLoader to cache local API queries (#11685)

### What?
Extends our dataloader to add a momiozed payload find function. This way
it will cache the query for the same find request using a cacheKey from
find operation args.

### Why?
This was needed internally for `filterOptions` that exist in an array or
other sitautions where you have the same exact query being made and
awaited many times.

### How?

- Added `find` to payloadDataLoader. Marked `@experimental` in case it
needs to change.
- Created a cache key from the args
- Validate filterOptions changed from `payload.find` to
`payloadDataLoader.find`
- Made `payloadDataLoader` no longer optional on `PayloadRequest`, since
other args are required which are created from createLocalReq (context
for example), I don't see a reason why dataLoader shouldn't be required
also.

Example usage: 
```ts
const result = await req.payloadDataLoader.find({
    collection,
    req,
    where,
  })
```
This commit is contained in:
Dan Ribbens
2025-03-18 17:14:33 -04:00
committed by GitHub
parent 67a7358de1
commit 975bbb756f
8 changed files with 108 additions and 10 deletions

View File

@@ -3,8 +3,10 @@ import type { BatchLoadFn } from 'dataloader'
import DataLoader from 'dataloader'
import type { FindArgs } from '../database/types.js'
import type { PayloadRequest, PopulateType, SelectType } from '../types/index.js'
import type { TypeWithID } from './config/types.js'
import type { Options } from './operations/local/find.js'
import { isValidID } from '../utilities/isValidID.js'
@@ -157,7 +159,65 @@ const batchAndLoadDocs =
return docs
}
export const getDataLoader = (req: PayloadRequest) => new DataLoader(batchAndLoadDocs(req))
export const getDataLoader = (req: PayloadRequest) => {
const findQueries = new Map()
const dataLoader = new DataLoader(batchAndLoadDocs(req)) as PayloadRequest['payloadDataLoader']
dataLoader.find = (args: FindArgs) => {
const key = createFindDataloaderCacheKey(args)
const cached = findQueries.get(key)
if (cached) {
return cached
}
const request = req.payload.find(args)
findQueries.set(key, request)
return request
}
return dataLoader
}
const createFindDataloaderCacheKey = ({
collection,
currentDepth,
depth,
disableErrors,
draft,
includeLockStatus,
joins,
limit,
overrideAccess,
page,
pagination,
populate,
req,
select,
showHiddenFields,
sort,
where,
}: Options<string, SelectType>): string =>
JSON.stringify([
collection,
currentDepth,
depth,
disableErrors,
draft,
includeLockStatus,
joins,
limit,
overrideAccess,
page,
pagination,
populate,
req?.locale,
req?.fallbackLocale,
req?.user?.id,
req?.transactionID,
select,
showHiddenFields,
sort,
where,
])
type CreateCacheKeyArgs = {
collectionSlug: string

View File

@@ -595,7 +595,7 @@ const validateFilterOptions: Validate<
falseCollections.push(collection)
}
const result = await payload.find({
const result = await req.payloadDataLoader.find({
collection,
depth: 0,
limit: 0,

View File

@@ -13,6 +13,7 @@ import type {
CollectionSlug,
DataFromGlobalSlug,
GlobalSlug,
Payload,
RequestContext,
TypedCollectionJoins,
TypedCollectionSelect,
@@ -20,7 +21,7 @@ import type {
TypedUser,
} from '../index.js'
import type { Operator } from './constants.js'
export type { Payload as Payload } from '../index.js'
export type { Payload } from '../index.js'
export type CustomPayloadRequestProperties = {
context: RequestContext
@@ -44,7 +45,19 @@ export type CustomPayloadRequestProperties = {
*/
payloadAPI: 'GraphQL' | 'local' | 'REST'
/** Optimized document loader */
payloadDataLoader?: DataLoader<string, TypeWithID>
payloadDataLoader: {
/**
* Wraps `payload.find` with a cache to deduplicate requests
* @experimental This is may be replaced by a more robust cache strategy in future versions
* By calling this method with the same arguments many times in one request, it will only be handled one time
* const result = await req.payloadDataLoader.find({
* collection,
* req,
* where: findWhere,
* })
*/
find: Payload['find']
} & DataLoader<string, TypeWithID>
/** Resized versions of the image that was uploaded during this request */
payloadUploadSizes?: Record<string, Buffer>
/** Query params on the request */

View File

@@ -1,7 +1,7 @@
import type { FileData, FileSizeImproved, TypeWithID } from 'payload'
import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.jsx'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
import type { HTMLConvertersAsync } from '../types.js'
export const UploadHTMLConverterAsync: HTMLConvertersAsync<SerializedUploadNode> = {

View File

@@ -1,7 +1,7 @@
import type { FileData, FileSizeImproved, TypeWithID } from 'payload'
import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.jsx'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
import type { HTMLConverters } from '../types.js'
export const UploadHTMLConverter: HTMLConverters<SerializedUploadNode> = {

View File

@@ -12,7 +12,6 @@ export const UploadJSXConverter: JSXConverters<SerializedUploadNode> = {
return null
}
const uploadDoc = uploadNode.value as FileData & TypeWithID
const url = uploadDoc.url