## Livewire → Alpine.js + HTTP Pattern (CRUD Operations)

### MANDATORY: Migrate Direct Livewire Action Calls to HTTP Controllers

This section extends the "Secure Delete Pattern" to cover **all CRUD operations** (Create, Read, Update, Delete) in Livewire components.

**⚠️ CRITICAL ARCHITECTURAL PRINCIPLE:**

Livewire components should **ONLY** be used for:
1. ✅ **UI State Management** (modals, loading states, visibility)
2. ✅ **Data Display** (rendering lists, tables)
3. ✅ **Event Listening** (refresh events from Alpine.js)

Livewire components should **NEVER** be used for:
1. ❌ **Direct Database Operations** (calling Actions/Models directly)
2. ❌ **CRUD Operations** (bypasses middleware stack)
3. ❌ **Business Logic** (belongs in Domain Actions)

### Why This Matters

**Security & Consistency:**
- Direct Action calls from Livewire bypass ALL HTTP middlewares
- No CSRF protection, authentication checks, or authorization
- Inconsistent with Laravel RESTful standards
- Difficult to test and maintain

**Example of the Problem:**

```php
// ❌ BAD: ApiNodeForm.php calling Actions directly
class ApiNodeForm extends Component
{
    public function save($requestConfig, $paginationConfig = null)
    {
        // This bypasses ALL security middlewares!
        if ($this->record) {
            UpdateApiNode::update($apiNode, $this->record->id);
        } else {
            CreateApiNode::create($apiNode);
        }
        
        return redirect()->route('ipaas.flows.create', $this->record->flow_id);
    }
}
```

**Problems with this approach:**
1. ❌ No CSRF token validation
2. ❌ No FormRequest validation (ApiNodeRequest exists but unused)
3. ❌ Bypasses `auth` and `can.manage.ipass` middlewares
4. ❌ No centralized logging in controller
5. ❌ Cannot test with standard HTTP tests
6. ❌ Inconsistent with ConnectorForm (which uses HTTP correctly)

---

### ✅ CORRECT Pattern: Alpine.js + HTTP Controller

**Architecture Flow:**
```
User Interaction (Blade/Alpine.js)
    ↓
HTTP Request (fetch/axios) with CSRF token
    ↓
Route Middleware Stack (auth, tenant, etc.)
    ↓
Controller Method (validates with FormRequest)
    ↓
Domain Action (pure business logic)
    ↓
JSON Response back to Alpine.js
    ↓
Livewire Component refresh (if needed)
```

---

### Migration Steps (Generic)

#### Step 1: Verify Controller & FormRequest Exist

**Check if controller already exists:**

```bash
# For Node components
ls -la src/App/Http/Controllers/Ipaas/Nodes/

# Should show:
# - ApiNodeController.php
# - MapNodeController.php
# - TransformNodeController.php
# - BranchNodeController.php
```

**Check if FormRequest exists:**

```bash
ls -la src/App/Http/Requests/Ipaas/Nodes/

# Should show:
# - ApiNodeRequest.php
# - MapNodeRequest.php
# - TransformNodeRequest.php
# - BranchNodeRequest.php
```

**If missing, create them following the FormRequest Security Rules.**

---

#### Step 2: Update Controller to Return JSON

Controllers must support JSON responses for AJAX requests:

