From d49bb4351f22f17f68477c3145594abbb60fab05 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Wed, 29 Nov 2023 14:03:05 -0500 Subject: [PATCH] feat(live-preview): batches api requests (#4315) --- packages/live-preview/src/mergeData.ts | 46 +++++-- packages/live-preview/src/promise.ts | 28 ----- packages/live-preview/src/traverseFields.ts | 118 +++++++----------- packages/live-preview/src/traverseRichText.ts | 42 +++---- packages/live-preview/src/types.ts | 8 ++ test/live-preview/int.spec.ts | 4 +- 6 files changed, 107 insertions(+), 139 deletions(-) delete mode 100644 packages/live-preview/src/promise.ts diff --git a/packages/live-preview/src/mergeData.ts b/packages/live-preview/src/mergeData.ts index 182f4e751a..448bf53eaf 100644 --- a/packages/live-preview/src/mergeData.ts +++ b/packages/live-preview/src/mergeData.ts @@ -1,5 +1,8 @@ +import type { PaginatedDocs } from 'payload/database' import type { fieldSchemaToJSON } from 'payload/utilities' +import type { PopulationsByCollection } from './types' + import { traverseFields } from './traverseFields' export const mergeData = async (args: { @@ -27,22 +30,51 @@ export const mergeData = async (args: { const result = { ...initialData } - const populationPromises: Promise[] = [] + const populationsByCollection: PopulationsByCollection = {} traverseFields({ - apiRoute, - depth, fieldSchema, incomingData, - populationPromises, + populationsByCollection, result, - serverURL, }) - await Promise.all(populationPromises) + await Promise.all( + Object.entries(populationsByCollection).map(async ([collection, populations]) => { + const ids = new Set(populations.map(({ id }) => id)) + const url = `${serverURL}${ + apiRoute || '/api' + }/${collection}?depth=${depth}&where[id][in]=${Array.from(ids).join(',')}` + + let res: PaginatedDocs + + try { + res = await fetch(url, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()) + + if (res?.docs?.length > 0) { + res.docs.forEach((doc) => { + populationsByCollection[collection].forEach((population) => { + if (population.id === doc.id) { + population.ref[population.accessor] = doc + } + }) + }) + } + } catch (err) { + console.error(err) // eslint-disable-line no-console + } + }), + ) return { ...result, - ...(returnNumberOfRequests ? { _numberOfRequests: populationPromises.length } : {}), + ...(returnNumberOfRequests + ? { _numberOfRequests: Object.keys(populationsByCollection).length } + : {}), } } diff --git a/packages/live-preview/src/promise.ts b/packages/live-preview/src/promise.ts deleted file mode 100644 index a327ebddb6..0000000000 --- a/packages/live-preview/src/promise.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const promise = async (args: { - accessor: number | string - apiRoute?: string - collection: string - depth: number - id: number | string - ref: Record - serverURL: string -}): Promise => { - const { id, accessor, apiRoute, collection, depth, ref, serverURL } = args - - const url = `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}` - - let res: Record | null | undefined = null - - try { - res = await fetch(url, { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - }).then((res) => res.json()) - } catch (err) { - console.error(err) // eslint-disable-line no-console - } - - ref[accessor] = res -} diff --git a/packages/live-preview/src/traverseFields.ts b/packages/live-preview/src/traverseFields.ts index e6f0f139bb..37451e2abb 100644 --- a/packages/live-preview/src/traverseFields.ts +++ b/packages/live-preview/src/traverseFields.ts @@ -1,26 +1,16 @@ import type { fieldSchemaToJSON } from 'payload/utilities' -import { promise } from './promise' +import type { PopulationsByCollection } from './types' + import { traverseRichText } from './traverseRichText' export const traverseFields = (args: { - apiRoute?: string - depth?: number fieldSchema: ReturnType incomingData: T - populationPromises: Promise[] + populationsByCollection: PopulationsByCollection result: T - serverURL: string }): void => { - const { - apiRoute, - depth, - fieldSchema: fieldSchemas, - incomingData, - populationPromises, - result, - serverURL, - } = args + const { fieldSchema: fieldSchemas, incomingData, populationsByCollection, result } = args fieldSchemas.forEach((fieldSchema) => { if ('name' in fieldSchema && typeof fieldSchema.name === 'string') { @@ -29,12 +19,9 @@ export const traverseFields = (args: { switch (fieldSchema.type) { case 'richText': result[fieldName] = traverseRichText({ - apiRoute, - depth, incomingData: incomingData[fieldName], - populationPromises, + populationsByCollection, result: result[fieldName], - serverURL, }) break @@ -51,13 +38,10 @@ export const traverseFields = (args: { } traverseFields({ - apiRoute, - depth, fieldSchema: fieldSchema.fields, incomingData: incomingRow, - populationPromises, + populationsByCollection, result: result[fieldName][i], - serverURL, }) return result[fieldName][i] @@ -86,13 +70,10 @@ export const traverseFields = (args: { } traverseFields({ - apiRoute, - depth, fieldSchema: incomingBlockJSON.fields, incomingData: incomingBlock, - populationPromises, + populationsByCollection, result: result[fieldName][i], - serverURL, }) return result[fieldName][i] @@ -110,13 +91,10 @@ export const traverseFields = (args: { } traverseFields({ - apiRoute, - depth, fieldSchema: fieldSchema.fields, incomingData: incomingData[fieldName] || {}, - populationPromises, + populationsByCollection, result: result[fieldName], - serverURL, }) break @@ -146,32 +124,28 @@ export const traverseFields = (args: { const newRelation = incomingRelation.relationTo if (oldID !== newID || oldRelation !== newRelation) { - populationPromises.push( - promise({ - id: incomingRelation.value, - accessor: 'value', - apiRoute, - collection: newRelation, - depth, - ref: result[fieldName][i], - serverURL, - }), - ) + if (!populationsByCollection[newRelation]) { + populationsByCollection[newRelation] = [] + } + + populationsByCollection[newRelation].push({ + id: incomingRelation.value, + accessor: 'value', + ref: result[fieldName][i], + }) } } else { // Handle `hasMany` monomorphic if (result[fieldName][i]?.id !== incomingRelation) { - populationPromises.push( - promise({ - id: incomingRelation, - accessor: i, - apiRoute, - collection: String(fieldSchema.relationTo), - depth, - ref: result[fieldName], - serverURL, - }), - ) + if (!populationsByCollection[fieldSchema.relationTo]) { + populationsByCollection[fieldSchema.relationTo] = [] + } + + populationsByCollection[fieldSchema.relationTo].push({ + id: incomingRelation, + accessor: i, + ref: result[fieldName], + }) } } }) @@ -217,17 +191,15 @@ export const traverseFields = (args: { // if the new value is not empty, populate it // otherwise set the value to null if (newID) { - populationPromises.push( - promise({ - id: newID, - accessor: 'value', - apiRoute, - collection: newRelation, - depth, - ref: result[fieldName], - serverURL, - }), - ) + if (!populationsByCollection[newRelation]) { + populationsByCollection[newRelation] = [] + } + + populationsByCollection[newRelation].push({ + id: newID, + accessor: 'value', + ref: result[fieldName], + }) } else { result[fieldName] = null } @@ -252,17 +224,15 @@ export const traverseFields = (args: { // if the new value is not empty, populate it // otherwise set the value to null if (newID) { - populationPromises.push( - promise({ - id: newID, - accessor: fieldName, - apiRoute, - collection: String(fieldSchema.relationTo), - depth, - ref: result as Record, - serverURL, - }), - ) + if (!populationsByCollection[fieldSchema.relationTo]) { + populationsByCollection[fieldSchema.relationTo] = [] + } + + populationsByCollection[fieldSchema.relationTo].push({ + id: newID, + accessor: fieldName, + ref: result as Record, + }) } else { result[fieldName] = null } diff --git a/packages/live-preview/src/traverseRichText.ts b/packages/live-preview/src/traverseRichText.ts index 85a38b816e..db7961885e 100644 --- a/packages/live-preview/src/traverseRichText.ts +++ b/packages/live-preview/src/traverseRichText.ts @@ -1,19 +1,13 @@ -import { promise } from './promise' +import type { PopulationsByCollection } from './types' export const traverseRichText = ({ - apiRoute, - depth, incomingData, - populationPromises, + populationsByCollection, result, - serverURL, }: { - apiRoute: string - depth: number incomingData: any - populationPromises: Promise[] + populationsByCollection: PopulationsByCollection result: any - serverURL: string }): any => { if (Array.isArray(incomingData)) { if (!result) { @@ -26,12 +20,9 @@ export const traverseRichText = ({ } return traverseRichText({ - apiRoute, - depth, incomingData: item, - populationPromises, + populationsByCollection, result: result[index], - serverURL, }) }) } else if (incomingData && typeof incomingData === 'object') { @@ -68,26 +59,21 @@ export const traverseRichText = ({ const needsPopulation = !result.value || typeof result.value !== 'object' if (needsPopulation) { - populationPromises.push( - promise({ - id: incomingData[key], - accessor: 'value', - apiRoute, - collection: incomingData.relationTo, - depth, - ref: result, - serverURL, - }), - ) + if (!populationsByCollection[incomingData.relationTo]) { + populationsByCollection[incomingData.relationTo] = [] + } + + populationsByCollection[incomingData.relationTo].push({ + id: incomingData[key], + accessor: 'value', + ref: result, + }) } } else { result[key] = traverseRichText({ - apiRoute, - depth, incomingData: incomingData[key], - populationPromises, + populationsByCollection, result: result[key], - serverURL, }) } }) diff --git a/packages/live-preview/src/types.ts b/packages/live-preview/src/types.ts index 382a15107d..874e1fa84a 100644 --- a/packages/live-preview/src/types.ts +++ b/packages/live-preview/src/types.ts @@ -1,3 +1,11 @@ export type LivePreviewArgs = {} export type LivePreview = void + +export type PopulationsByCollection = { + [slug: string]: Array<{ + accessor: number | string + id: number | string + ref: Record + }> +} diff --git a/test/live-preview/int.spec.ts b/test/live-preview/int.spec.ts index bbd9a6367b..6e35ce1300 100644 --- a/test/live-preview/int.spec.ts +++ b/test/live-preview/int.spec.ts @@ -340,7 +340,7 @@ describe('Collections - Live Preview', () => { returnNumberOfRequests: true, }) - expect(merge1._numberOfRequests).toEqual(4) + expect(merge1._numberOfRequests).toEqual(1) expect(merge1.arrayOfRelationships).toHaveLength(1) expect(merge1.arrayOfRelationships).toMatchObject([ { @@ -396,7 +396,7 @@ describe('Collections - Live Preview', () => { returnNumberOfRequests: true, }) - expect(merge2._numberOfRequests).toEqual(4) + expect(merge2._numberOfRequests).toEqual(1) expect(merge2.arrayOfRelationships).toHaveLength(2) expect(merge2.arrayOfRelationships).toMatchObject([ {