diff --git a/packages/live-preview-sdk/src/graphql/entries.ts b/packages/live-preview-sdk/src/graphql/entries.ts index 35d6188a..015e6ee6 100644 --- a/packages/live-preview-sdk/src/graphql/entries.ts +++ b/packages/live-preview-sdk/src/graphql/entries.ts @@ -14,17 +14,18 @@ import { SUPPORTED_RICHTEXT_EMBEDS, isAsset, isRichText } from '../helpers/entit import { CollectionItem, SysProps, - EntityReferenceMap, Entity, ASSET_TYPENAME, UpdateFieldProps, UpdateReferenceFieldProps, UpdateEntryProps, GraphQLParams, + ReferenceMap, } from '../types'; import { updateAsset } from './assets'; import { isRelevantField, updateAliasedInformation } from './queryUtils'; import { buildCollectionName, generateTypeName } from './utils'; +import { MAX_DEPTH } from '../constants'; /** * Updates GraphQL response data based on CMA entry object @@ -40,9 +41,10 @@ export async function updateEntry({ dataFromPreviewApp, updateFromEntryEditor, locale, - entityReferenceMap, gqlParams, sendMessage, + depth, + referenceMap, }: UpdateEntryProps): Promise { if (dataFromPreviewApp.sys.id !== updateFromEntryEditor.sys.id) { return dataFromPreviewApp; @@ -63,11 +65,12 @@ export async function updateEntry({ await updateRichTextField({ dataFromPreviewApp: copyOfDataFromPreviewApp, updateFromEntryEditor, - entityReferenceMap, name, locale, gqlParams, sendMessage, + depth, + referenceMap, }); } else if (field.type === 'Link') { await updateSingleRefField({ @@ -75,9 +78,10 @@ export async function updateEntry({ updateFromEntryEditor, name, locale, - entityReferenceMap, gqlParams, sendMessage, + depth, + referenceMap, }); } else if (field.type === 'Array' && field.items?.type === 'Link') { await updateMultiRefField({ @@ -85,9 +89,10 @@ export async function updateEntry({ updateFromEntryEditor, name, locale, - entityReferenceMap, gqlParams, sendMessage, + depth, + referenceMap, }); } } @@ -106,15 +111,21 @@ function isEntityLinkEmpty(obj: RichTextLink) { return Object.values(obj).every((arr) => arr.length === 0); } -async function processNode( - node: any, - entries: RichTextLink, - assets: RichTextLink, - entityReferenceMap: EntityReferenceMap, - locale: string, - sendMessage: SendMessage, - gqlParams?: GraphQLParams -) { +async function processNode({ + node, + entries, + assets, + ...props +}: { + node: any; + entries: RichTextLink; + assets: RichTextLink; + locale: string; + sendMessage: SendMessage; + gqlParams?: GraphQLParams; + depth: number; + referenceMap: ReferenceMap; +}) { // Check if the node is an embedded entity if (SUPPORTED_RICHTEXT_EMBEDS.includes(node.nodeType)) { if (node.data && node.data.target && node.data.target.sys) { @@ -126,21 +137,15 @@ async function processNode( // Use the updateReferenceEntryField or updateReferenceAssetField function to resolve the entity reference if (node.data.target.sys.linkType === 'Entry') { ref = await updateReferenceEntryField({ + ...props, referenceFromPreviewApp: null, updatedReference, - entityReferenceMap, - locale, - gqlParams, - sendMessage, }); } else if (node.data.target.sys.linkType === 'Asset') { ref = await updateReferenceAssetField({ + ...props, referenceFromPreviewApp: null, updatedReference, - entityReferenceMap, - locale, - gqlParams, - sendMessage, }); } @@ -174,32 +179,38 @@ async function processNode( // since embedded entries can be part of other rich text content (e.g. embedded inline entries) // we need to recursively check for these entries to display them for (const contentNode of node.content) { - await processNode( - contentNode, + await processNode({ + ...props, + node: contentNode, entries, assets, - entityReferenceMap, - locale, - sendMessage, - gqlParams - ); + }); } } } -async function processRichTextField( - richTextNode: any | null, - entityReferenceMap: EntityReferenceMap, - locale: string, - sendMessage: SendMessage, - gqlParams?: GraphQLParams -): Promise<{ entries: RichTextLink; assets: RichTextLink }> { +async function processRichTextField({ + richTextNode, + ...props +}: { + richTextNode: any | null; + locale: string; + sendMessage: SendMessage; + gqlParams?: GraphQLParams; + depth: number; + referenceMap: ReferenceMap; +}): Promise<{ entries: RichTextLink; assets: RichTextLink }> { const entries: RichTextLink = { block: [], inline: [], hyperlink: [] }; const assets: RichTextLink = { block: [], inline: [], hyperlink: [] }; if (richTextNode) { for (const node of richTextNode.content) { - await processNode(node, entries, assets, entityReferenceMap, locale, sendMessage, gqlParams); + await processNode({ + ...props, + node, + entries, + assets, + }); } } @@ -214,9 +225,10 @@ async function updateRichTextField({ updateFromEntryEditor, name, locale, - entityReferenceMap, gqlParams, sendMessage, + depth, + referenceMap, }: UpdateFieldProps) { if (!dataFromPreviewApp[name]) { dataFromPreviewApp[name] = {}; @@ -227,29 +239,30 @@ async function updateRichTextField({ updateFromEntryEditor?.fields?.[name] || null; // Update the rich text embedded entries - dataFromPreviewApp[name].links = await processRichTextField( - dataFromPreviewApp[name].json, - entityReferenceMap, + dataFromPreviewApp[name].links = await processRichTextField({ + richTextNode: dataFromPreviewApp[name].json, locale, sendMessage, - gqlParams - ); + gqlParams, + depth, + referenceMap, + }); } async function updateReferenceAssetField({ referenceFromPreviewApp, updatedReference, - entityReferenceMap, locale, gqlParams, sendMessage, + referenceMap, }: SetOptional, 'gqlParams'>) { const { reference } = await resolveReference({ - entityReferenceMap, referenceId: updatedReference.sys.id, isAsset: true, locale, sendMessage, + referenceMap, }); return updateAsset( @@ -263,19 +276,24 @@ async function updateReferenceAssetField({ ); } +function isInDepthLimit(depth: number, gqlParams?: GraphQLParams): boolean { + return !!gqlParams || depth < MAX_DEPTH; +} + async function updateReferenceEntryField({ referenceFromPreviewApp, updatedReference, - entityReferenceMap, locale, gqlParams, sendMessage, + depth, + referenceMap, }: SetOptional, 'gqlParams'>) { const { reference, typeName } = await resolveReference({ - entityReferenceMap, referenceId: updatedReference.sys.id, locale, sendMessage, + referenceMap, }); // If we have the typename of the updated reference, we can work with it @@ -296,29 +314,31 @@ async function updateReferenceEntryField({ if (isRichText(value)) { // richtext merged[key] = { json: value }; - merged[key].links = await processRichTextField( - value, - entityReferenceMap, + merged[key].links = await processRichTextField({ + richTextNode: value, locale, sendMessage, - gqlParams - ); + gqlParams, + depth, + referenceMap, + }); } - if ('sys' in value) { + if ('sys' in value && isInDepthLimit(depth, gqlParams)) { // single reference merged[key] = value; await updateSingleRefField({ dataFromPreviewApp: merged, updateFromEntryEditor: reference, locale, - entityReferenceMap, name: key, gqlParams, sendMessage, + depth: depth + 1, + referenceMap, }); } - } else if (Array.isArray(value) && value[0]?.sys) { + } else if (Array.isArray(value) && value[0]?.sys && isInDepthLimit(depth, gqlParams)) { // multi references const name = buildCollectionName(key); merged[name] = { items: value }; @@ -326,10 +346,11 @@ async function updateReferenceEntryField({ dataFromPreviewApp: merged, updateFromEntryEditor: reference, locale, - entityReferenceMap, name: key, gqlParams, sendMessage, + depth: depth + 1, + referenceMap, }); } else { // primitive fields @@ -343,10 +364,11 @@ async function updateReferenceEntryField({ async function updateReferenceField({ referenceFromPreviewApp, updatedReference, - entityReferenceMap, locale, gqlParams, sendMessage, + depth, + referenceMap, }: UpdateReferenceFieldProps) { if (!updatedReference) { return null; @@ -365,20 +387,22 @@ async function updateReferenceField({ return updateReferenceAssetField({ referenceFromPreviewApp, updatedReference, - entityReferenceMap, locale, gqlParams, sendMessage, + depth, + referenceMap, }); } return updateReferenceEntryField({ referenceFromPreviewApp, updatedReference, - entityReferenceMap, locale, gqlParams, sendMessage, + depth, + referenceMap, }); } @@ -387,9 +411,10 @@ async function updateSingleRefField({ updateFromEntryEditor, name, locale, - entityReferenceMap, gqlParams, sendMessage, + depth, + referenceMap, }: UpdateFieldProps) { const updatedReference = updateFromEntryEditor?.fields?.[name] as Asset | Entry | undefined; dataFromPreviewApp[name] = await updateReferenceField({ @@ -397,10 +422,11 @@ async function updateSingleRefField({ __typename?: string; }, updatedReference, - entityReferenceMap: entityReferenceMap as EntityReferenceMap, locale, gqlParams, sendMessage, + depth, + referenceMap, }); } @@ -409,9 +435,10 @@ async function updateMultiRefField({ updateFromEntryEditor, name, locale, - entityReferenceMap, gqlParams, sendMessage, + depth, + referenceMap, }: UpdateFieldProps) { const fieldName = buildCollectionName(name); @@ -427,10 +454,11 @@ async function updateMultiRefField({ __typename?: string; }, updatedReference: updatedItem, - entityReferenceMap: entityReferenceMap as EntityReferenceMap, locale, gqlParams, sendMessage, + depth, + referenceMap, }); return result; diff --git a/packages/live-preview-sdk/src/helpers/resolveReference.ts b/packages/live-preview-sdk/src/helpers/resolveReference.ts index ebb034bb..a38fae8b 100644 --- a/packages/live-preview-sdk/src/helpers/resolveReference.ts +++ b/packages/live-preview-sdk/src/helpers/resolveReference.ts @@ -7,7 +7,7 @@ import { import type { Asset, Entry } from 'contentful'; import { generateTypeName } from '../graphql/utils'; -import { ASSET_TYPENAME, EntityReferenceMap } from '../types'; +import { ASSET_TYPENAME, ReferenceMap } from '../types'; const store: Record = {}; @@ -42,39 +42,39 @@ function getStore(locale: string, sendMessage: SendMessage): EditorEntityStore { } export async function resolveReference(info: { - entityReferenceMap: EntityReferenceMap; referenceId: string; locale: string; sendMessage: SendMessage; + referenceMap: ReferenceMap; }): Promise<{ reference: Entry; typeName: string }>; export async function resolveReference(info: { - entityReferenceMap: EntityReferenceMap; referenceId: string; isAsset: true; locale: string; sendMessage: SendMessage; + referenceMap: ReferenceMap; }): Promise<{ reference: Asset; typeName: string }>; /** * Returns the requested reference from - * 1) the entityReferenceMap if it was already resolved once + * 1) the referenceMap if it was already resolved once * 2) loads it from the editor directly */ export async function resolveReference({ - entityReferenceMap, referenceId, isAsset, locale, sendMessage, + referenceMap, }: { - entityReferenceMap: EntityReferenceMap; referenceId: string; isAsset?: boolean; locale: string; sendMessage: SendMessage; + referenceMap: ReferenceMap; }): Promise<{ reference: Entry | Asset; typeName: string }> { - const reference = entityReferenceMap.get(referenceId); - + const reference = referenceMap.get(referenceId); if (reference) { + referenceMap.set(referenceId, reference); return { reference, typeName: @@ -101,6 +101,7 @@ export async function resolveReference({ throw new Error(`Unknown reference ${referenceId}`); } + referenceMap.set(referenceId, result); return { reference: result, typeName: generateTypeName(result.sys.contentType.sys.id), diff --git a/packages/live-preview-sdk/src/liveUpdates.ts b/packages/live-preview-sdk/src/liveUpdates.ts index 2a66c21a..6c014654 100644 --- a/packages/live-preview-sdk/src/liveUpdates.ts +++ b/packages/live-preview-sdk/src/liveUpdates.ts @@ -20,10 +20,10 @@ import { ContentType, Entity, EntityWithSys, - EntityReferenceMap, hasSysInformation, Subscription, GraphQLParams, + ReferenceMap, } from './types'; interface MergeEntityProps { @@ -31,7 +31,6 @@ interface MergeEntityProps { locale: string; updateFromEntryEditor: Entry | Asset; contentType: ContentType; - entityReferenceMap: EntityReferenceMap; gqlParams?: GraphQLParams; } @@ -48,6 +47,7 @@ export class LiveUpdates { private storage: StorageMap; private defaultLocale: string; private sendMessage: (method: PostMessageMethods, data: EditorMessage) => void; + private referenceMap: ReferenceMap = new Map(); constructor({ locale, targetOrigin }: { locale: string; targetOrigin: string[] }) { this.defaultLocale = locale; @@ -59,7 +59,6 @@ export class LiveUpdates { private async mergeEntity({ contentType, dataFromPreviewApp, - entityReferenceMap, locale, updateFromEntryEditor, gqlParams, @@ -69,6 +68,8 @@ export class LiveUpdates { data: Entity; updated: boolean; }> { + const depth = 0; + if ('__typename' in dataFromPreviewApp) { // GraphQL const data = await (dataFromPreviewApp.__typename === 'Asset' @@ -78,9 +79,10 @@ export class LiveUpdates { dataFromPreviewApp, updateFromEntryEditor: updateFromEntryEditor as Entry, locale, - entityReferenceMap, gqlParams, sendMessage: this.sendMessage, + depth, + referenceMap: this.referenceMap, })); return { @@ -91,17 +93,14 @@ export class LiveUpdates { if (this.isCfEntity(dataFromPreviewApp)) { // REST - const depth = 0; - const visitedReferenceMap = new Map(); return { data: await rest.updateEntity( contentType, dataFromPreviewApp as Entry, updateFromEntryEditor as Entry, locale, - entityReferenceMap, depth, - visitedReferenceMap, + this.referenceMap, this.sendMessage ), updated: true, @@ -201,6 +200,10 @@ export class LiveUpdates { ) { const { entity, contentType, entityReferenceMap } = message as EntryUpdatedMessage; + for (const [key, value] of entityReferenceMap.entries()) { + this.referenceMap.set(key, value); + } + await Promise.all( [...this.subscriptions].map(async ([, s]) => { try { @@ -212,7 +215,6 @@ export class LiveUpdates { locale: s.locale || this.defaultLocale, updateFromEntryEditor: entity, contentType: contentType, - entityReferenceMap: entityReferenceMap, gqlParams: s.gqlParams, }); diff --git a/packages/live-preview-sdk/src/rest/entities.ts b/packages/live-preview-sdk/src/rest/entities.ts index e6adc5de..d1144880 100644 --- a/packages/live-preview-sdk/src/rest/entities.ts +++ b/packages/live-preview-sdk/src/rest/entities.ts @@ -17,7 +17,7 @@ import { isResourceLink, isRichText, } from '../helpers/entities'; -import { ContentType, EntityReferenceMap } from '../types'; +import { ContentType, ReferenceMap } from '../types'; export type Reference = Asset | Entry | WithResourceName; @@ -34,39 +34,24 @@ function getFieldName(contentType: ContentType, field: ContentType['fields'][num } /** - * Update the reference from the entry editor with the information from the entityReferenceMap. + * Update the reference from the entry editor with the information from the referenceMap. * If the information is not yet available, it send a message to the editor to retrieve it. */ async function updateRef( dataFromPreviewApp: Reference | undefined, updateFromEntryEditor: Reference, locale: string, - entityReferenceMap: EntityReferenceMap, depth: number, - visitedReferenceMap: Map, + referenceMap: Map, sendMessage: SendMessage ): Promise { - let reference; - const id = - 'urn' in updateFromEntryEditor.sys - ? updateFromEntryEditor.sys.urn - : updateFromEntryEditor.sys.id; - - // If the ID of the updateFromEntryEditor is in visitedReferences, then stop the recursion - if (visitedReferenceMap.has(id)) { - debug.warn('Detected a circular reference, stopping recursion'); - reference = visitedReferenceMap.get(id); - } else { - const { reference: resolvedReference } = await resolveReference({ - entityReferenceMap, - referenceId: updateFromEntryEditor.sys.id, - ...(isAsset(updateFromEntryEditor) ? { isAsset: true } : undefined), - locale, - sendMessage, - }); - reference = resolvedReference; - visitedReferenceMap.set(id, resolvedReference); - } + const { reference } = await resolveReference({ + referenceId: updateFromEntryEditor.sys.id, + ...(isAsset(updateFromEntryEditor) ? { isAsset: true } : undefined), + locale, + sendMessage, + referenceMap, + }); if (!reference) { return dataFromPreviewApp; @@ -86,9 +71,8 @@ async function updateRef( reference, locale, key as keyof Reference['fields'], - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ); // multi ref fields @@ -98,9 +82,8 @@ async function updateRef( reference, locale, key as keyof Reference['fields'], - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ); // rich text fields @@ -110,9 +93,8 @@ async function updateRef( reference as Entry, key, locale, - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ); // single and multi resource link fields @@ -139,9 +121,8 @@ async function updateMultiRefField( updateFromEntryEditor: Reference, locale: string, name: keyof Reference['fields'], - entityReferenceMap: EntityReferenceMap, depth: number, - visitedReferenceMap: Map, + referenceMap: Map, sendMessage: SendMessage ) { if (!updateFromEntryEditor.fields?.[name]) { @@ -156,9 +137,8 @@ async function updateMultiRefField( (dataFromPreviewApp.fields[name] as Reference[])?.[index], updateFromEntryReference, locale, - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ) ) @@ -173,9 +153,8 @@ async function updateSingleRefField( updateFromEntryEditor: Reference, locale: string, name: keyof Reference['fields'], - entityReferenceMap: EntityReferenceMap, depth: number, - visitedReferenceMap: Map, + referenceMap: Map, sendMessage: SendMessage ) { const matchUpdateFromEntryEditor = updateFromEntryEditor?.fields?.[name] as Reference | undefined; @@ -191,9 +170,8 @@ async function updateSingleRefField( dataFromPreviewApp.fields[name] as Reference | undefined, matchUpdateFromEntryEditor, locale, - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ); @@ -202,10 +180,9 @@ async function updateSingleRefField( async function resolveRichTextLinks( node: any, - entityReferenceMap: EntityReferenceMap, locale: string, depth: number, - visitedReferenceMap: Map, + referenceMap: Map, sendMessage: SendMessage ) { if (SUPPORTED_RICHTEXT_EMBEDS.includes(node.nodeType)) { @@ -220,9 +197,8 @@ async function resolveRichTextLinks( undefined, updatedReference, locale, - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ); } @@ -231,14 +207,7 @@ async function resolveRichTextLinks( if (node.content) { for (const childNode of node.content) { - await resolveRichTextLinks( - childNode, - entityReferenceMap, - locale, - depth + 1, -visitedReferenceMap, - sendMessage - ); + await resolveRichTextLinks(childNode, locale, depth + 1, referenceMap, sendMessage); } } } @@ -248,9 +217,8 @@ async function updateRichTextField( updateFromEntryEditor: Entry, name: string, locale: string, - entityReferenceMap: EntityReferenceMap, depth: number, - visitedReferenceMap: Map, + referenceMap: Map, sendMessage: SendMessage ) { const richText = updateFromEntryEditor.fields?.[name]; @@ -260,14 +228,7 @@ async function updateRichTextField( dataFromPreviewApp.fields[name] = richText; // Resolve the linked entries or assets within the rich text field for (const node of richText.content) { - await resolveRichTextLinks( - node, - entityReferenceMap, - locale, - depth, - visitedReferenceMap, - sendMessage - ); + await resolveRichTextLinks(node, locale, depth, referenceMap, sendMessage); } } } @@ -275,10 +236,6 @@ async function updateRichTextField( /** * Updates REST response data based on CMA entry object * - * @param contentType - * @param dataFromPreviewApp The REST response to be updated - * @param updateFromEntryEditor CMA entry object containing the update - * @param locale Locale code * @returns Updated REST response data */ export async function updateEntity( @@ -286,9 +243,8 @@ export async function updateEntity( dataFromPreviewApp: Entry, updateFromEntryEditor: Entry | Asset, locale: string, - entityReferenceMap: EntityReferenceMap, depth: number, - visitedReferenceMap: Map, + referenceMap: ReferenceMap, sendMessage: SendMessage ): Promise { if (dataFromPreviewApp.sys.id !== updateFromEntryEditor.sys.id) { @@ -310,9 +266,8 @@ export async function updateEntity( updateFromEntryEditor, locale, name as keyof Reference['fields'], - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ); } else if (field.type === 'Array' && field.items?.type === 'Link' && depth < MAX_DEPTH) { @@ -321,9 +276,8 @@ export async function updateEntity( updateFromEntryEditor, locale, name as keyof Reference['fields'], - entityReferenceMap, depth + 1, - visitedReferenceMap, + referenceMap, sendMessage ); } else if (field.type === 'RichText') { @@ -332,9 +286,8 @@ export async function updateEntity( updateFromEntryEditor as Entry, name, locale, - entityReferenceMap, depth, - visitedReferenceMap, + referenceMap, sendMessage ); } else if (field.type === 'ResourceLink') { diff --git a/packages/live-preview-sdk/src/types.ts b/packages/live-preview-sdk/src/types.ts index 8da34e43..a3589848 100644 --- a/packages/live-preview-sdk/src/types.ts +++ b/packages/live-preview-sdk/src/types.ts @@ -56,16 +56,17 @@ export interface CollectionItem { __typename?: string; } -export class EntityReferenceMap extends Map {} +export type ReferenceMap = Map; export type UpdateEntryProps = { contentType: ContentType; dataFromPreviewApp: Entity & { sys: SysProps }; updateFromEntryEditor: Entry; locale: string; - entityReferenceMap: EntityReferenceMap; gqlParams?: GraphQLParams; sendMessage: SendMessage; + depth: number; + referenceMap: ReferenceMap; }; export type UpdateFieldProps = { @@ -73,18 +74,20 @@ export type UpdateFieldProps = { updateFromEntryEditor: Entry; name: string; locale: string; - entityReferenceMap: EntityReferenceMap; gqlParams?: GraphQLParams; sendMessage: SendMessage; + depth: number; + referenceMap: ReferenceMap; }; export type UpdateReferenceFieldProps = { referenceFromPreviewApp: (Entity & { __typename?: string }) | null | undefined; updatedReference?: (Pick | Pick) & { __typename?: string }; - entityReferenceMap: EntityReferenceMap; locale: string; gqlParams?: GraphQLParams; sendMessage: SendMessage; + depth: number; + referenceMap: ReferenceMap; }; /**