n8n-editor-ui
Version:
Workflow Editor UI for n8n
182 lines (135 loc) • 9.12 kB
Markdown
# Node Migration Recipe
Migrate files from direct `workflowsStore` / `workflowState` node access to `workflowDocumentStore`.
## API Mapping
### Read accessors
| 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)` |
### Collection mutations
| 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)` |
### Per-node mutations
| 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. |
## Migration guidelines
- **Always name the variable `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.
- **Migrate all guarded APIs per file together.** When migrating a file, replace ALL `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.
- **Each ticket lists both surfaces.** The "Facade methods used" section covers `workflowsStore` reads; the "`workflowState` migration" section covers per-node mutations. Both need to move to `workflowDocumentStore`.
- **Consolidate existing inline `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.
- **Remove dead `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`.
- **Update callers when signatures change.** Removing a parameter or changing a composable's signature is a cascading change — grep for all call sites and update them. Check both production code and tests.
- **Fix downstream test spies.** Tests for *consumers* of a migrated composable may spy on `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.
## Access Patterns
### 1. Vue components inside WorkflowLayout
Components rendered inside the `WorkflowLayout` tree (canvas, NDV, node settings, etc.) use the injected store:
```ts
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)
```
### 2. Pinia stores and composables outside WorkflowLayout
Code outside the WorkflowLayout tree (stores, standalone composables, utils) uses a computed accessor:
```ts
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)
```
### 3. Standalone exported functions (no reactive context)
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:
```ts
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;
}
```
### Fallback values
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 |
## Test Patterns
### Component / composable tests (inside WorkflowLayout)
Mock `injectWorkflowDocumentStore` and return a real store:
```ts
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))),
);
});
```
### Store tests (computed pattern)
Mock the store factory:
```ts
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'),
}));
```
### Fixing `mockedStore` + `allNodes` detachment
If tests use `mockedStore(useWorkflowsStore)`, the `allNodes` computed gets detached from `workflow.nodes`. Fix by wiring it back:
```ts
Object.defineProperty(workflowsStore, 'allNodes', {
get: () => workflowsStore.workflow.nodes,
configurable: true,
});
```
## What NOT to migrate
- **`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.
- **Execution-related methods** (e.g., `renameNodeSelectedAndExecution`, `removeNodeExecutionDataById`) — these are not node document state
- **`workflowState.executingNode`** and other execution-state properties — these are not node document state
## Maintaining this recipe
Update 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.