diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 37a63cd0d453a..adc812b49ff61 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -33,8 +33,6 @@ import { SAVED_BOOK_ID } from './react_embeddables/saved_book/constants'; import { registerCreateSavedBookAction } from './react_embeddables/saved_book/create_saved_book_action'; import { registerAddSearchPanelAction } from './react_embeddables/search/register_add_search_panel_action'; import { registerSearchEmbeddable } from './react_embeddables/search/register_search_embeddable'; -import { bookCmDefinitions } from '../common/book/content_management/cm_services'; -import { fieldListCmDefinitions } from '../common/field_list/content_management/cm_services'; import { BOOK_CONTENT_ID, BOOK_LATEST_VERSION } from '../common/book/content_management/schema'; import { setKibanaServices } from './kibana_services'; import { BookSerializedState } from './react_embeddables/saved_book/types'; @@ -103,8 +101,16 @@ export class EmbeddableExamplesPlugin implements Plugin startServicesPromise.then(([_, startDeps]) => resolve(startDeps))) ); - embeddable.registerEmbeddableContentManagementDefinition(bookCmDefinitions); - embeddable.registerEmbeddableContentManagementDefinition(fieldListCmDefinitions); + embeddable.registerEmbeddableContentManagementDefinition('book', async () => { + const { bookCmDefinitions } = await import('../common/book/content_management/cm_services'); + return bookCmDefinitions; + }); + embeddable.registerEmbeddableContentManagementDefinition('field_list', async () => { + const { fieldListCmDefinitions } = await import( + '../common/field_list/content_management/cm_services' + ); + return fieldListCmDefinitions; + }); contentManagement.registry.register({ id: BOOK_CONTENT_ID, diff --git a/examples/embeddable_examples/server/plugin.ts b/examples/embeddable_examples/server/plugin.ts index 97bd34c674382..57373c3325c60 100644 --- a/examples/embeddable_examples/server/plugin.ts +++ b/examples/embeddable_examples/server/plugin.ts @@ -44,7 +44,9 @@ export class EmbeddableExamplesPlugin implements Plugin + Promise.resolve(bookCmDefinitionsWithSchemas) + ); return {}; } diff --git a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts index 74a7ee43dc8fa..a27d9eeffaab8 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts @@ -196,10 +196,15 @@ export class DashboardStorage { outcome, } = await soClient.resolve(DASHBOARD_SAVED_OBJECT_TYPE, id); - const { item, error: itemError } = savedObjectToItem(savedObject, this.embeddable, false, { - getTagNamesFromReferences: (references: SavedObjectReference[]) => - this.getTagNamesFromReferences(references, allTags), - }); + const { item, error: itemError } = await savedObjectToItem( + savedObject, + this.embeddable, + false, + { + getTagNamesFromReferences: (references: SavedObjectReference[]) => + this.getTagNamesFromReferences(references, allTags), + } + ); if (itemError) { throw Boom.badRequest(`Invalid response. ${itemError.message}`); } @@ -286,10 +291,15 @@ export class DashboardStorage { { ...optionsToLatest, references: soReferences } ); - const { item, error: itemError } = savedObjectToItem(savedObject, this.embeddable, false, { - getTagNamesFromReferences: (references: SavedObjectReference[]) => - this.getTagNamesFromReferences(references, allTags), - }); + const { item, error: itemError } = await savedObjectToItem( + savedObject, + this.embeddable, + false, + { + getTagNamesFromReferences: (references: SavedObjectReference[]) => + this.getTagNamesFromReferences(references, allTags), + } + ); if (itemError) { throw Boom.badRequest(`Invalid response. ${itemError.message}`); } @@ -373,7 +383,7 @@ export class DashboardStorage { } ); - const { item, error: itemError } = savedObjectToItem( + const { item, error: itemError } = await savedObjectToItem( partialSavedObject, this.embeddable, true, @@ -448,7 +458,7 @@ export class DashboardStorage { const hits = await Promise.all( soResponse.saved_objects .map(async (so) => { - const { item } = savedObjectToItem(so, this.embeddable, false, { + const { item } = await savedObjectToItem(so, this.embeddable, false, { allowedAttributes: soQuery.fields, allowedReferences: optionsToLatest?.includeReferences, getTagNamesFromReferences: (references: SavedObjectReference[]) => diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts index e0d61f40ebc3a..4657985156a88 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/cm_services.ts @@ -33,7 +33,7 @@ import { DEFAULT_PANEL_WIDTH, DEFAULT_DASHBOARD_OPTIONS, } from '../../../common/content_management'; -import { getResultV3ToV2 } from './transform_utils'; +// import { getResultV3ToV2 } from './transform_utils'; const apiError = schema.object({ error: schema.string(), @@ -230,7 +230,7 @@ const searchSourceSchema = schema.object( { defaultValue: {}, unknowns: 'allow' } ); -const sectionGridDataSchema = schema.object({ +export const sectionGridDataSchema = schema.object({ y: schema.number({ meta: { description: 'The y coordinate of the section in grid units' } }), i: schema.maybe( schema.string({ @@ -564,8 +564,8 @@ export const getServiceDefinition = (embeddable: EmbeddableStart): ServicesDefin out: { result: { schema: dashboardGetResultSchema, - // TODO Ignoring references for now... - down: (data) => getResultV3ToV2(data, embeddable), + // TODO Ignoring down transforms for now since it needs to be a promise. + // down: (data) => getResultV3ToV2(data, embeddable), }, }, }, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts index dba2794f190be..f46700c97b713 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transform_utils.ts @@ -36,12 +36,12 @@ import type { SavedObjectToItemReturn, } from './types'; -export function dashboardAttributesOut( +export async function dashboardAttributesOut( attributes: DashboardSavedObjectAttributes | Partial, embeddable: EmbeddableStart, references?: SavedObjectReference[], getTagNamesFromReferences?: (references: SavedObjectReference[]) => string[] -): DashboardAttributes | Partial { +): Promise> { const { controlGroupInput, description, @@ -71,7 +71,7 @@ export function dashboardAttributesOut( }), ...(optionsJSON && { options: transformOptionsOut(optionsJSON) }), ...((panelsJSON || sections) && { - panels: transformPanelsOut({ panelsJSON, sections, embeddable, references }), + panels: await transformPanelsOut({ panelsJSON, sections, embeddable, references }), }), ...(refreshInterval && { refreshInterval: { pause: refreshInterval.pause, value: refreshInterval.value }, @@ -85,10 +85,10 @@ export function dashboardAttributesOut( }; } -export const getResultV3ToV2 = ( +export const getResultV3ToV2 = async ( result: DashboardGetOut, embeddable: EmbeddableStart -): DashboardCrudTypesV2['GetOut'] => { +): Promise => { const { meta, item } = result; const { attributes, references, ...rest } = item; const { @@ -105,7 +105,7 @@ export const getResultV3ToV2 = ( version, } = attributes; - const { panelsJSON, references: panelReferences } = transformPanelsIn(panels, embeddable); + const { panelsJSON, references: panelReferences } = await transformPanelsIn(panels, embeddable); const v2Attributes = { ...(controlGroupInput && { @@ -134,11 +134,11 @@ export const getResultV3ToV2 = ( }; }; -export const itemToSavedObject = ({ +export const itemToSavedObject = async ({ attributes, embeddable, references = [], -}: ItemToSavedObjectParams): ItemToSavedObjectReturn => { +}: ItemToSavedObjectParams): Promise => { try { const { controlGroupInput, kibanaSavedObjectMeta, options, panels, tags, ...rest } = attributes; const { @@ -146,7 +146,7 @@ export const itemToSavedObject = ({ sections, references: panelReferences, } = panels - ? transformPanelsIn(panels, embeddable) + ? await transformPanelsIn(panels, embeddable) : { panelsJSON: '[]', sections: [], references: [] }; const soAttributes = { ...rest, @@ -186,7 +186,7 @@ export const itemToSavedObjectWithTags = async ({ replaceTagReferencesByName && tags && tags.length ? await replaceTagReferencesByName({ references, newTagNames: tags }) : references; - return itemToSavedObject({ + return await itemToSavedObject({ attributes: restAttributes, embeddable, references: soReferences, @@ -209,28 +209,28 @@ interface SavedObjectToItemOptions { getTagNamesFromReferences?: (references: SavedObjectReference[]) => string[]; } -export function savedObjectToItem( +export async function savedObjectToItem( savedObject: SavedObject, embeddable: EmbeddableStart, partial: false, opts?: SavedObjectToItemOptions -): SavedObjectToItemReturn; +): Promise>; -export function savedObjectToItem( +export async function savedObjectToItem( savedObject: PartialSavedObject, embeddable: EmbeddableStart, partial: true, opts?: SavedObjectToItemOptions -): SavedObjectToItemReturn; +): Promise>; -export function savedObjectToItem( +export async function savedObjectToItem( savedObject: | SavedObject | PartialSavedObject, embeddable: EmbeddableStart, partial: boolean /* partial arg is used to enforce the correct savedObject type */, { allowedAttributes, allowedReferences, getTagNamesFromReferences }: SavedObjectToItemOptions = {} -): SavedObjectToItemReturn { +): Promise> { const { id, type, @@ -248,10 +248,15 @@ export function savedObjectToItem( try { const attributesOut = allowedAttributes ? pick( - dashboardAttributesOut(attributes, embeddable, references, getTagNamesFromReferences), + await dashboardAttributesOut( + attributes, + embeddable, + references, + getTagNamesFromReferences + ), allowedAttributes ) - : dashboardAttributesOut(attributes, embeddable, references, getTagNamesFromReferences); + : await dashboardAttributesOut(attributes, embeddable, references, getTagNamesFromReferences); // if includeReferences is provided, only include references of those types const referencesOut = allowedReferences diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts index 850dced5f86c2..f3b19ddd95456 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/in/panels_in_transforms.ts @@ -18,15 +18,15 @@ import type { import type { DashboardAttributes, DashboardPanel, DashboardSection } from '../../types'; import { isDashboardSection, prefixReferencesFromPanel } from '../../../../../common'; -export function transformPanelsIn( +export async function transformPanelsIn( widgets: DashboardAttributes['panels'] | undefined, embeddable: EmbeddableStart, dropSections: boolean = false -): { +): Promise<{ panelsJSON: DashboardSavedObjectAttributes['panelsJSON']; sections: DashboardSavedObjectAttributes['sections']; references: SavedObjectReference[]; -} { +}> { if (!widgets) return { panelsJSON: '[]', sections: [], references: [] }; // Step 1: Add a panelIndex to each panel if necessary @@ -42,7 +42,7 @@ export function transformPanelsIn( const { panels: panelsWithSections, sections } = extractSections(extractedPanels, dropSections); // Step 4: Transform panels properties - const transformedPanels = transformPanelsProperties(panelsWithSections, embeddable); + const transformedPanels = await transformPanelsProperties(panelsWithSections, embeddable); // Step 5: Stringify panels const panelsJSON = JSON.stringify(transformedPanels); @@ -170,29 +170,33 @@ function extractSections(widgets: Array, drop return { panels, sections }; } -function panelToSavedObject( +async function panelToSavedObject( panelConfig: DashboardPanel['panelConfig'], panelType: DashboardPanel['type'], embeddable: EmbeddableStart ) { - const embeddableCmDefintions = embeddable.getEmbeddableContentManagementDefinition(panelType); + const embeddableCmDefintions = await embeddable.getEmbeddableContentManagementDefinition( + panelType + ); if (!embeddableCmDefintions) return panelConfig; const { itemToSavedObject } = embeddableCmDefintions.versions[embeddableCmDefintions.latestVersion]; return itemToSavedObject?.(panelConfig) ?? panelConfig; } -function transformPanelsProperties(panels: DashboardPanel[], embeddable: EmbeddableStart) { - return panels.map( - ({ panelConfig, gridData, id, panelIndex, panelRefName, title, type, version }) => ({ - gridData, - id, - embeddableConfig: panelToSavedObject(panelConfig, type, embeddable), - panelIndex, - panelRefName, - title, - type, - version, - }) +async function transformPanelsProperties(panels: DashboardPanel[], embeddable: EmbeddableStart) { + return Promise.all( + panels.map( + async ({ panelConfig, gridData, id, panelIndex, panelRefName, title, type, version }) => ({ + gridData, + id, + embeddableConfig: await panelToSavedObject(panelConfig, type, embeddable), + panelIndex, + panelRefName, + title, + type, + version, + }) + ) ); } diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts index 1379a5363a6e0..25f136bf73287 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/transforms/out/panels_out_transforms.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { asyncForEachWithLimit } from '@kbn/std'; import type { SavedObject, SavedObjectReference } from '@kbn/core/server'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/server'; import { SavedDashboardPanel, SavedDashboardSection } from '../../../../dashboard_saved_object'; @@ -20,23 +21,27 @@ export interface TransformPanelsOutOptions { references?: SavedObjectReference[]; } -export function transformPanelsOut({ +export async function transformPanelsOut({ panelsJSON = '[]', sections = [], embeddable, references, -}: TransformPanelsOutOptions): DashboardAttributes['panels'] { +}: TransformPanelsOutOptions): Promise { // Step 1: Parse the panelsJSON const panels = JSON.parse(panelsJSON); // Step 2: Inject sections into panels and transform properties - const panelsWithSections = injectSections(panels, sections); + const panelsWithSections = await injectSections(panels, sections, embeddable); // Step 3: Inject panel references return injectPanelReferences(panelsWithSections, embeddable, references); } -function injectSections(panels: SavedDashboardPanel[], sections: SavedDashboardSection[]) { +async function injectSections( + panels: SavedDashboardPanel[], + sections: SavedDashboardSection[], + embeddable: EmbeddableStart +): Promise> { const sectionsMap: { [uuid: string]: DashboardPanel | DashboardSection } = sections.reduce( (prev, section) => { const sectionId = section.gridData.i; @@ -44,24 +49,30 @@ function injectSections(panels: SavedDashboardPanel[], sections: SavedDashboardS }, {} ); - panels.forEach((panel: SavedDashboardPanel) => { + // TODO - 30 is an arbitrary limit. We might want to revise this based on analytics or performance. + await asyncForEachWithLimit(panels, 30, async (panel: SavedDashboardPanel) => { const { sectionId } = panel.gridData; if (sectionId) { - (sectionsMap[sectionId] as DashboardSection).panels.push(transformPanelProperties(panel)); + (sectionsMap[sectionId] as DashboardSection).panels.push( + await transformPanelProperties(panel, embeddable) + ); } else { - sectionsMap[panel.panelIndex] = transformPanelProperties(panel, embeddable); + sectionsMap[panel.panelIndex] = await transformPanelProperties(panel, embeddable); } }); return Object.values(sectionsMap); } -function transformPanelProperties(panel: SavedDashboardPanel, embeddable: EmbeddableStart) { +async function transformPanelProperties( + panel: SavedDashboardPanel, + embeddable: EmbeddableStart +): Promise { const { embeddableConfig, gridData, id, panelIndex, panelRefName, title, type, version } = panel; const { sectionId, ...restOfGridData } = gridData; // drop section ID, if it exists return { gridData: restOfGridData, id, - panelConfig: savedPanelToItem(embeddableConfig, type, embeddable), + panelConfig: await savedPanelToItem(embeddableConfig, type, embeddable), panelIndex, panelRefName, title, @@ -71,7 +82,7 @@ function transformPanelProperties(panel: SavedDashboardPanel, embeddable: Embedd } function injectPanelReferences( - widgets: DashboardAttributes['panels'], + widgets: Array, embeddable: EmbeddableStart, references: SavedObjectReference[] = [] ): Array { @@ -133,12 +144,14 @@ function injectPanelSavedObjectId(panel: DashboardPanel, references: SavedObject }; } -function savedPanelToItem( +async function savedPanelToItem( embeddableConfig: SavedDashboardPanel['embeddableConfig'], panelType: SavedDashboardPanel['type'], embeddable: EmbeddableStart ) { - const embeddableCmDefintions = embeddable.getEmbeddableContentManagementDefinition(panelType); + const embeddableCmDefintions = await embeddable.getEmbeddableContentManagementDefinition( + panelType + ); if (!embeddableCmDefintions) return embeddableConfig; const { savedObjectToItem } = embeddableCmDefintions.versions[embeddableCmDefintions.latestVersion]; diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts b/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts index 07a581768aa25..45cdbd56a3793 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v3/types.ts @@ -23,6 +23,7 @@ import { dashboardItemSchema, controlGroupInputSchema, panelGridDataSchema, + sectionGridDataSchema, panelSchema, sectionSchema, dashboardAttributesSchema, @@ -45,7 +46,9 @@ export type DashboardPanel = Omit, 'panelConfig'> & { panelConfig: TypeOf['panelConfig'] & { [key: string]: any }; gridData: GridData; }; -export type DashboardSection = TypeOf; +export type DashboardSection = Omit, 'gridData'> & { + gridData: SectionGridData; +}; export type DashboardAttributes = Omit, 'panels'> & { panels: Array; }; @@ -58,6 +61,7 @@ export type PartialDashboardItem = Omit; export type GridData = WithRequiredProperty, 'i'>; +export type SectionGridData = WithRequiredProperty, 'i'>; export type DashboardGetIn = GetIn; export type DashboardGetOut = TypeOf; diff --git a/src/platform/plugins/shared/embeddable/common/embeddable_content_management/registry.ts b/src/platform/plugins/shared/embeddable/common/embeddable_content_management/registry.ts index 69222594b86c0..6257e98abf86d 100644 --- a/src/platform/plugins/shared/embeddable/common/embeddable_content_management/registry.ts +++ b/src/platform/plugins/shared/embeddable/common/embeddable_content_management/registry.ts @@ -10,26 +10,31 @@ import { EmbeddableContentManagementDefinition } from '..'; export class EmbeddableContentManagementRegistry { - private registry: Map = new Map(); + private registry: Map Promise> = new Map(); public registerContentManagementDefinition = ( - definition: EmbeddableContentManagementDefinition + id: string, + getDefinition: () => Promise ) => { - if (this.registry.has(definition.id)) { - throw new Error( - `Content management definition for type "${definition.id}" is already registered.` - ); + if (this.registry.has(id)) { + throw new Error(`Content management definition for type "${id}" is already registered.`); } - if (!(definition.latestVersion in definition.versions)) { - throw new Error( - `Content management definition for type "${definition.id}" does not include the current version "${definition.latestVersion}".` - ); - } - this.registry.set(definition.id, definition); + // if (!(definition.latestVersion in definition.versions)) { + // throw new Error( + // `Content management definition for type "${definition.id}" does not include the current version "${definition.latestVersion}".` + // ); + // } + this.registry.set(id, getDefinition); }; - public getContentManagementDefinition = (id: string) => { - return this.registry.get(id); + public getContentManagementDefinition = async ( + id: string + ): Promise => { + const getDefinition = this.registry.get(id); + if (!getDefinition) { + return; + } + return await getDefinition(); }; } diff --git a/src/platform/plugins/shared/embeddable/common/types.ts b/src/platform/plugins/shared/embeddable/common/types.ts index bdd6a2de282fb..ca1a66e5fe404 100644 --- a/src/platform/plugins/shared/embeddable/common/types.ts +++ b/src/platform/plugins/shared/embeddable/common/types.ts @@ -65,5 +65,5 @@ export interface CommonEmbeddableStartContract { export interface CanGetEmbeddableContentManagementDefinition { getEmbeddableContentManagementDefinition: ( id: string - ) => EmbeddableContentManagementDefinition | undefined; + ) => Promise; } diff --git a/src/platform/plugins/shared/embeddable/public/types.ts b/src/platform/plugins/shared/embeddable/public/types.ts index 9f4332f139a19..d09720217f671 100644 --- a/src/platform/plugins/shared/embeddable/public/types.ts +++ b/src/platform/plugins/shared/embeddable/public/types.ts @@ -19,11 +19,11 @@ import type { registerAddFromLibraryType } from './add_from_library/registry'; import type { registerReactEmbeddableFactory } from './react_embeddable_system'; import type { EmbeddableStateTransfer } from './state_transfer'; import type { - EmbeddableContentManagementDefinition, EmbeddableStateWithType, CanGetEmbeddableContentManagementDefinition, } from '../common'; import { EnhancementRegistryDefinition } from './enhancements/types'; +import { EmbeddableContentManagementRegistry } from '../common/embeddable_content_management/registry'; export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; @@ -72,9 +72,7 @@ export interface EmbeddableSetup { */ registerReactEmbeddableFactory: typeof registerReactEmbeddableFactory; - registerEmbeddableContentManagementDefinition: ( - definition: EmbeddableContentManagementDefinition - ) => void; + registerEmbeddableContentManagementDefinition: EmbeddableContentManagementRegistry['registerContentManagementDefinition']; /** * @deprecated diff --git a/src/platform/plugins/shared/embeddable/server/plugin.ts b/src/platform/plugins/shared/embeddable/server/plugin.ts index e3d23b5d91b21..51d4e4d486622 100644 --- a/src/platform/plugins/shared/embeddable/server/plugin.ts +++ b/src/platform/plugins/shared/embeddable/server/plugin.ts @@ -31,25 +31,20 @@ import { EmbeddableStateWithType, CommonEmbeddableStartContract, EmbeddableRegistryDefinition, - EmbeddableContentManagementDefinition, + CanGetEmbeddableContentManagementDefinition, } from '../common/types'; import { getAllMigrations } from '../common/lib/get_all_migrations'; import { EmbeddableContentManagementRegistry } from '../common/embeddable_content_management/registry'; export interface EmbeddableSetup extends PersistableStateService { registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; - registerEmbeddableContentManagementDefinition: ( - definition: EmbeddableContentManagementDefinition - ) => void; + registerEmbeddableContentManagementDefinition: EmbeddableContentManagementRegistry['registerContentManagementDefinition']; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; getAllMigrations: () => MigrateFunctionsObject; } -export type EmbeddableStart = PersistableStateService & { - getEmbeddableContentManagementDefinition: ( - id: string - ) => EmbeddableContentManagementDefinition | undefined; -}; +export type EmbeddableStart = PersistableStateService & + CanGetEmbeddableContentManagementDefinition; export class EmbeddableServerPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();