# 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,
});
```

## Current workflow id (`workflowsStore.workflowId`)

`workflowsStore.workflowId` is a deprecated global pointer to "the open workflow".
It is being removed: consumers become document-store-first and the id is derived
from the route. There is **no `storeToRefs` destructuring** of it — every access
is the literal `workflowsStore.workflowId` member expression, guarded by ESLint
(`no-restricted-syntax`, `warn` during migration, flipped to `error` once empty).

The per-document store is keyed *by* the id, so it cannot itself answer "which
workflow is current" — that comes from the route (or the injected current-document
pointer). Pick the replacement by context:

| Context | Get the current document | Read |
|---|---|---|
| Components / composables inside `WorkflowLayout` | `injectWorkflowDocumentStore()` (`ShallowRef<Store \| null>`) | `workflowDocumentStore.value?.workflowId ?? ''` |
| Out-of-tree Pinia stores / standalone composables | `computed(() => useWorkflowDocumentStore(createWorkflowDocumentId(useWorkflowId().value)))` | `workflowDocumentStore.value.workflowId` |
| Non-reactive functions (push handlers) | receive `documentId` via `options`, resolved per event in `usePushConnection.processEvent` from the injected document store | `useWorkflowDocumentStore(documentId)` / `useWorkflowExecutionStateStore(documentId)`, and their `.workflowId` for equality guards |
| Lifecycle snapshot (e.g. collaboration) | capture the id into a local/ref at the start of the operation | the captured value |

Notes:
- `useWorkflowId()` (`@/app/composables/useWorkflowId`) resolves `inject(WorkflowIdKey)` first, then the route — use it in any setup context.
- Replace `watch(() => workflowsStore.workflowId, …)` with `watch(workflowId, …)` where `workflowId = useWorkflowId()` (or the route-derived computed).
- Tests that set `workflowsStore.workflowId = 'x'` (or `workflow.id`) move to providing `WorkflowIdKey` / the route param. Component/composable tests already `provide` `WorkflowIdKey`; push-handler tests pass `documentId` in the handler `options` argument.

## 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
- **`workflowExecutionStateStore.executingNode`** and other execution-state properties — these are not node document state (they live in `useWorkflowExecutionStateStore`)

## 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.
