diff --git a/src/app/utilities/rete-editor.spec.ts b/src/app/utilities/rete-editor.spec.ts index a98084b..97abb85 100644 --- a/src/app/utilities/rete-editor.spec.ts +++ b/src/app/utilities/rete-editor.spec.ts @@ -6,7 +6,7 @@ import { DEFAULT_NODE_CAPABILITIES, RESOURCE_NODE_CAPABILITIES } from '@models/f import { HFNode } from '@models/nodes'; import { vi } from 'vitest'; -import { isResourceNode, resolveNodeCapabilities, ReteRuntimeContext } from './rete-editor'; +import { isResourceNode, resolveNodeCapabilities, ReteRuntimeContext, syncResourceLinkPresentation } from './rete-editor'; function makeNode(typeName: string, nodeFamily: 'block' | 'container' | 'resource'): HFNode { return { data: { typeName, nodeFamily } } as unknown as HFNode; @@ -88,6 +88,45 @@ describe('isResourceNode', () => { }); describe('the link between a resource and a step that uses it', () => { + it('replaces an S3 operation choice with PostgreSQL choices when its resource changes', async () => { + const { NodeEditor, ClassicPreset } = await import('rete'); + const { FLOW_RESOURCE_LINK_PORT_KEY, FLOW_RESOURCE_PORT_KEY } = await import('@models/flow'); + const editor = new NodeEditor(); + const socket = new ClassicPreset.Socket('__FLOW_RESOURCE__'); + const s3 = new ClassicPreset.Node('S3') as any; + s3.data = { id: 's3', typeName: 'S3Storage', nodeFamily: 'resource' }; + s3.addOutput(FLOW_RESOURCE_PORT_KEY, new ClassicPreset.Output(socket)); + const postgres = new ClassicPreset.Node('PostgreSQL') as any; + postgres.data = { id: 'postgres', typeName: 'PostgresStorage', nodeFamily: 'resource' }; + postgres.addOutput(FLOW_RESOURCE_PORT_KEY, new ClassicPreset.Output(socket)); + const operation = new ClassicPreset.Node('Operation') as any; + operation.data = { + id: 'operation', typeName: 'StorageOperation', nodeFamily: 'block', __resourceRefField: 'storageNode', + specificConfiguration: { storageNode: 's3', resourceType: 'S3Storage', operationId: 'GET_OBJECT', settings: { key: 'a' } } + }; + operation.addInput(FLOW_RESOURCE_LINK_PORT_KEY, new ClassicPreset.Input(socket, undefined, true)); + await editor.addNode(s3); + await editor.addNode(postgres); + await editor.addNode(operation); + const runtime = makeRuntime({ + blocksService: { peekBlockType: vi.fn().mockReturnValue(null) } as any + }); + const area = { update: vi.fn().mockResolvedValue(undefined) } as any; + + const oldLink = new ClassicPreset.Connection(s3, FLOW_RESOURCE_PORT_KEY, operation, FLOW_RESOURCE_LINK_PORT_KEY); + await editor.addConnection(oldLink); + await editor.removeConnection(oldLink.id); + await syncResourceLinkPresentation(editor, area, runtime, oldLink as any); + const newLink = new ClassicPreset.Connection(postgres, FLOW_RESOURCE_PORT_KEY, operation, FLOW_RESOURCE_LINK_PORT_KEY); + await editor.addConnection(newLink); + await syncResourceLinkPresentation(editor, area, runtime, newLink as any); + + expect(operation.data.specificConfiguration).toMatchObject({ + storageNode: 'postgres', resourceType: 'PostgresStorage', operationId: null, settings: {} + }); + expect(area.update).toHaveBeenCalledWith('node', operation.id); + }); + it('is what fills the step\'s resource field, and empties it when the link is gone', async () => { const { NodeEditor, ClassicPreset } = await import('rete'); const { exportGraph } = await import('./rete-editor'); diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index e2dd2c8..2308348 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -846,7 +846,7 @@ async function replaceResourceLink(editor: NodeEditor, stepNodeId: st } /** Keep a node's visible Resource state in step with the one allowed resource connection. */ -async function syncResourceLinkPresentation( +export async function syncResourceLinkPresentation( editor: NodeEditor, area: AreaPlugin, runtime: ReteRuntimeContext, @@ -855,15 +855,23 @@ async function syncResourceLinkPresentation( if (getGraphConnectionKind(connection.sourceOutput, connection.targetInput) !== 'resource') return; const target = editor.getNode(connection.target) as HFNode | undefined; - const reference = resourceRefOf(runtime, target); - if (!target?.data || !reference) return; + const targetData = target?.data as Record | undefined; + // A block rebuilt after a structural setting update may no longer have its descriptor in the + // client cache, but addBlockToEditor has already recorded the field that owns this link. + const rememberedField = typeof targetData?.['__resourceRefField'] === 'string' + ? targetData['__resourceRefField'] + : null; + const reference = resourceRefOf(runtime, target) + ?? (rememberedField ? { field: rememberedField, resourceKind: '' } : null); + if (!target || !targetData || !reference) return; const activeLink = editor.getConnections().find((candidate) => candidate.target === target.id && candidate.targetInput === FLOW_RESOURCE_LINK_PORT_KEY); const source = activeLink ? editor.getNode(activeLink.source) as HFNode | undefined : undefined; const resourceId = typeof source?.data?.id === 'string' ? source.data.id : null; - const resourceType = typeof source?.data?.typeName === 'string' ? source.data.typeName : null; - const targetData = target.data as Record; + const resourceType = typeof source?.data?.typeName === 'string' && source.data.typeName.trim() + ? source.data.typeName + : null; const configuration = targetData['specificConfiguration'] as Record | undefined; const resourceChanged = configuration?.[reference.field] !== resourceId; if (!resourceChanged && configuration?.['resourceType'] === resourceType) return; @@ -871,8 +879,8 @@ async function syncResourceLinkPresentation( targetData['specificConfiguration'] = { ...(configuration ?? {}), [reference.field]: resourceId, - ...(targetData['typeName'] === 'StorageOperation' ? { resourceType } : {}), - ...(resourceChanged && targetData['typeName'] === 'StorageOperation' ? { operationId: null, settings: {} } : {}) + ...(reference.field === 'storageNode' ? { resourceType } : {}), + ...(resourceChanged && reference.field === 'storageNode' ? { operationId: null, settings: {} } : {}) }; await area.update('node', target.id); }