feat(live-preview): batches api requests (#4315)
This commit is contained in:
@@ -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 <T>(args: {
|
||||
@@ -27,22 +30,51 @@ export const mergeData = async <T>(args: {
|
||||
|
||||
const result = { ...initialData }
|
||||
|
||||
const populationPromises: Promise<void>[] = []
|
||||
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 }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
export const promise = async (args: {
|
||||
accessor: number | string
|
||||
apiRoute?: string
|
||||
collection: string
|
||||
depth: number
|
||||
id: number | string
|
||||
ref: Record<string, unknown>
|
||||
serverURL: string
|
||||
}): Promise<void> => {
|
||||
const { id, accessor, apiRoute, collection, depth, ref, serverURL } = args
|
||||
|
||||
const url = `${serverURL}${apiRoute || '/api'}/${collection}/${id}?depth=${depth}`
|
||||
|
||||
let res: Record<string, unknown> | 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
|
||||
}
|
||||
@@ -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 = <T>(args: {
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||
incomingData: T
|
||||
populationPromises: Promise<void>[]
|
||||
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 = <T>(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 = <T>(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 = <T>(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 = <T>(args: {
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
apiRoute,
|
||||
depth,
|
||||
fieldSchema: fieldSchema.fields,
|
||||
incomingData: incomingData[fieldName] || {},
|
||||
populationPromises,
|
||||
populationsByCollection,
|
||||
result: result[fieldName],
|
||||
serverURL,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -146,32 +124,28 @@ export const traverseFields = <T>(args: {
|
||||
const newRelation = incomingRelation.relationTo
|
||||
|
||||
if (oldID !== newID || oldRelation !== newRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: incomingRelation.value,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: newRelation,
|
||||
depth,
|
||||
ref: result[fieldName][i],
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Handle `hasMany` monomorphic
|
||||
if (result[fieldName][i]?.id !== incomingRelation) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
id: incomingRelation,
|
||||
accessor: i,
|
||||
apiRoute,
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result[fieldName],
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -217,17 +191,15 @@ export const traverseFields = <T>(args: {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
if (!populationsByCollection[newRelation]) {
|
||||
populationsByCollection[newRelation] = []
|
||||
}
|
||||
|
||||
populationsByCollection[newRelation].push({
|
||||
id: newID,
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: newRelation,
|
||||
depth,
|
||||
ref: result[fieldName],
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
@@ -252,17 +224,15 @@ export const traverseFields = <T>(args: {
|
||||
// if the new value is not empty, populate it
|
||||
// otherwise set the value to null
|
||||
if (newID) {
|
||||
populationPromises.push(
|
||||
promise({
|
||||
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||
populationsByCollection[fieldSchema.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[fieldSchema.relationTo].push({
|
||||
id: newID,
|
||||
accessor: fieldName,
|
||||
apiRoute,
|
||||
collection: String(fieldSchema.relationTo),
|
||||
depth,
|
||||
ref: result as Record<string, unknown>,
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
result[fieldName] = null
|
||||
}
|
||||
|
||||
@@ -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<any>[]
|
||||
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({
|
||||
if (!populationsByCollection[incomingData.relationTo]) {
|
||||
populationsByCollection[incomingData.relationTo] = []
|
||||
}
|
||||
|
||||
populationsByCollection[incomingData.relationTo].push({
|
||||
id: incomingData[key],
|
||||
accessor: 'value',
|
||||
apiRoute,
|
||||
collection: incomingData.relationTo,
|
||||
depth,
|
||||
ref: result,
|
||||
serverURL,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
result[key] = traverseRichText({
|
||||
apiRoute,
|
||||
depth,
|
||||
incomingData: incomingData[key],
|
||||
populationPromises,
|
||||
populationsByCollection,
|
||||
result: result[key],
|
||||
serverURL,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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<string, unknown>
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user