# REST API Endpunkte - EspoCRM Custom Entities **Version:** 1.4 **Datum:** 12. März 2026 **Base URL:** `https://your-crm.com/api/v1` **Changelog:** - v1.4 (12. März 2026): Custom API Endpoints mit routes.json Pattern hinzugefügt; Standard Junction Entity API Dokumentation entfernt (nur Custom API Pattern wird verwendet) - v1.3 (11. März 2026): pending_sync Status zu globalem syncStatus hinzugefügt - v1.1 (11. März 2026): Aktivierungsstatus-Feld hinzugefügt (new, active, paused, deactivated) - v1.0 (11. März 2026): Initiale Version --- ## 🔐 Authentifizierung Alle API-Requests benötigen einen API-Key im Header: ```bash -H "X-Api-Key: your-api-key-here" -H "Content-Type: application/json" ``` **API-Key erstellen:** 1. Admin → Users → [Your User] → API Users Tab 2. "Create API User" → Key kopieren --- ## 📋 Inhaltsverzeichnis 1. [CAdvowareAkten (Advoware Akten)](#cadvowareakten-advoware-akten) 2. [CAIKnowledge (AI Knowledge Base)](#caiknowledge-ai-knowledge-base) 3. [CDokumente (Dokumente)](#cdokumente-dokumente) 4. [Custom API Endpoints für Junction Tables](#-custom-api-endpoints-für-junction-tables-best-practice) 5. [Filtering & Sorting](#filtering--sorting) 6. [Praktische Beispiele](#praktische-beispiele) --- ## CAdvowareAkten (Advoware Akten) **Entity:** Verwaltung von Advoware-Akten mit Sync-Status-Tracking ### Standard CRUD Operationen #### Liste aller Akten abrufen ```http GET /api/v1/CAdvowareAkten ``` **Query Parameter:** - `maxSize` - Max. Anzahl Ergebnisse (default: 20) - `offset` - Offset für Pagination (default: 0) - `select` - Komma-separierte Feldliste - `orderBy` - Sortierfeld - `order` - `asc` oder `desc` **Response:** ```json { "total": 150, "list": [ { "id": "64e3f8a1b2c5d", "name": "Akte 2026-001", "aktenzeichen": "123/2026", "aktennummer": 123, "aktenpfad": "/advoware/2026/001", "aktivierungsstatus": "new", "syncStatus": "unclean", "lastSync": null, "createdAt": "2026-03-11 10:00:00", "modifiedAt": "2026-03-11 15:30:00" } ] } ``` #### Einzelne Akte abrufen ```http GET /api/v1/CAdvowareAkten/{id} ``` **Response:** ```json { "id": "64e3f8a1b2c5d", "name": "Akte 2026-001", "aktenzeichen": "123/2026", "aktennummer": 123, "aktenpfad": "/advoware/2026/001", "aktivierungsstatus": "new", "syncStatus": "unclean", "lastSync": null, "vmhRumungsklageId": "64e3f8a1234ab", "vmhRumungsklageName": "Räumungsklage Muster", "assignedUserId": "user-id", "assignedUserName": "Max Mustermann" } ``` #### Neue Akte erstellen ```http POST /api/v1/CAdvowareAkten Content-Type: application/json { "name": "Akte 2026-002", "aktenzeichen": "124/2026", "aktennummer": 124, "aktenpfad": "/advoware/2026/002", "aktivierungsstatus": "new", "syncStatus": "unclean" } ``` **Response:** ```json { "id": "64e3f8a1b2c5e" } ``` #### Akte aktualisieren ```http PUT /api/v1/CAdvowareAkten/{id} Content-Type: application/json { "aktivierungsstatus": "active", "syncStatus": "synced", "lastSync": "2026-03-11T20:00:00+00:00" } ``` **Response:** ```json { "id": "64e3f8a1b2c5d" } ``` #### Akte löschen ```http DELETE /api/v1/CAdvowareAkten/{id} ``` **Response:** ```json { "success": true } ``` ### Relationship-Endpunkte #### Verknüpfte Dokumente abrufen ```http GET /api/v1/CAdvowareAkten/{id}/dokumentes ``` **Response:** ```json { "total": 5, "list": [ { "id": "dok-123", "name": "Vertrag.pdf", "description": "Mietvertrag", "createdAt": "2026-03-10 09:00:00" } ] } ``` **⚠️ WICHTIG:** Dieser Endpoint gibt **KEINE** Junction-Spalten zurück (`hnr`, `syncstatus`, `lastSync`). Nutze dafür den [Custom Junction API Endpoint](#-custom-api-endpoints-für-junction-tables-best-practice). #### Dokument mit Akte verknüpfen ```http POST /api/v1/CAdvowareAkten/{id}/dokumentes Content-Type: application/json { "id": "dokument-id-789" } ``` **Hooks werden ausgelöst:** - `DokumenteSyncStatus` - Setzt Junction `syncstatus = 'new'` - `CheckGlobalSyncStatus` - Berechnet globalen `syncStatus` - `PropagateDocumentsUp` - Verknüpft mit Räumungsklage/Mietinkasso **Response:** ```json { "success": true } ``` #### Dokument von Akte entknüpfen ```http DELETE /api/v1/CAdvowareAkten/{id}/dokumentes/{dokumentId} ``` **Hooks werden ausgelöst:** - `PropagateDocumentsUp` - Entknüpft von Räumungsklage/Mietinkasso #### Verknüpfte Räumungsklage ```http GET /api/v1/CAdvowareAkten/{id}/vmhRumungsklage ``` #### Verknüpftes Mietinkasso ```http GET /api/v1/CAdvowareAkten/{id}/mietinkasso ``` ### Filterung & Suche #### Nach aktivierungsstatus filtern ```http GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=aktivierungsstatus&where[0][value]=new ``` **Verfügbare Werte:** - `new` - Neu angelegt (Standard, blaue Badge) - `active` - Aktiv synchronisiert (grüne Badge) - `paused` - Synchronisation pausiert (gelbe Badge) - `deactivated` - Synchronisation deaktiviert (rote Badge) #### Nach syncStatus filtern ```http GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean ``` **Verfügbare Werte:** - `synced` - Alle Dokumente synchronisiert (grüne Badge) - `unclean` - Mindestens ein Dokument neu oder geändert (gelbe Badge) - `pending_sync` - Synchronisierung läuft (blaue Badge) #### Nach Aktenzeichen suchen ```http GET /api/v1/CAdvowareAkten?where[0][type]=contains&where[0][attribute]=aktenzeichen&where[0][value]=2026 ``` #### Mehrere Filter kombinieren ```http GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean&where[1][type]=greaterThan&where[1][attribute]=createdAt&where[1][value]=2026-03-01 ``` #### Nur bestimmte Felder ```http GET /api/v1/CAdvowareAkten?select=id,name,aktivierungsstatus,syncStatus,lastSync ``` #### Mit Sortierung ```http GET /api/v1/CAdvowareAkten?orderBy=createdAt&order=desc ``` --- ## CAIKnowledge (AI Knowledge Base) **Entity:** Verwaltung von AI Knowledge Base Entries mit Sync-Status ### Standard CRUD Operationen #### Liste aller Knowledge Entries ```http GET /api/v1/CAIKnowledge ``` **Response:** ```json { "total": 50, "list": [ { "id": "kb-123", "name": "Knowledge Base 2026-001", "datenbankId": "kb-external-123", "aktivierungsstatus": "active", "syncStatus": "synced", "lastSync": "2026-03-11 19:00:00", "createdAt": "2026-03-10 10:00:00" } ] } ``` #### Einzelnen Entry abrufen ```http GET /api/v1/CAIKnowledge/{id} ``` #### Neuen Entry erstellen ```http POST /api/v1/CAIKnowledge Content-Type: application/json { "name": "Knowledge Base 2026-002", "datenbankId": "kb-external-456", "aktivierungsstatus": "new", "syncStatus": "unclean" } ``` #### Entry aktualisieren ```http PUT /api/v1/CAIKnowledge/{id} Content-Type: application/json { "aktivierungsstatus": "active", "syncStatus": "synced", "lastSync": "2026-03-11T20:00:00+00:00" } ``` #### Entry löschen ```http DELETE /api/v1/CAIKnowledge/{id} ``` ### Relationship-Endpunkte #### Verknüpfte Dokumente abrufen ```http GET /api/v1/CAIKnowledge/{id}/dokumentes ``` **⚠️ WICHTIG:** Gibt **KEINE** Junction-Spalten zurück. Nutze [Custom Junction API Endpoint](#-custom-api-endpoints-für-junction-tables-best-practice). #### Dokument verknüpfen ```http POST /api/v1/CAIKnowledge/{id}/dokumentes Content-Type: application/json { "id": "dokument-id-789" } ``` **Hooks werden ausgelöst:** - `DokumenteSyncStatus` - Setzt Junction `syncstatus = 'new'` - `CheckGlobalSyncStatus` - Berechnet globalen `syncStatus` - `PropagateDocumentsUp` - Verknüpft mit Räumungsklage/Mietinkasso #### Dokument entknüpfen ```http DELETE /api/v1/CAIKnowledge/{id}/dokumentes/{dokumentId} ``` ### Filterung & Suche #### Nach aktivierungsstatus filtern ```http GET /api/v1/CAIKnowledge?where[0][type]=equals&where[0][attribute]=aktivierungsstatus&where[0][value]=active ``` **Verfügbare Werte:** - `new` - Neu angelegt (Standard, blaue Badge) - `active` - Aktiv synchronisiert (grüne Badge) - `paused` - Synchronisation pausiert (gelbe Badge) - `deactivated` - Synchronisation deaktiviert (rote Badge) #### Nach datenbankId suchen ```http GET /api/v1/CAIKnowledge?where[0][type]=equals&where[0][attribute]=datenbankId&where[0][value]=kb-123 ``` #### Alle unclean Entries ```http GET /api/v1/CAIKnowledge?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean ``` **Verfügbare syncStatus Werte:** - `synced` - Alle Dokumente synchronisiert (grüne Badge) - `unclean` - Mindestens ein Dokument neu oder geändert (gelbe Badge) - `pending_sync` - Synchronisierung läuft (blaue Badge) --- ## CDokumente (Dokumente) **Entity:** Dokumentenverwaltung ### Standard CRUD Operationen #### Liste aller Dokumente ```http GET /api/v1/CDokumente ``` #### Einzelnes Dokument abrufen ```http GET /api/v1/CDokumente/{id} ``` #### Neues Dokument erstellen ```http POST /api/v1/CDokumente Content-Type: application/json { "name": "Vertrag.pdf", "description": "Mietvertrag Mustermann" } ``` #### Dokument aktualisieren ```http PUT /api/v1/CDokumente/{id} Content-Type: application/json { "description": "Aktualisierte Beschreibung" } ``` **⚠️ Hooks werden ausgelöst:** - `UpdateJunctionSyncStatus` - Markiert alle Junction-Einträge als "unclean" #### Dokument löschen ```http DELETE /api/v1/CDokumente/{id} ``` ### Relationship-Endpunkte #### Verknüpfte AdvowareAkten ```http GET /api/v1/CDokumente/{id}/advowareAktens ``` #### Verknüpfte AIKnowledge Entries ```http GET /api/v1/CDokumente/{id}/aIKnowledges ``` --- ## Filtering & Sorting ### Where-Clause Typen #### equals (Exakte Übereinstimmung) ```http GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean ``` #### contains (Text-Suche) ```http GET /api/v1/CAdvowareAkten?where[0][type]=contains&where[0][attribute]=name&where[0][value]=2026 ``` #### in (Liste von Werten) ```http GET /api/v1/CAdvowareAkten?where[0][type]=in&where[0][attribute]=syncStatus&where[0][value][0]=new&where[0][value][1]=unclean ``` #### greaterThan / lessThan ```http GET /api/v1/CAdvowareAkten?where[0][type]=greaterThan&where[0][attribute]=createdAt&where[0][value]=2026-03-01 ``` #### isNull / isNotNull ```http GET /api/v1/CAdvowareAkten?where[0][type]=isNull&where[0][attribute]=lastSync ``` ### Sortierung ```http GET /api/v1/CAdvowareAkten?orderBy=createdAt&order=desc ``` ### Pagination ```http GET /api/v1/CAdvowareAkten?maxSize=50&offset=100 ``` ### Feld-Selektion ```http GET /api/v1/CAdvowareAkten?select=id,name,syncStatus,lastSync ``` --- ## Praktische Beispiele ### Beispiel 1: Alle unsynchronisierten Akten mit Details ```bash curl -X GET "https://crm.example.com/api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean&select=id,name,aktenzeichen,aktivierungsstatus,syncStatus&orderBy=createdAt&order=desc" \ -H "X-Api-Key: your-api-key" ``` ### Beispiel 2: Alle Dokumente einer Akte mit Junction-Spalten ```bash # Custom API Endpoint für CAIKnowledge (siehe Custom API Sektion unten) curl -X GET "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/69b1b03582bb6e2da/dokumentes" \ -H "X-Api-Key: your-api-key" # Response enthält alle Dokumente MIT Junction-Spalten in einem Call ``` ### Beispiel 3: Junction-Spalten aktualisieren ```bash # Via Custom Junction API Endpoint (empfohlen) curl -X PUT "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/kb-123/dokumentes/dok-789" \ -H "X-Api-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "aiDocumentId": "EXTERNAL-AI-123", "syncstatus": "synced", "updateLastSync": true }' ``` ### Beispiel 4: Sync-Status aktualisieren nach erfolgreicher Synchronisation ```bash # Direktes Update mit Custom API (kein Suchen nötig!) curl -X PUT "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/kb-123/dokumentes/dok-789" \ -H "X-Api-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "syncstatus": "synced", "updateLastSync": true }' # Update global status (optional, Hooks machen das automatisch) curl -X PUT "https://crm.example.com/api/v1/CAIKnowledge/kb-123" \ -H "X-Api-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "aktivierungsstatus": "active", "syncStatus": "synced", "lastSync": "2026-03-11T20:00:00+00:00" }' ``` ### Beispiel 5: Suche AI-Dokument via externe ID ```bash # Custom API gibt alle Dokumente mit Junction-Daten zurück # Clientseitig filtern nach aiDocumentId: curl -X GET "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/kb-123/dokumentes" \ -H "X-Api-Key: your-api-key" | jq '.list[] | select(.aiDocumentId=="ai-doc-external-789")' ``` ### Beispiel 6: Alle neuen/geänderten Dokumente für Sync ```bash # Custom API gibt alle Dokumente zurück - clientseitig filtern nach syncstatus curl -X GET "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/kb-123/dokumentes" \ -H "X-Api-Key: your-api-key" | jq '.list[] | select(.syncstatus=="new" or .syncstatus=="unclean")' ``` ### Beispiel 7: Alle aktiven Akten abrufen ```bash # Finde alle Akten die aktiv synchronisiert werden curl -X GET "https://crm.example.com/api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=aktivierungsstatus&where[0][value]=active&select=id,name,aktenzeichen,aktivierungsstatus,syncStatus" \ -H "X-Api-Key: your-api-key" ``` ### Beispiel 8: Akte von "new" auf "active" setzen ```bash # Aktiviere eine neu angelegte Akte curl -X PUT "https://crm.example.com/api/v1/CAdvowareAkten/akte-123" \ -H "X-Api-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "aktivierungsstatus": "active" }' ``` ### Beispiel 9: Synchronisations-Workflow mit pending_sync ```bash # Schritt 1: Hole alle Akten mit Status "unclean" die synchronisiert werden müssen AKTEN=$(curl -s -X GET "https://crm.example.com/api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean&where[1][type]=equals&where[1][attribute]=aktivierungsstatus&where[1][value]=active" \ -H "X-Api-Key: your-api-key") # Schritt 2: Setze Status auf "pending_sync" vor Synchronisation curl -X PUT "https://crm.example.com/api/v1/CAdvowareAkten/akte-123" \ -H "X-Api-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "syncStatus": "pending_sync" }' # Schritt 3: Führe Synchronisation durch... # (Hole Junction-Einträge, synchronisiere mit Advoware, etc.) # Schritt 4: Nach erfolgreicher Synchronisation curl -X PUT "https://crm.example.com/api/v1/CAdvowareAkten/akte-123" \ -H "X-Api-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "syncStatus": "synced", "lastSync": "2026-03-11T20:00:00+00:00" }' # Schritt 5: Bei Fehler während Synchronisation curl -X PUT "https://crm.example.com/api/v1/CAdvowareAkten/akte-123" \ -H "X-Api-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "syncStatus": "unclean" }' ``` --- ## 🎯 Wichtige Hinweise ### Aktivierungsstatus **Zweck:** Steuerung der Synchronisations-Aktivität für Akten und AI Knowledge Entries **Verfügbare Status:** - `new` (Standard) - Neu angelegte Einträge, noch nicht für Sync aktiviert - `active` - Aktiv synchronisiert, alle Sync-Prozesse laufen - `paused` - Synchronisation temporär pausiert, kann wieder aktiviert werden - `deactivated` - Synchronisation dauerhaft deaktiviert ### Blake3 Hash für Änderungserkennung **Zweck:** Dokumentänderungen zwischen Synchronisationen erkennen Das `blake3hash` Feld ist direkt am CDokumente Entity verfügbar und wird automatisch vom `CDokumente` Hook berechnet: ```bash # Custom API gibt blake3hash mit zurück curl -X GET "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/kb-123/dokumentes" \ -H "X-Api-Key: your-api-key" # Response enthält blake3hash für jedes Dokument: { "list": [{ "documentId": "dok-123", "documentName": "contract.pdf", "blake3hash": "b7cb1f2a3fd62f86aabff41a51921d96d10b54371c74d31c917b3c3074a204ca", "syncstatus": "synced" }] } ``` **Änderungserkennung:** 1. Bei Sync: Speichere aktuellen `blake3hash` clientseitig oder in eigenem System 2. Nächster Sync: Frage Custom API ab, vergleiche `blake3hash` 3. Wenn unterschiedlich → Dokument geändert → Re-Sync nötig 4. Update `syncstatus` via Custom API PUT Endpoint ### Globaler syncStatus **Zweck:** Übersicht über den Synchronisationszustand aller Dokumente einer Akte/eines AI Knowledge Entries **Verfügbare Status:** - `synced` (grün) - Alle Dokumente vollständig synchronisiert - `unclean` (gelb) - Mindestens ein Dokument ist neu, geändert oder gelöscht - `pending_sync` (blau) - Synchronisierung wurde gestartet aber noch nicht abgeschlossen **Status-Übergänge:** ``` unclean → pending_sync (beim Start der Synchronisation) pending_sync → synced (nach erfolgreicher Synchronisation aller Dokumente) pending_sync → unclean (bei Fehler oder wenn ein Dokument während Sync geändert wurde) synced → unclean (wenn ein Dokument geändert/hinzugefügt/gelöscht wird) ``` **Verwendung:** ```bash # 1. Vor Synchronisation: Status auf pending_sync setzen PUT /api/v1/CAdvowareAkten/{id} { "syncStatus": "pending_sync" } # 2. Nach erfolgreicher Synchronisation: Status auf synced setzen PUT /api/v1/CAdvowareAkten/{id} { "syncStatus": "synced", "lastSync": "2026-03-11T20:00:00+00:00" } # 3. Bei Fehler: Zurück auf unclean PUT /api/v1/CAdvowareAkten/{id} { "syncStatus": "unclean" } ``` **Filterung:** ```bash # Alle Akten die auf Synchronisation warten GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=unclean # Alle Akten bei denen gerade eine Synchronisation läuft GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=pending_sync # Alle erfolgreich synchronisierten Akten GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=syncStatus&where[0][value]=synced ``` **Anwendungsfälle:** ```bash # Neue Akte anlegen (automatisch status="new") POST /api/v1/CAdvowareAkten { "name": "...", "aktivierungsstatus": "new" } # Akte für Sync aktivieren PUT /api/v1/CAdvowareAkten/{id} { "aktivierungsstatus": "active" } # Sync temporär pausieren (z.B. während Wartung) PUT /api/v1/CAdvowareAkten/{id} { "aktivierungsstatus": "paused" } # Sync permanent deaktivieren PUT /api/v1/CAdvowareAkten/{id} { "aktivierungsstatus": "deactivated" } ``` **Filterung:** ```bash # Nur aktive Einträge für Sync-Job GET /api/v1/CAdvowareAkten?where[0][type]=equals&where[0][attribute]=aktivierungsstatus&where[0][value]=active # Alle pausierte oder deaktivierte GET /api/v1/CAdvowareAkten?where[0][type]=in&where[0][attribute]=aktivierungsstatus&where[0][value][0]=paused&where[0][value][1]=deactivated ``` ### Junction-Spalten via REST API **✅ RICHTIG:** Nutze Custom API Endpoints ```bash GET /api/v1/JunctionData/CAIKnowledge/{knowledgeId}/dokumentes # (Siehe Custom API Endpoints Sektion unten) ``` **❌ FALSCH:** Standard Relationship-Endpoints geben additionalColumns NICHT zurück ```bash GET /api/v1/CAdvowareAkten/{id}/dokumentes # hnr/syncStatus NICHT in Response! ``` --- ## 🚀 Custom API Endpoints für Junction Tables (Best Practice) **Problem:** Die Standard Junction-Entity-API hat folgende Einschränkungen: - ACL-Zugriffsprobleme (oft 403 Forbidden trotz korrekter Konfiguration) - Keine JOIN-Queries → Separater Call für Dokumentdetails nötig - Umständliche Filterung - Hooks können unerwünschte Seiteneffekte haben **Lösung:** Custom API Endpoint mit direkten SQL-Queries (seit EspoCRM 7.4+) ### Implementation (Beispiel: CAIKnowledge Junction API) #### 1. Routes definieren **Datei:** `custom/Espo/Custom/Resources/routes.json` ```json [ { "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes", "method": "get", "actionClassName": "Espo\\Custom\\Api\\JunctionData\\GetDokumentes" }, { "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId", "method": "put", "actionClassName": "Espo\\Custom\\Api\\JunctionData\\UpdateJunction" }, { "route": "/JunctionData/CAIKnowledge/:knowledgeId/dokumentes/:documentId", "method": "post", "actionClassName": "Espo\\Custom\\Api\\JunctionData\\LinkDokument" } ] ``` **Wichtig:** - Nach Änderungen: **Administration → Clear Cache** (erforderlich!) - Route-Parameter mit `:paramName` → verfügbar als `$request->getRouteParam('paramName')` - `actionClassName` mit vollständigem Namespace (doppelte Backslashes!) #### 2. Action Class: GET - Alle Dokumente mit Junction-Daten **Datei:** `custom/Espo/Custom/Api/JunctionData/GetDokumentes.php` ```php getRouteParam('knowledgeId'); if (!$knowledgeId) { throw new BadRequest('Knowledge ID is required'); } // Verify knowledge exists $knowledge = $this->entityManager->getEntityById('CAIKnowledge', $knowledgeId); if (!$knowledge) { throw new NotFound('Knowledge entry not found'); } $pdo = $this->entityManager->getPDO(); // Direct SQL query with JOIN - much more efficient! $sql = " SELECT j.id as junctionId, j.c_a_i_knowledge_id as cAIKnowledgeId, j.c_dokumente_id as cDokumenteId, j.ai_document_id as aiDocumentId, j.syncstatus, j.last_sync as lastSync, d.id as documentId, d.name as documentName, d.blake3hash as blake3hash, d.created_at as documentCreatedAt, d.modified_at as documentModifiedAt FROM c_a_i_knowledge_dokumente j INNER JOIN c_dokumente d ON j.c_dokumente_id = d.id WHERE j.c_a_i_knowledge_id = :knowledgeId AND j.deleted = 0 AND d.deleted = 0 ORDER BY j.id DESC "; $sth = $pdo->prepare($sql); $sth->execute(['knowledgeId' => $knowledgeId]); $results = $sth->fetchAll(\PDO::FETCH_ASSOC); return ResponseComposer::json([ 'total' => count($results), 'list' => $results ]); } } ``` **Wichtige Details:** - **Snake_case:** DB-Spaltennamen verwenden `snake_case` (z.B. `last_sync`, nicht `lastSync`) - **Dependency Injection:** Constructor Injection funktioniert automatisch - **PDO direkt:** `$this->entityManager->getPDO()` für rohe SQL-Queries - **Validierung:** Entity-Existenz prüfen für bessere Errors - **ResponseComposer::json():** Moderne Response-Methode #### 3. Action Class: PUT - Junction-Spalten aktualisieren **Datei:** `custom/Espo/Custom/Api/JunctionData/UpdateJunction.php` ```php getRouteParam('knowledgeId'); $documentId = $request->getRouteParam('documentId'); $data = $request->getParsedBody(); if (!$knowledgeId || !$documentId) { throw new BadRequest('Knowledge ID and Document ID are required'); } $pdo = $this->entityManager->getPDO(); // Build dynamic UPDATE with only provided fields $setClauses = []; $params = [ 'knowledgeId' => $knowledgeId, 'documentId' => $documentId ]; if (isset($data->aiDocumentId)) { $setClauses[] = "ai_document_id = :aiDocumentId"; $params['aiDocumentId'] = $data->aiDocumentId; } if (isset($data->syncstatus)) { $allowedStatuses = ['new', 'unclean', 'synced', 'failed', 'unsupported']; if (!in_array($data->syncstatus, $allowedStatuses)) { throw new BadRequest('Invalid syncstatus. Allowed: ' . implode(', ', $allowedStatuses)); } $setClauses[] = "syncstatus = :syncstatus"; $params['syncstatus'] = $data->syncstatus; } if (isset($data->lastSync)) { $setClauses[] = "last_sync = :lastSync"; $params['lastSync'] = $data->lastSync; } elseif (isset($data->updateLastSync) && $data->updateLastSync === true) { $setClauses[] = "last_sync = NOW()"; } if (empty($setClauses)) { throw new BadRequest('No fields to update. Provide: aiDocumentId, syncstatus, or lastSync'); } $sql = " UPDATE c_a_i_knowledge_dokumente SET " . implode(', ', $setClauses) . " WHERE c_a_i_knowledge_id = :knowledgeId AND c_dokumente_id = :documentId AND deleted = 0 "; $sth = $pdo->prepare($sql); $sth->execute($params); if ($sth->rowCount() === 0) { throw new NotFound('Junction entry not found or no changes made'); } // Return updated data return ResponseComposer::json($this->getJunctionEntry($knowledgeId, $documentId)); } private function getJunctionEntry(string $knowledgeId, string $documentId): array { $pdo = $this->entityManager->getPDO(); $sql = " SELECT id as junctionId, c_a_i_knowledge_id as cAIKnowledgeId, c_dokumente_id as cDokumenteId, ai_document_id as aiDocumentId, syncstatus, last_sync as lastSync FROM c_a_i_knowledge_dokumente WHERE c_a_i_knowledge_id = :knowledgeId AND c_dokumente_id = :documentId AND deleted = 0 "; $sth = $pdo->prepare($sql); $sth->execute([ 'knowledgeId' => $knowledgeId, 'documentId' => $documentId ]); $result = $sth->fetch(\PDO::FETCH_ASSOC); if (!$result) { throw new NotFound('Junction entry not found'); } return $result; } } ``` **Highlights:** - **Dynamisches UPDATE:** Nur Felder updaten, die im Request Body übergeben wurden - **Validierung:** Enum-Werte prüfen bevor DB-Update - **NOW()-Trick:** `updateLastSync: true` → Automatischer Timestamp - **rowCount():** Prüfen ob wirklich ein Record betroffen war #### 4. Praktische API-Verwendung **GET: Alle Dokumente mit Junction-Daten abrufen** ```bash curl -X GET "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/69b1b03582bb6e2da/dokumentes" \ -H "X-Api-Key: e53def10eea27b92a6cd00f40a3e09a4" ``` **Response:** ```json { "total": 2, "list": [ { "junctionId": 5, "cAIKnowledgeId": "69b1b03582bb6e2da", "cDokumenteId": "6974aba30cd69c723", "aiDocumentId": "NEW-DOC-FROM-API", "syncstatus": "new", "lastSync": "2026-03-12 21:38:24", "documentId": "6974aba30cd69c723", "documentName": "2. dokuments", "blake3hash": null, "documentCreatedAt": "2026-01-24 11:23:15", "documentModifiedAt": "2026-03-03 09:48:14" }, { "junctionId": 1, "cAIKnowledgeId": "69b1b03582bb6e2da", "cDokumenteId": "69a68b556a39771bf", "aiDocumentId": "UPDATED-VIA-API-123", "syncstatus": "synced", "lastSync": "2026-03-12 21:37:44", "documentId": "69a68b556a39771bf", "documentName": "hollibolli", "blake3hash": "b7cb1f2a3fd62f86aabff41a51921d96d10b54371c74d31c917b3c3074a204ca", "documentCreatedAt": "2026-03-03 07:18:45", "documentModifiedAt": "2026-03-12 20:19:01" } ] } ``` **PUT: Junction-Spalten aktualisieren** ```bash curl -X PUT "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/69b1b03582bb6e2da/dokumentes/69a68b556a39771bf" \ -H "X-Api-Key: e53def10eea27b92a6cd00f40a3e09a4" \ -H "Content-Type: application/json" \ -d '{ "aiDocumentId": "EXTERNAL-AI-DOC-123", "syncstatus": "synced", "updateLastSync": true }' ``` **Response:** ```json { "junctionId": 1, "cAIKnowledgeId": "69b1b03582bb6e2da", "cDokumenteId": "69a68b556a39771bf", "aiDocumentId": "EXTERNAL-AI-DOC-123", "syncstatus": "synced", "lastSync": "2026-03-12 21:42:15" } ``` **POST: Neues Dokument verknüpfen + Junction-Daten setzen** ```bash curl -X POST "http://localhost:8080/api/v1/JunctionData/CAIKnowledge/69b1b03582bb6e2da/dokumentes/6974aba30cd69c723" \ -H "X-Api-Key: e53def10eea27b92a6cd00f40a3e09a4" \ -H "Content-Type: application/json" \ -d '{ "aiDocumentId": "NEW-EXTERNAL-DOC", "syncstatus": "new", "updateLastSync": true }' ``` ### Vorteile Custom API Endpoints ✅ **Performance:** JOIN-Queries in einem Call statt mehrere API-Requests ✅ **Keine ACL-Probleme:** Direkter DB-Zugriff umgeht ACL-System ✅ **Volle Kontrolle:** Exakte Control über SQL und Response-Struktur ✅ **Hooks umgehen:** SQL-Updates lösen keine Entity-Hooks aus (wenn gewünscht) ✅ **Flexible Responses:** Kann mehrere Entities in einer Response joinen ✅ **Type Safety:** Moderne PHP 8+ mit Constructor Property Promotion ✅ **Wartbar:** Klare Trennung zwischen Routes, Actions und Business Logic ### Best Practices 1. **Immer validieren:** Entity-Existenz prüfen bevor DB-Operations 2. **Snake_case beachten:** DB-Spaltennamen verwenden Unterstriche 3. **Prepared Statements:** Immer PDO Prepared Statements für SQL-Injection-Schutz 4. **Error Handling:** Spezifische Exceptions (BadRequest, NotFound, Forbidden) 5. **Documentation:** PHPDoc für jede Action Class mit Endpoint-Beschreibung 6. **Testing:** API-Endpoints mit Python/Bash testen vor Produktiveinsatz 7. **Cache löschen:** Nach routes.json Änderungen IMMER Cache clearen! ### Wann Custom API verwenden? **✅ Verwende Custom API wenn:** - Junction-Spalten häufig gelesen/geschrieben werden - JOINs über mehrere Tabellen nötig sind - Performance kritisch ist (viele Dokumente) - ACL-Probleme mit Standard Junction-Entity API - Spezielle Business Logic beim Read/Write nötig **❌ Verwende Standard API wenn:** - Einfache CRUD ohne Junction-Spalten - ACL-System explizit gewünscht - Hooks sollen ausgelöst werden - Wenig Datenvolumen ### Hooks & Automatische Updates Folgende Operationen lösen automatisch Hooks aus: **Dokument verknüpfen:** ```bash POST /api/v1/CAdvowareAkten/{id}/dokumentes ``` - → `DokumenteSyncStatus`: Setzt Junction `syncstatus = 'new'` - → `CheckGlobalSyncStatus`: Berechnet globalen Status - → `PropagateDocumentsUp`: Verknüpft mit Räumungsklage/Mietinkasso **Dokument ändern:** ```bash PUT /api/v1/CDokumente/{id} ``` - → `UpdateJunctionSyncStatus`: Markiert alle Junction-Einträge als "unclean" **Vor Entity speichern:** ```bash PUT /api/v1/CAdvowareAkten/{id} ``` - → `CheckGlobalSyncStatus`: Berechnet globalen Status aus Junction-Einträgen --- **Letzte Aktualisierung:** 12. März 2026 **Version:** 1.4 Für weitere Fragen: Siehe `custom/docs/ESPOCRM_BEST_PRACTICES.md`