```php
// src/App/Http/Controllers/Ipaas/Nodes/MapNodeController.php

class MapNodeController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('can.manage.ipass');
    }

    /**
     * Store a newly created map node.
     * Returns JSON for AJAX requests, redirect for traditional forms.
     */
    public function store(MapNodeRequest $request)
    {
        try {
            $validated = $request->validated();
            $mapNode = CreateMapNode::create($validated);
            
            Log::info('Map node created successfully', [
                'map_node_id' => $mapNode->id,
                'flow_id' => $mapNode->flow_id,
                'user_id' => auth()->id(),
            ]);

            // ✅ Return JSON for AJAX (Alpine.js)
            if ($request->expectsJson()) {
                return response()->json([
                    'success' => true,
                    'id' => $mapNode->id,
                    'message' => 'Map node created successfully',
                    'record' => $mapNode
                ], 201);
            }

            // Traditional redirect (if not AJAX)
            return redirect()
                ->route('ipaas.flows.edit', $mapNode->flow_id)
                ->with('success', 'Map node created successfully');
                
        } catch (\Exception $e) {
            Log::error('Map node creation failed', [
                'error' => $e->getMessage(),
                'user_id' => auth()->id(),
            ]);

            if ($request->expectsJson()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Failed to create map node'
                ], 500);
            }

            return back()->with('error', 'Failed to create map node');
        }
    }

    /**
     * Update the specified map node.
     * 
     * Uses paramResource pattern: ID comes from query param (?id=123)
     * converted to route param by 'convert.query.to.route' middleware.
     */
    public function update(MapNodeRequest $request)
    {
        try {
            // Get ID from request (query param converted by middleware)
            $id = $request->get('id');
            $validated = $request->validated();
            $mapNode = UpdateMapNode::update($validated, $id);
            
            Log::info('Map node updated successfully', [
                'map_node_id' => $mapNode->id,
                'user_id' => auth()->id(),
            ]);

            if ($request->expectsJson()) {
                return response()->json([
                    'success' => true,
                    'id' => $mapNode->id,
                    'message' => 'Map node updated successfully',
                    'record' => $mapNode
                ], 200);
            }

            return redirect()
                ->route('ipaas.flows.edit', $mapNode->flow_id)
                ->with('success', 'Map node updated successfully');
                
        } catch (ModelNotFoundException $e) {
            Log::warning('Map node not found for update', [
                'map_node_id' => $id,
                'user_id' => auth()->id(),
            ]);

            if ($request->expectsJson()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Map node not found'
                ], 404);
            }

            abort(404, 'Map node not found');
            
        } catch (\Exception $e) {
            Log::error('Map node update failed', [
                'map_node_id' => $id,
                'error' => $e->getMessage(),
                'user_id' => auth()->id(),
            ]);

            if ($request->expectsJson()) {
                return response()->json([
                    'success' => false,
                    'message' => 'Failed to update map node'
                ], 500);
            }

            return back()->with('error', 'Failed to update map node');
        }
    }
}
```

---

#### Step 3: Add/Verify Routes

**This project uses `paramResource` macro (not standard Laravel `resource`)**

The `paramResource` macro generates routes where IDs come as query params (`?id=123`) instead of URL segments (`/{id}`). A middleware converts query params to route params automatically.

```php
// routes/tenant.php

Route::prefix('ipaas/flows')->group(function () {
    
    // Use paramResource (project standard)
    Route::paramResource('mapnode', MapNodeController::class);
    Route::paramResource('transformnode', TransformNodeController::class);
    Route::paramResource('apinode', ApiNodeController::class);
    
    // This generates routes like:
    // POST   /ipaas/flows/mapnode          → store()
    // PUT    /ipaas/flows/mapnode/update   → update() (with ?id=123 query param)
    // DELETE /ipaas/flows/mapnode/destroy  → destroy() (with ?id=123 query param)
});
```

**Why `paramResource` instead of standard `resource()`?**
- This project uses ~105 `paramResource` routes vs only 3 standard `resource()` routes
- Maintains consistency with existing codebase (CRM, Manufacturing, Accounting, etc.)
- Uses `convert.query.to.route` middleware to handle ID conversion

---

#### Step 4: Update Blade View with Alpine.js

**Before (Direct Livewire Call):**

```blade
{{-- ❌ BAD: map-node-form.blade.php --}}
<button wire:click="save({{ json_encode($rules) }})" class="btn primary-btn">
    Save Map Node
</button>
```

**After (Alpine.js + HTTP):**

