fix(storage): refresh operations after resource change

This commit is contained in:
Lucio Lelii 2026-09-29 21:40:15 +02:00
parent d4ab8f8fb1
commit 421ef23518
2 changed files with 55 additions and 8 deletions

View File

@ -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<any>();
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');

View File

@ -846,7 +846,7 @@ async function replaceResourceLink(editor: NodeEditor<HFSchemes>, 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<HFSchemes>,
area: AreaPlugin<HFSchemes, AreaExtra>,
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<string, any> | 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<string, any>;
const resourceType = typeof source?.data?.typeName === 'string' && source.data.typeName.trim()
? source.data.typeName
: null;
const configuration = targetData['specificConfiguration'] as Record<string, unknown> | 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);
}