packages/frontend/editor-ui/MIGRATION_RECIPE.md
Migrate files from direct workflowsStore / workflowState node access to workflowDocumentStore.
| Before | After (workflowDocumentStore) |
|---|---|
workflowsStore.allNodes | .allNodes |
workflowsStore.nodesByName | .nodesByName |
workflowsStore.getNodeById(id) | .getNodeById(id) |
workflowsStore.getNodeByName(name) | .getNodeByName(name) |
workflowsStore.getNodes() | .getNodes() |
workflowsStore.getNodesByIds(ids) | .getNodesByIds(ids) |
workflowsStore.workflow.nodes (direct — includes .find(), .findIndex(), .length, .map(), = [...] assignment) | .allNodes (for reads), .setNodes() (for assignment) |
workflowsStore.canvasNames | .canvasNames |
workflowsStore.findNodeByPartialId(id) | .findNodeByPartialId(id) |
| Before | After (workflowDocumentStore) |
|---|---|
workflowsStore.setNodes(nodes) | .setNodes(nodes) |
workflowsStore.addNode(node) | .addNode(node) |
workflowsStore.removeNode(node) | .removeNode(node) |
workflowsStore.removeNodeById(id) | .removeNodeById(id) |
workflowState.removeAllNodes(opts) | .removeAllNodes(opts) |
| Before | After (workflowDocumentStore) |
|---|---|
workflowState.setNodeParameters(...) | .setNodeParameters(...) |
workflowState.setNodeValue(...) | .setNodeValue(...) |
workflowState.setNodePositionById(...) | .setNodePositionById(...) |
workflowState.updateNodeProperties(...) | .updateNodeProperties(...) |
workflowState.updateNodeById(...) | .updateNodeById(...) |
workflowState.setNodeIssue(...) | .setNodeIssue(...) |
workflowState.resetAllNodesIssues() | .resetAllNodesIssues() |
workflowState.setLastNodeParameters(...) | .setLastNodeParameters(...) |
workflowState.resetParametersLastUpdatedAt(...) | .resetParametersLastUpdatedAt(...) |
workflowState.updateNodeAtIndex(idx, data) | .updateNodeById(id, data) — same semantics, pass node ID instead of index. updateNodeAtIndex exists in the facade as an internal helper but is not exposed publicly. |
workflowDocumentStore. Never abbreviate to docStore, wds, documentStore, or any other shorthand. The canonical name is workflowDocumentStore — in production code, tests, and local variables alike. This keeps the codebase grep-friendly and avoids confusion with other stores.workflowsStore reads AND workflowState mutations in one pass. Don't leave some calls on the old API — partial migrations make the code harder to follow and the ESLint warnings will remain.workflowsStore reads; the "workflowState migration" section covers per-node mutations. Both need to move to workflowDocumentStore.useWorkflowDocumentStore() calls. Some files may already have partial migrations (e.g. for usedCredentials or pinData). When you add the computed accessor, consolidate all inline calls into the single computed. Pinia deduplicates store instances by ID, so this is always safe.workflowState parameters after migration. If a composable accepts workflowState only for node mutations (e.g. setNodeIssue, updateNodeProperties), migrating those to workflowDocumentStore makes the parameter dead. Remove it from the signature and update all callers. Keep workflowState only if the composable still uses non-node-document properties like executingNode.workflowState.updateNodeProperties (or similar) to assert behavior. After migration, the composable calls workflowDocumentStore instead, so those spies see zero calls. Grep for the method name across all test files — not just the ones for the file you migrated.Components rendered inside the WorkflowLayout tree (canvas, NDV, node settings, etc.) use the injected store:
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
const workflowDocumentStore = injectWorkflowDocumentStore();
// Usage — inject returns ShallowRef<Store | null>, so ?.value?. chain
workflowDocumentStore?.value?.allNodes ?? []
workflowDocumentStore?.value?.getNodeById(id)
workflowDocumentStore?.value?.getNodeByName(name)
Code outside the WorkflowLayout tree (stores, standalone composables, utils) uses a computed accessor:
import { useWorkflowDocumentStore, createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
const workflowDocumentStore = computed(() =>
workflowsStore.workflowId
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
: undefined,
);
// Usage — computed wraps the store, so .value?. chain
workflowDocumentStore.value?.allNodes ?? []
workflowDocumentStore.value?.getNodeById(id)
workflowDocumentStore.value?.getNodeByName(name)
Some files export plain functions (not composables or components) that are called imperatively — e.g. push connection handlers. These have no Vue reactive setup context, so computed() won't work. Construct the store inline at call time:
import { useWorkflowDocumentStore, createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
function handleSomeEvent() {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = workflowsStore.workflowId
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
: undefined;
const node = workflowDocumentStore?.getNodeByName(name) ?? null;
}
Since workflowDocumentStore can be undefined (no workflow loaded), always provide a fallback:
| Return type | Fallback |
|---|---|
Array (.allNodes, .getNodes(), .getNodesByIds()) | ?? [] |
Single node (.getNodeById(), .getNodeByName(), .findNodeByPartialId()) | ?? null |
Map/Record (.nodesByName, .canvasNames) | ?? {} |
Void mutations (.setNodeIssue(), .updateNodeProperties(), etc.) | Optional chaining only (?.) — no fallback needed |
Mock injectWorkflowDocumentStore and return a real store:
import { injectWorkflowDocumentStore, useWorkflowDocumentStore, createWorkflowDocumentId } from '@/app/stores/workflowDocument.store';
vi.mock('@/app/stores/workflowDocument.store', async () => {
const actual = await vi.importActual('@/app/stores/workflowDocument.store');
return { ...actual, injectWorkflowDocumentStore: vi.fn() };
});
beforeEach(() => {
workflowsStore.workflow.id = 'test-workflow';
vi.mocked(injectWorkflowDocumentStore).mockReturnValue(
shallowRef(useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))),
);
});
Mock the store factory:
const { mockDocumentStore } = vi.hoisted(() => ({
mockDocumentStore: {
allNodes: [],
getNodeById: vi.fn(),
getNodeByName: vi.fn(),
// ... only the methods your test needs
},
}));
vi.mock('@/app/stores/workflowDocument.store', () => ({
useWorkflowDocumentStore: vi.fn().mockReturnValue(mockDocumentStore),
createWorkflowDocumentId: vi.fn().mockReturnValue('test-id'),
}));
mockedStore + allNodes detachmentIf tests use mockedStore(useWorkflowsStore), the allNodes computed gets detached from workflow.nodes. Fix by wiring it back:
Object.defineProperty(workflowsStore, 'allNodes', {
get: () => workflowsStore.workflow.nodes,
configurable: true,
});
workflowsStore.workflowObject (39 files) — provides indirect node access via Workflow class methods (.getNode(), .nodes, .getParentNodes(), etc.). This is intentionally NOT migrated until both nodes and connections move to workflowDocumentStore. No ESLint guard for this — it's accepted tech debt.renameNodeSelectedAndExecution, removeNodeExecutionDataById) — these are not node document stateworkflowState.executingNode and other execution-state properties — these are not node document stateUpdate this file when a migration reveals a new pattern, edge case, or pitfall that would save the next person time. Don't add noise — only document something if you had to figure it out and it wasn't already covered above.