```blade
{{-- ✅ GOOD: map-node-form.blade.php --}}

<div x-data="{
        submitting: false,
        isUpdate: {{ $mapNode ? 'true' : 'false' }},
        mapNodeId: @js($mapNode->id ?? null),
        flowId: @js($record->flow_id ?? null),
        
        async saveMapNode() {
            if (this.submitting) return;
            this.submitting = true;
            
            try {
                // 1. Gather form data
                const formData = new FormData(document.getElementById('mapNodeForm'));
                const formDataObj = Object.fromEntries(formData);
                
                // 2. Get additional data from Livewire if needed
                const additionalData = await $wire.getAdditionalFormData();
                
                // 3. Merge datasets
                const payload = {
                    ...formDataObj,
                    ...additionalData
                };
                
            // 4. Determine URL and method (paramResource pattern)
            // paramResource uses /mapnode/update?id=123 for update
            // and /mapnode for store
            let url, method;
            if (this.isUpdate) {
                url = `/ipaas/flows/mapnode/update?id=${this.mapNodeId}`;
                method = 'PUT';
            } else {
                url = '/ipaas/flows/mapnode';
                method = 'POST';
            }
            
            // 5. Make fetch request to controller
            const response = await fetch(url, {
                method: method,
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                    'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
                },
                body: JSON.stringify(payload)
            });
                
                const data = await response.json();
                
                // 6. Handle response
                if (data.success) {
                    $wire.dispatch('showalert', { 
                        type: 'success', 
                        message: data.message 
                    });
                    
                    // Redirect to flow editor
                    window.location.href = `/ipaas/flows/${data.record.flow_id}/edit`;
                } else {
                    $wire.dispatch('showalert', { 
                        type: 'error', 
                        message: data.message 
                    });
                }
                
            } catch (error) {
                console.error('Map node save error:', error);
                $wire.dispatch('showalert', {
                    type: 'error',
                    message: 'Network error: ' + error.message
                });
            } finally {
                this.submitting = false;
            }
        }
    }">
    
    <form id="mapNodeForm">
        @csrf
        
        {{-- Form fields --}}
        <input type="hidden" name="flow_id" value="{{ $record->flow_id }}">
        <input type="hidden" name="rules" x-bind:value="JSON.stringify(mappingRules)">
        <input type="hidden" name="last_input" value="{{ $input }}">
        <input type="hidden" name="last_output" x-bind:value="output">
        
        {{-- Other form fields... --}}
    </form>
    
    <button type="button" 
            @click="saveMapNode()"
            :disabled="submitting"
            class="btn primary-btn">
        <span x-show="!submitting">Save Map Node</span>
        <span x-show="submitting">Saving...</span>
    </button>
</div>
```

---

#### Step 5: Simplify Livewire Component

**Before (Direct Action Calls):**

```php
// ❌ BAD: src/App/Livewire/Ipaas/Nodes/MapNodeForm.php

class MapNodeForm extends Component
{
    public function save($rules)
    {
        // Direct Action call - bypasses middleware!
        $mapNode = [
            "rules" => $rules,
            "last_input" => $this->input,
            "last_output" => $this->output,
            "flow_id" => $this->record->flow_id
        ];
        
        if ($this->mapNode) {
            $mapNode = UpdateMapNode::update($mapNode, $this->mapNode->id);
        } else {
            $mapNode = CreateMapNode::create($mapNode);
        }
        
        return redirect()->route('ipaas.flows.create', $this->record->flow_id);
    }
}
```

**After (Alpine.js handles HTTP):**

```php
// ✅ GOOD: src/App/Livewire/Ipaas/Nodes/MapNodeForm.php

class MapNodeForm extends Component
{
    public $record;
    public $input;
    public $output;
    public $fields = [];
    public $mapNode;
    public $rules;

    public function mount($record, $mapNode)
    {
        // Only initialization logic
        $this->record = $record;
        $this->mapNode = $mapNode;
        
        if ($mapNode) {
            $this->output = $this->mapNode->last_output;
            $this->rules = $this->mapNode->rules ?? json_encode([]);
        }
        
        if ($this->input != null && is_string($this->input)) {
            $this->fields = IpaasHelper::getKeys(json_decode($this->input, true));
        }
    }

    /**
     * Prepare additional data to be sent to controller via Alpine.js
     * Called from Alpine.js before making fetch request
     */
    public function getAdditionalFormData()
    {
        return [
            'last_node_type' => $this->record->node_type ?? null,
            'last_node_id' => $this->record->id ?? null,
        ];
    }

    public function preview($mappings)
    {
        // Preview is OK in Livewire (read-only operation, no persistence)
        try {
            $this->rules = json_encode($mappings);
            $this->output = IpaasHelper::applyMappedStructure($mappings, $this->input);
        } catch (Exception $e) {
            Log::error('Error in preview: ' . $e->getMessage());
            throw $e;
        }
    }

    public function render()
    {
        return view('livewire.ipaas.nodes.map-node-form');
    }
}
```

---

### Migration Checklist

When migrating a Livewire component from direct Action calls to HTTP:

**Pre-Migration:**
- [ ] Verify controller exists (`src/App/Http/Controllers/Ipaas/Nodes/`)
- [ ] Verify FormRequest exists with security validation
- [ ] Verify tests exist (`tests/Feature/Http/Controllers/Ipaas/`)
- [ ] Read existing controller implementation

