mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 10:03:31 +01:00
feat: Phase 5 BYOB foundation + Phase 3 GitHub integration
Phase 5 - BYOB Backend (TASK-007A/B): - LocalSQL Adapter with full CloudStore API compatibility - QueryBuilder translates Parse-style queries to SQL - SchemaManager with PostgreSQL/Supabase export - LocalBackendServer with REST endpoints - BackendManager with IPC handlers for Electron - In-memory fallback when better-sqlite3 unavailable Phase 3 - GitHub Panel (GIT-004): - Issues tab with list/detail views - Pull Requests tab with list/detail views - GitHub API client with OAuth support - Repository info hook integration Phase 3 - Editor UX Bugfixes (TASK-013): - Legacy runtime detection banners - Read-only enforcement for legacy projects - Code editor modal close improvements - Property panel stuck state fix - Blockly node deletion and UI polish Phase 11 - Cloud Functions Planning: - Architecture documentation for workflow automation - Execution history storage schema design - Canvas overlay concept for debugging Docs: Updated LEARNINGS.md and COMMON-ISSUES.md
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
# CF11-001: Logic Nodes (IF/Switch/ForEach/Merge)
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------ |
|
||||
| **ID** | CF11-001 |
|
||||
| **Phase** | Phase 11 |
|
||||
| **Series** | 1 - Advanced Workflow Nodes |
|
||||
| **Priority** | 🟡 High |
|
||||
| **Difficulty** | 🟡 Medium |
|
||||
| **Estimated Time** | 12-16 hours |
|
||||
| **Prerequisites** | Phase 5 TASK-007C (Workflow Runtime) |
|
||||
| **Branch** | `feature/cf11-001-logic-nodes` |
|
||||
|
||||
## Objective
|
||||
|
||||
Create advanced workflow logic nodes that enable conditional branching, multi-way routing, array iteration, and parallel execution paths - essential for building non-trivial automation workflows.
|
||||
|
||||
## Background
|
||||
|
||||
Current Noodl nodes are designed for UI and data flow but lack the control-flow constructs needed for workflow automation. To compete with n8n/Zapier, we need:
|
||||
|
||||
- **IF Node**: Route data based on conditions
|
||||
- **Switch Node**: Multi-way branching (like a switch statement)
|
||||
- **For Each Node**: Iterate over arrays
|
||||
- **Merge Node**: Combine multiple execution paths
|
||||
|
||||
## Current State
|
||||
|
||||
- Basic condition node exists but isn't suited for workflows
|
||||
- No iteration nodes
|
||||
- No way to merge parallel execution paths
|
||||
|
||||
## Desired State
|
||||
|
||||
- IF node with visual expression builder
|
||||
- Switch node with multiple case outputs
|
||||
- For Each node for array iteration
|
||||
- Merge node to combine paths
|
||||
- All nodes work in CloudRunner workflow context
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- [ ] IF Node implementation
|
||||
- [ ] Switch Node implementation
|
||||
- [ ] For Each Node implementation
|
||||
- [ ] Merge Node implementation
|
||||
- [ ] Property editor integrations
|
||||
- [ ] CloudRunner execution support
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- UI runtime nodes (frontend-only)
|
||||
- Visual expression builder (can use existing or defer)
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### IF Node
|
||||
|
||||
Routes execution based on a boolean condition.
|
||||
|
||||
```typescript
|
||||
// packages/noodl-viewer-cloud/src/nodes/logic/IfNode.ts
|
||||
|
||||
const IfNode = {
|
||||
name: 'Workflow IF',
|
||||
displayName: 'IF',
|
||||
category: 'Workflow Logic',
|
||||
color: 'logic',
|
||||
|
||||
inputs: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
displayName: 'Condition',
|
||||
description: 'Boolean expression to evaluate'
|
||||
},
|
||||
data: {
|
||||
type: '*',
|
||||
displayName: 'Data',
|
||||
description: 'Data to pass through'
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run',
|
||||
description: 'Trigger to evaluate condition'
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
onTrue: {
|
||||
type: 'signal',
|
||||
displayName: 'True',
|
||||
description: 'Triggered when condition is true'
|
||||
},
|
||||
onFalse: {
|
||||
type: 'signal',
|
||||
displayName: 'False',
|
||||
description: 'Triggered when condition is false'
|
||||
},
|
||||
data: {
|
||||
type: '*',
|
||||
displayName: 'Data',
|
||||
description: 'Pass-through data'
|
||||
}
|
||||
},
|
||||
|
||||
run(context) {
|
||||
const condition = context.inputs.condition;
|
||||
context.outputs.data = context.inputs.data;
|
||||
|
||||
if (condition) {
|
||||
context.triggerOutput('onTrue');
|
||||
} else {
|
||||
context.triggerOutput('onFalse');
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Switch Node
|
||||
|
||||
Routes to one of multiple outputs based on value matching.
|
||||
|
||||
```typescript
|
||||
// packages/noodl-viewer-cloud/src/nodes/logic/SwitchNode.ts
|
||||
|
||||
const SwitchNode = {
|
||||
name: 'Workflow Switch',
|
||||
displayName: 'Switch',
|
||||
category: 'Workflow Logic',
|
||||
color: 'logic',
|
||||
|
||||
inputs: {
|
||||
value: {
|
||||
type: '*',
|
||||
displayName: 'Value',
|
||||
description: 'Value to switch on'
|
||||
},
|
||||
data: {
|
||||
type: '*',
|
||||
displayName: 'Data'
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run'
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
default: {
|
||||
type: 'signal',
|
||||
displayName: 'Default',
|
||||
description: 'Triggered if no case matches'
|
||||
},
|
||||
data: {
|
||||
type: '*',
|
||||
displayName: 'Data'
|
||||
}
|
||||
},
|
||||
|
||||
// Dynamic outputs for cases - configured via property panel
|
||||
dynamicports: {
|
||||
outputs: {
|
||||
cases: {
|
||||
type: 'signal'
|
||||
// Generated from cases array: case_0, case_1, etc.
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setup(context) {
|
||||
// Register cases from configuration
|
||||
const cases = context.parameters.cases || [];
|
||||
cases.forEach((caseValue, index) => {
|
||||
context.registerOutput(`case_${index}`, {
|
||||
type: 'signal',
|
||||
displayName: `Case: ${caseValue}`
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
run(context) {
|
||||
const value = context.inputs.value;
|
||||
const cases = context.parameters.cases || [];
|
||||
context.outputs.data = context.inputs.data;
|
||||
|
||||
const matchIndex = cases.indexOf(value);
|
||||
if (matchIndex >= 0) {
|
||||
context.triggerOutput(`case_${matchIndex}`);
|
||||
} else {
|
||||
context.triggerOutput('default');
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### For Each Node
|
||||
|
||||
Iterates over an array, executing the output for each item.
|
||||
|
||||
```typescript
|
||||
// packages/noodl-viewer-cloud/src/nodes/logic/ForEachNode.ts
|
||||
|
||||
const ForEachNode = {
|
||||
name: 'Workflow For Each',
|
||||
displayName: 'For Each',
|
||||
category: 'Workflow Logic',
|
||||
color: 'logic',
|
||||
|
||||
inputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
displayName: 'Items',
|
||||
description: 'Array to iterate over'
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run'
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
iteration: {
|
||||
type: 'signal',
|
||||
displayName: 'For Each Item',
|
||||
description: 'Triggered for each item'
|
||||
},
|
||||
currentItem: {
|
||||
type: '*',
|
||||
displayName: 'Current Item'
|
||||
},
|
||||
currentIndex: {
|
||||
type: 'number',
|
||||
displayName: 'Index'
|
||||
},
|
||||
completed: {
|
||||
type: 'signal',
|
||||
displayName: 'Completed',
|
||||
description: 'Triggered when iteration is complete'
|
||||
},
|
||||
allResults: {
|
||||
type: 'array',
|
||||
displayName: 'Results',
|
||||
description: 'Collected results from all iterations'
|
||||
}
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const items = context.inputs.items || [];
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
context.outputs.currentItem = items[i];
|
||||
context.outputs.currentIndex = i;
|
||||
|
||||
// Trigger and wait for downstream to complete
|
||||
const result = await context.triggerOutputAndWait('iteration');
|
||||
if (result !== undefined) {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
context.outputs.allResults = results;
|
||||
context.triggerOutput('completed');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Merge Node
|
||||
|
||||
Waits for all input paths before continuing.
|
||||
|
||||
```typescript
|
||||
// packages/noodl-viewer-cloud/src/nodes/logic/MergeNode.ts
|
||||
|
||||
const MergeNode = {
|
||||
name: 'Workflow Merge',
|
||||
displayName: 'Merge',
|
||||
category: 'Workflow Logic',
|
||||
color: 'logic',
|
||||
|
||||
inputs: {
|
||||
// Dynamic inputs based on configuration
|
||||
},
|
||||
|
||||
outputs: {
|
||||
merged: {
|
||||
type: 'signal',
|
||||
displayName: 'Merged',
|
||||
description: 'Triggered when all inputs received'
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
displayName: 'Data',
|
||||
description: 'Combined data from all inputs'
|
||||
}
|
||||
},
|
||||
|
||||
dynamicports: {
|
||||
inputs: {
|
||||
branches: {
|
||||
type: 'signal'
|
||||
// Generated: branch_0, branch_1, etc.
|
||||
},
|
||||
branchData: {
|
||||
type: '*'
|
||||
// Generated: data_0, data_1, etc.
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setup(context) {
|
||||
const branchCount = context.parameters.branchCount || 2;
|
||||
context._receivedBranches = new Set();
|
||||
context._branchData = {};
|
||||
|
||||
for (let i = 0; i < branchCount; i++) {
|
||||
context.registerInput(`branch_${i}`, {
|
||||
type: 'signal',
|
||||
displayName: `Branch ${i + 1}`
|
||||
});
|
||||
context.registerInput(`data_${i}`, {
|
||||
type: '*',
|
||||
displayName: `Data ${i + 1}`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onInputChange(context, inputName, value) {
|
||||
if (inputName.startsWith('branch_')) {
|
||||
const index = parseInt(inputName.split('_')[1]);
|
||||
context._receivedBranches.add(index);
|
||||
context._branchData[index] = context.inputs[`data_${index}`];
|
||||
|
||||
const branchCount = context.parameters.branchCount || 2;
|
||||
if (context._receivedBranches.size >= branchCount) {
|
||||
context.outputs.data = { ...context._branchData };
|
||||
context.triggerOutput('merged');
|
||||
|
||||
// Reset for next execution
|
||||
context._receivedBranches.clear();
|
||||
context._branchData = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Key Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
| ---------------------------- | ------------------------ |
|
||||
| `nodes/logic/IfNode.ts` | IF node definition |
|
||||
| `nodes/logic/SwitchNode.ts` | Switch node definition |
|
||||
| `nodes/logic/ForEachNode.ts` | For Each node definition |
|
||||
| `nodes/logic/MergeNode.ts` | Merge node definition |
|
||||
| `nodes/logic/index.ts` | Module exports |
|
||||
| `tests/logic-nodes.test.ts` | Unit tests |
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: IF Node (3h)
|
||||
|
||||
1. Create node definition
|
||||
2. Implement run logic
|
||||
3. Add to node registry
|
||||
4. Test with CloudRunner
|
||||
|
||||
### Step 2: Switch Node (4h)
|
||||
|
||||
1. Create node with dynamic ports
|
||||
2. Implement case matching logic
|
||||
3. Property editor for case configuration
|
||||
4. Test edge cases
|
||||
|
||||
### Step 3: For Each Node (4h)
|
||||
|
||||
1. Create node definition
|
||||
2. Implement async iteration
|
||||
3. Handle `triggerOutputAndWait` pattern
|
||||
4. Test with arrays and objects
|
||||
|
||||
### Step 4: Merge Node (3h)
|
||||
|
||||
1. Create node with dynamic inputs
|
||||
2. Implement branch tracking
|
||||
3. Handle reset logic
|
||||
4. Test parallel paths
|
||||
|
||||
### Step 5: Integration & Testing (2h)
|
||||
|
||||
1. Register all nodes
|
||||
2. Integration tests
|
||||
3. Manual testing in editor
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- [ ] IF Node routes correctly on true/false
|
||||
- [ ] Switch Node matches correct case
|
||||
- [ ] Switch Node triggers default when no match
|
||||
- [ ] For Each iterates all items
|
||||
- [ ] For Each handles empty arrays
|
||||
- [ ] For Each collects results
|
||||
- [ ] Merge waits for all branches
|
||||
- [ ] Merge combines data correctly
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- [ ] IF → downstream nodes execute correctly
|
||||
- [ ] Switch → multiple paths work
|
||||
- [ ] For Each → nested workflows work
|
||||
- [ ] Merge → parallel execution converges
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] IF Node works in CloudRunner
|
||||
- [ ] Switch Node with dynamic cases works
|
||||
- [ ] For Each iterates and collects results
|
||||
- [ ] Merge combines parallel paths
|
||||
- [ ] All nodes appear in node picker
|
||||
- [ ] Property panels work correctly
|
||||
- [ ] All tests pass
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
| ------------------------ | --------------------------------------- |
|
||||
| For Each performance | Limit max iterations, batch if needed |
|
||||
| Merge race conditions | Use Set for tracking, atomic operations |
|
||||
| Dynamic ports complexity | Follow existing patterns from BYOB |
|
||||
|
||||
## References
|
||||
|
||||
- [Node Patterns Guide](../../../reference/NODE-PATTERNS.md)
|
||||
- [LEARNINGS-NODE-CREATION](../../../reference/LEARNINGS-NODE-CREATION.md)
|
||||
- [n8n IF Node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/) - Reference
|
||||
Reference in New Issue
Block a user