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 { fieldSchemaToJSON } from 'payload/utilities'
|
||||||
|
|
||||||
|
import type { PopulationsByCollection } from './types'
|
||||||
|
|
||||||
import { traverseFields } from './traverseFields'
|
import { traverseFields } from './traverseFields'
|
||||||
|
|
||||||
export const mergeData = async <T>(args: {
|
export const mergeData = async <T>(args: {
|
||||||
@@ -27,22 +30,51 @@ export const mergeData = async <T>(args: {
|
|||||||
|
|
||||||
const result = { ...initialData }
|
const result = { ...initialData }
|
||||||
|
|
||||||
const populationPromises: Promise<void>[] = []
|
const populationsByCollection: PopulationsByCollection = {}
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
incomingData,
|
incomingData,
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result,
|
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 {
|
return {
|
||||||
...result,
|
...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 type { fieldSchemaToJSON } from 'payload/utilities'
|
||||||
|
|
||||||
import { promise } from './promise'
|
import type { PopulationsByCollection } from './types'
|
||||||
|
|
||||||
import { traverseRichText } from './traverseRichText'
|
import { traverseRichText } from './traverseRichText'
|
||||||
|
|
||||||
export const traverseFields = <T>(args: {
|
export const traverseFields = <T>(args: {
|
||||||
apiRoute?: string
|
|
||||||
depth?: number
|
|
||||||
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||||
incomingData: T
|
incomingData: T
|
||||||
populationPromises: Promise<void>[]
|
populationsByCollection: PopulationsByCollection
|
||||||
result: T
|
result: T
|
||||||
serverURL: string
|
|
||||||
}): void => {
|
}): void => {
|
||||||
const {
|
const { fieldSchema: fieldSchemas, incomingData, populationsByCollection, result } = args
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
fieldSchema: fieldSchemas,
|
|
||||||
incomingData,
|
|
||||||
populationPromises,
|
|
||||||
result,
|
|
||||||
serverURL,
|
|
||||||
} = args
|
|
||||||
|
|
||||||
fieldSchemas.forEach((fieldSchema) => {
|
fieldSchemas.forEach((fieldSchema) => {
|
||||||
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
|
if ('name' in fieldSchema && typeof fieldSchema.name === 'string') {
|
||||||
@@ -29,12 +19,9 @@ export const traverseFields = <T>(args: {
|
|||||||
switch (fieldSchema.type) {
|
switch (fieldSchema.type) {
|
||||||
case 'richText':
|
case 'richText':
|
||||||
result[fieldName] = traverseRichText({
|
result[fieldName] = traverseRichText({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
incomingData: incomingData[fieldName],
|
incomingData: incomingData[fieldName],
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result: result[fieldName],
|
result: result[fieldName],
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -51,13 +38,10 @@ export const traverseFields = <T>(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
fieldSchema: fieldSchema.fields,
|
fieldSchema: fieldSchema.fields,
|
||||||
incomingData: incomingRow,
|
incomingData: incomingRow,
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result: result[fieldName][i],
|
result: result[fieldName][i],
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return result[fieldName][i]
|
return result[fieldName][i]
|
||||||
@@ -86,13 +70,10 @@ export const traverseFields = <T>(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
fieldSchema: incomingBlockJSON.fields,
|
fieldSchema: incomingBlockJSON.fields,
|
||||||
incomingData: incomingBlock,
|
incomingData: incomingBlock,
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result: result[fieldName][i],
|
result: result[fieldName][i],
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return result[fieldName][i]
|
return result[fieldName][i]
|
||||||
@@ -110,13 +91,10 @@ export const traverseFields = <T>(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
fieldSchema: fieldSchema.fields,
|
fieldSchema: fieldSchema.fields,
|
||||||
incomingData: incomingData[fieldName] || {},
|
incomingData: incomingData[fieldName] || {},
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result: result[fieldName],
|
result: result[fieldName],
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -146,32 +124,28 @@ export const traverseFields = <T>(args: {
|
|||||||
const newRelation = incomingRelation.relationTo
|
const newRelation = incomingRelation.relationTo
|
||||||
|
|
||||||
if (oldID !== newID || oldRelation !== newRelation) {
|
if (oldID !== newID || oldRelation !== newRelation) {
|
||||||
populationPromises.push(
|
if (!populationsByCollection[newRelation]) {
|
||||||
promise({
|
populationsByCollection[newRelation] = []
|
||||||
id: incomingRelation.value,
|
}
|
||||||
accessor: 'value',
|
|
||||||
apiRoute,
|
populationsByCollection[newRelation].push({
|
||||||
collection: newRelation,
|
id: incomingRelation.value,
|
||||||
depth,
|
accessor: 'value',
|
||||||
ref: result[fieldName][i],
|
ref: result[fieldName][i],
|
||||||
serverURL,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle `hasMany` monomorphic
|
// Handle `hasMany` monomorphic
|
||||||
if (result[fieldName][i]?.id !== incomingRelation) {
|
if (result[fieldName][i]?.id !== incomingRelation) {
|
||||||
populationPromises.push(
|
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||||
promise({
|
populationsByCollection[fieldSchema.relationTo] = []
|
||||||
id: incomingRelation,
|
}
|
||||||
accessor: i,
|
|
||||||
apiRoute,
|
populationsByCollection[fieldSchema.relationTo].push({
|
||||||
collection: String(fieldSchema.relationTo),
|
id: incomingRelation,
|
||||||
depth,
|
accessor: i,
|
||||||
ref: result[fieldName],
|
ref: result[fieldName],
|
||||||
serverURL,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -217,17 +191,15 @@ export const traverseFields = <T>(args: {
|
|||||||
// if the new value is not empty, populate it
|
// if the new value is not empty, populate it
|
||||||
// otherwise set the value to null
|
// otherwise set the value to null
|
||||||
if (newID) {
|
if (newID) {
|
||||||
populationPromises.push(
|
if (!populationsByCollection[newRelation]) {
|
||||||
promise({
|
populationsByCollection[newRelation] = []
|
||||||
id: newID,
|
}
|
||||||
accessor: 'value',
|
|
||||||
apiRoute,
|
populationsByCollection[newRelation].push({
|
||||||
collection: newRelation,
|
id: newID,
|
||||||
depth,
|
accessor: 'value',
|
||||||
ref: result[fieldName],
|
ref: result[fieldName],
|
||||||
serverURL,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
result[fieldName] = null
|
result[fieldName] = null
|
||||||
}
|
}
|
||||||
@@ -252,17 +224,15 @@ export const traverseFields = <T>(args: {
|
|||||||
// if the new value is not empty, populate it
|
// if the new value is not empty, populate it
|
||||||
// otherwise set the value to null
|
// otherwise set the value to null
|
||||||
if (newID) {
|
if (newID) {
|
||||||
populationPromises.push(
|
if (!populationsByCollection[fieldSchema.relationTo]) {
|
||||||
promise({
|
populationsByCollection[fieldSchema.relationTo] = []
|
||||||
id: newID,
|
}
|
||||||
accessor: fieldName,
|
|
||||||
apiRoute,
|
populationsByCollection[fieldSchema.relationTo].push({
|
||||||
collection: String(fieldSchema.relationTo),
|
id: newID,
|
||||||
depth,
|
accessor: fieldName,
|
||||||
ref: result as Record<string, unknown>,
|
ref: result as Record<string, unknown>,
|
||||||
serverURL,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
result[fieldName] = null
|
result[fieldName] = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { promise } from './promise'
|
import type { PopulationsByCollection } from './types'
|
||||||
|
|
||||||
export const traverseRichText = ({
|
export const traverseRichText = ({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
incomingData,
|
incomingData,
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result,
|
result,
|
||||||
serverURL,
|
|
||||||
}: {
|
}: {
|
||||||
apiRoute: string
|
|
||||||
depth: number
|
|
||||||
incomingData: any
|
incomingData: any
|
||||||
populationPromises: Promise<any>[]
|
populationsByCollection: PopulationsByCollection
|
||||||
result: any
|
result: any
|
||||||
serverURL: string
|
|
||||||
}): any => {
|
}): any => {
|
||||||
if (Array.isArray(incomingData)) {
|
if (Array.isArray(incomingData)) {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -26,12 +20,9 @@ export const traverseRichText = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return traverseRichText({
|
return traverseRichText({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
incomingData: item,
|
incomingData: item,
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result: result[index],
|
result: result[index],
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else if (incomingData && typeof incomingData === 'object') {
|
} else if (incomingData && typeof incomingData === 'object') {
|
||||||
@@ -68,26 +59,21 @@ export const traverseRichText = ({
|
|||||||
const needsPopulation = !result.value || typeof result.value !== 'object'
|
const needsPopulation = !result.value || typeof result.value !== 'object'
|
||||||
|
|
||||||
if (needsPopulation) {
|
if (needsPopulation) {
|
||||||
populationPromises.push(
|
if (!populationsByCollection[incomingData.relationTo]) {
|
||||||
promise({
|
populationsByCollection[incomingData.relationTo] = []
|
||||||
id: incomingData[key],
|
}
|
||||||
accessor: 'value',
|
|
||||||
apiRoute,
|
populationsByCollection[incomingData.relationTo].push({
|
||||||
collection: incomingData.relationTo,
|
id: incomingData[key],
|
||||||
depth,
|
accessor: 'value',
|
||||||
ref: result,
|
ref: result,
|
||||||
serverURL,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result[key] = traverseRichText({
|
result[key] = traverseRichText({
|
||||||
apiRoute,
|
|
||||||
depth,
|
|
||||||
incomingData: incomingData[key],
|
incomingData: incomingData[key],
|
||||||
populationPromises,
|
populationsByCollection,
|
||||||
result: result[key],
|
result: result[key],
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
export type LivePreviewArgs = {}
|
export type LivePreviewArgs = {}
|
||||||
|
|
||||||
export type LivePreview = void
|
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,
|
returnNumberOfRequests: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(merge1._numberOfRequests).toEqual(4)
|
expect(merge1._numberOfRequests).toEqual(1)
|
||||||
expect(merge1.arrayOfRelationships).toHaveLength(1)
|
expect(merge1.arrayOfRelationships).toHaveLength(1)
|
||||||
expect(merge1.arrayOfRelationships).toMatchObject([
|
expect(merge1.arrayOfRelationships).toMatchObject([
|
||||||
{
|
{
|
||||||
@@ -396,7 +396,7 @@ describe('Collections - Live Preview', () => {
|
|||||||
returnNumberOfRequests: true,
|
returnNumberOfRequests: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(merge2._numberOfRequests).toEqual(4)
|
expect(merge2._numberOfRequests).toEqual(1)
|
||||||
expect(merge2.arrayOfRelationships).toHaveLength(2)
|
expect(merge2.arrayOfRelationships).toHaveLength(2)
|
||||||
expect(merge2.arrayOfRelationships).toMatchObject([
|
expect(merge2.arrayOfRelationships).toMatchObject([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user