**Controller Updates:**
- [ ] Add `auth` and `can.manage.ipass` middleware in `__construct()`
- [ ] Update `store()` to return JSON for `$request->expectsJson()`
- [ ] Update `update()` to return JSON for `$request->expectsJson()`
- [ ] Add comprehensive logging (info, warning, error)
- [ ] Use FormRequest for validation
- [ ] Use Domain Actions for business logic
- [ ] Handle exceptions properly (ModelNotFoundException, generic Exception)

**Route Updates:**
- [ ] Add POST route for `store()` (e.g., `/ipaas/nodes/map`)
- [ ] Add PUT route for `update()` (e.g., `/ipaas/nodes/map/{id}`)
- [ ] Add route middleware (`auth`, `can.manage.ipass`)
- [ ] Test routes with `php artisan route:list | grep mapnode`

**Blade View Updates:**
- [ ] Wrap form in `x-data` with Alpine.js component
- [ ] Add `submitting` state for loading indicator
- [ ] Implement `async saveNode()` function with fetch
- [ ] Include CSRF token in fetch headers
- [ ] Handle success/error responses
- [ ] Show loading state on submit button
- [ ] Remove `wire:click` from save button

**Livewire Component Updates:**
- [ ] Remove `save()` method (now handled by Alpine + Controller)
- [ ] Remove direct Action calls (`CreateNode::create`, `UpdateNode::update`)
- [ ] Keep `mount()` for initialization only
- [ ] Add `getAdditionalFormData()` if Livewire properties needed in request
- [ ] Keep `preview()` if it's a read-only operation
- [ ] Keep `$listeners` for refresh events if needed

**Testing:**
- [ ] Run existing controller tests: `php artisan test --filter MapNodeControllerTest`
- [ ] Test CSRF protection is working
- [ ] Test authentication is enforced
- [ ] Test authorization is enforced (`can_manage_ipass` permission)
- [ ] Test validation errors return proper JSON
- [ ] Test successful creation returns JSON with correct structure
- [ ] Test successful update returns JSON
- [ ] Test 404 when updating non-existent node
- [ ] Manual browser test: Create node
- [ ] Manual browser test: Update node
- [ ] Manual browser test: Check logs for proper context

**Documentation:**
- [ ] Update component docblock explaining the pattern
- [ ] Add comments explaining why Alpine.js is used
- [ ] Document any edge cases or special handling

---

### Benefits of This Pattern

**✅ Security:**
1. CSRF protection automatically applied
2. All route middlewares engaged (auth, authorization, tenant context)
3. FormRequest validation with security rules
4. Cannot bypass security via direct method calls
5. Centralized authorization checks

**✅ Architecture:**
1. Consistent with Laravel RESTful standards
2. Controllers handle HTTP concerns
3. Domain Actions remain pure business logic
4. Clear separation of concerns
5. Easier to understand and maintain

**✅ Testing:**
1. Standard HTTP tests (no Livewire-specific test setup)
2. Can test controllers independently
3. Can test FormRequests independently
4. Can test Domain Actions independently
5. Integration tests are more reliable

**✅ Reusability:**
1. Same controller can serve Livewire, React, Vue, or API clients
2. Domain Actions work anywhere (controllers, commands, jobs)
3. FormRequests ensure consistent validation everywhere
4. Easy to add API endpoints later

---

### When to Use Livewire vs Alpine.js + HTTP

**✅ Use Livewire for:**
- Rendering lists and tables
- Handling pagination
- Filtering and search (UI state)
- Opening/closing modals
- Showing/hiding elements
- Real-time updates (polling, events)
- Preview operations (read-only, no persistence)

**✅ Use Alpine.js + HTTP for:**
- Creating records (POST)
- Updating records (PUT/PATCH)
- Deleting records (DELETE)
- Any operation requiring validation
- Any operation requiring authorization
- Any operation requiring audit logging
- Any operation requiring CSRF protection

**❌ Never do this:**
- Call Domain Actions directly from Livewire components
- Call Models directly from Livewire components
- Bypass controllers for CRUD operations
- Skip FormRequest validation
- Skip middleware checks

---

### Historical Context

**Issue Discovered**: November 2024 - iPaaS Security & Architecture Audit
- **Root Cause**: 
  - `ApiNodeForm`, `MapNodeForm`, `TransformNodeForm` calling Actions directly
  - `ConnectorForm` already using Alpine.js + HTTP correctly (good example)
  - Inconsistent patterns within same module
