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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -12,7 +12,6 @@ export const UploadJSXConverter: JSXConverters<SerializedUploadNode> = {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const uploadDoc = uploadNode.value as FileData & TypeWithID
|
||||
|
||||
const url = uploadDoc.url
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import { PostgresAdapter } from '@payloadcms/db-postgres/types'
|
||||
import type { PostgresAdapter } from '@payloadcms/db-postgres/types'
|
||||
|
||||
import { cosineDistance, desc, gt, sql } from 'drizzle-orm'
|
||||
import path from 'path'
|
||||
import { buildConfig, getPayload } from 'payload'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { CollectionSlug, Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { createLocalReq } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
@@ -28,7 +29,9 @@ describe('dataloader', () => {
|
||||
},
|
||||
})
|
||||
|
||||
if (loginResult.token) token = loginResult.token
|
||||
if (loginResult.token) {
|
||||
token = loginResult.token
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -187,4 +190,26 @@ describe('dataloader', () => {
|
||||
expect(innerMostRelationship).toStrictEqual(relationB.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('find', () => {
|
||||
it('should call the same query only once in a request', async () => {
|
||||
const req = await createLocalReq({}, payload)
|
||||
const spy = jest.spyOn(payload, 'find')
|
||||
|
||||
const findArgs = {
|
||||
collection: 'items' as CollectionSlug,
|
||||
req,
|
||||
depth: 0,
|
||||
where: {
|
||||
name: { exists: true },
|
||||
},
|
||||
}
|
||||
|
||||
void req.payloadDataLoader.find(findArgs)
|
||||
void req.payloadDataLoader.find(findArgs)
|
||||
await req.payloadDataLoader.find(findArgs)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user