- **Security Risks**:
  - Bypassed authentication checks
  - Bypassed authorization checks (`can_manage_ipass`)
  - No CSRF protection
  - No FormRequest validation
  - No centralized logging
- **Impact**: 
  - Potential for unauthorized node creation/modification
  - Difficult to audit who changed what
  - Inconsistent with rest of application
- **Resolution**: 
  - Migrated all Node forms to Alpine.js + HTTP pattern
  - Followed `ConnectorForm` as reference implementation
  - Updated all controllers to support JSON responses
- **Prevention**: 
  - This documentation
  - Code review guidelines
  - Automated tests for middleware enforcement

**Files to Migrate (November 2024):**
- [ ] `src/App/Livewire/Ipaas/Nodes/ApiNodeForm.php`
- [ ] `src/App/Livewire/Ipaas/Nodes/MapNodeForm.php`
- [ ] `src/App/Livewire/Ipaas/Nodes/TransformNodeForm.php`
- [ ] `resources/views/livewire/ipaas/nodes/api-node-form.blade.php`
- [ ] `resources/views/livewire/ipaas/nodes/map-node-form.blade.php`
- [ ] `resources/views/livewire/ipaas/nodes/transform-node-form.blade.php`

**Reference Implementation (Already Correct):**
- ✅ `src/App/Livewire/Ipaas/Connector/ConnectorForm.php`
- ✅ `resources/views/livewire/ipaas/connector/connector-form.blade.php`
- ✅ `src/App/Http/Controllers/Ipaas/ConnectorController.php`
- ✅ `src/App/Livewire/Ipaas/Nodes/BranchNodeForm.php` (uses Http::put())

**Controllers Already Prepared:**
- ✅ `src/App/Http/Controllers/Ipaas/Nodes/ApiNodeController.php`
- ✅ `src/App/Http/Controllers/Ipaas/Nodes/MapNodeController.php`
- ✅ `src/App/Http/Controllers/Ipaas/Nodes/TransformNodeController.php`

**Tests Already Written:**
- ✅ `tests/Feature/Http/Controllers/Ipaas/ApiNodeControllerTest.php`
- ✅ `tests/Feature/Http/Controllers/Ipaas/MapNodeControllerTest.php`
- ✅ `tests/Feature/Http/Controllers/Ipaas/TransformNodeControllerTest.php`

---

## Blade-to-JavaScript Data Passing: `@js()` vs `{{ }}` (MANDATORY)

### Rule: Always Use `@js()` for PHP Values Inside Alpine `x-data` JS Literals

**Context:** When passing PHP server-side values into Alpine.js `x-data` component initialization blocks, the escaping context is **JavaScript**, not HTML. Blade's `{{ }}` applies `htmlspecialchars()` (HTML escaping), which corrupts values in JS contexts.

**The Bug Pattern:**

```blade
{{-- ❌ BAD — HTML-escaping in a JS context --}}
<div x-data="{
    path: '{{ $path ?? '/' }}',
    folderId: '{{ $folderId ?? '' }}',
}">
```

If `$path` is `O'Brien/files`, Blade outputs `path: 'O&#039;Brien/files'`. JavaScript reads the literal string `O&#039;Brien/files` — not `O'Brien/files`. Paths with apostrophes, ampersands, angle brackets, or any HTML-special character are silently corrupted.

**The Correct Pattern:**

```blade
{{-- ✅ GOOD — JS-safe encoding via @js() --}}
<div x-data="{
    path: @js($path ?? '/'),
    folderId: @js($folderId ?? ''),
}">
```

`@js()` produces a JSON-encoded value (e.g., `"O'Brien\/files"`) that Alpine reads correctly as a JS string. It handles its own quoting — do **not** wrap `@js()` output in additional single quotes.

**Enforcement:**

- ❌ NEVER use `'{{ $phpVar }}'` inside an Alpine `x-data` attribute for string values
- ✅ ALWAYS use `@js($phpVar)` — it handles quoting, JSON encoding, and null safety
- ✅ Booleans can use `{{ $flag ? 'true' : 'false' }}` since they contain no special characters, but `@js($flag)` is still preferred for consistency

**Heuristic Origin:** `resources/views/livewire/media/form.blade.php` — `path` and `folderId` used `{{ }}` while companion `cloud-services/form.blade.php` correctly used `@js()`. Caught in bug review 2026-04-11.
