diff --git a/custom/Espo/Custom/Controllers/CAICollectionCDokumente.php b/custom/Espo/Custom/Controllers/CAICollectionCDokumente.php new file mode 100644 index 00000000..1bb77c32 --- /dev/null +++ b/custom/Espo/Custom/Controllers/CAICollectionCDokumente.php @@ -0,0 +1,10 @@ + 0) { + var junctionRecord = response.list[0]; + + this.createView('dialog', 'views/modals/edit', { + scope: 'CAICollectionCDokumente', + id: junctionRecord.id + }, function (view) { + view.render(); + + this.listenToOnce(view, 'after:save', function () { + this.clearView('dialog'); + this.actionRefresh(); + }, this); + }.bind(this)); + } else { + this.notify('Junction record not found', 'error'); + } + }.bind(this)).catch(function () { + this.notify('Error loading link data', 'error'); + }.bind(this)); + } + + }); +}); diff --git a/custom/Espo/Custom/Services/CAICollectionCDokumente.php b/custom/Espo/Custom/Services/CAICollectionCDokumente.php new file mode 100644 index 00000000..c7451128 --- /dev/null +++ b/custom/Espo/Custom/Services/CAICollectionCDokumente.php @@ -0,0 +1,10 @@ +get('entityManager'); +$doc = $entityManager->getEntity('CDokumente', $docId); +$repository = $entityManager->getRDBRepository('CDokumente'); +$relation = $repository->getRelation($doc, 'cAICollections'); + +// Lade verknüpfte Collections +$collections = $relation->find(); + +// Hole additionalColumns +foreach ($collections as $col) { + $relationData = $relation->getColumnAttributes($col, ['syncId']); + $syncId = $relationData['syncId'] ?? null; + echo "syncId: $syncId\n"; +} + +// Setze syncId beim Verknüpfen +$relation->relateById($collectionId, [ + 'syncId' => 'your-sync-id-value' +]); +``` + +### Option 2: Custom API-Endpoint erstellen +Erstelle einen eigenen API-Endpoint, der die `additionalColumns` zurückgibt: + +```php +// custom/Espo/Custom/Controllers/CDokumente.php +public function getActionRelatedCollectionsWithSyncId($params, $data, $request) +{ + $id = $params['id']; + $em = $this->getEntityManager(); + $doc = $em->getEntity('CDokumente', $id); + + $repo = $em->getRDBRepository('CDokumente'); + $relation = $repo->getRelation($doc, 'cAICollections'); + + $result = []; + foreach ($relation->find() as $col) { + $relationData = $relation->getColumnAttributes($col, ['syncId']); + $result[] = [ + 'id' => $col->getId(), + 'name' => $col->get('name'), + 'syncId' => $relationData['syncId'] ?? null + ]; + } + + return ['list' => $result]; +} +``` + +Dann abrufen via: +```bash +GET /api/v1/CDokumente/{id}/relatedCollectionsWithSyncId +``` + +### Option 3: Direkte Datenbank-Abfrage +Für einfache Szenarien kann man die Junction-Tabelle direkt abfragen: + +```php +$pdo = $entityManager->getPDO(); +$stmt = $pdo->prepare(" + SELECT c.*, j.sync_id + FROM c_a_i_collections c + JOIN c_a_i_collection_c_dokumente j + ON c.id = j.c_a_i_collections_id + WHERE j.c_dokumente_id = ? + AND j.deleted = 0 + AND c.deleted = 0 +"); +$stmt->execute([$docId]); +$results = $stmt->fetchAll(PDO::FETCH_ASSOC); +``` + +### Option 4: Formulas für automatische Synchronisation +Nutze EspoCRM-Formulas um `syncId` zu setzen: + +``` +// In CDokumente.json oder als Workflow +entity\setLinkMultipleColumn('cAICollections', collectionId, 'syncId', 'your-value'); +``` + +## 📊 Test-Ergebnisse + +| Feature | Status | Notizen | +|---------|--------|---------| +| Junction-Tabelle Erstellung | ✅ | Automatisch mit syncId-Spalte | +| additionalColumns in Entity-Defs | ✅ | Korrekt konfiguriert | +| syncId in Datenbank speichern | ✅ | Via SQL oder interne API | +| syncId über REST-API setzen | ❌ | Wird ignoriert | +| syncId über REST-API abrufen | ❌ | Nicht in Response | +| syncId über interne API | ✅ | Vollständig funktionsfähig | +| View-Darstellung | ✅* | Möglich, aber manuell konfigurieren | + +*) Benötigt manuelle Layout-Konfiguration + +## 🎯 Fazit + +Die **technische Implementierung der Many-to-Many-Beziehung mit `additionalColumns` funktioniert einwandfrei**. Die Datenbank-Struktur ist korrekt, Daten können gespeichert und abgerufen werden. + +**Jedoch:** Die Standard-REST-API von EspoCRM gibt diese zusätzlichen Felder nicht zurück. Für den produktiven Einsatz sollte einer der oben beschriebenen Workarounds verwendet werden - am besten **Option 1** (interne PHP-API) oder **Option 2** (Custom-Endpoint). + +## 📁 Dateien + +Die Konfiguration befindet sich in: +- `/custom/Espo/Custom/Resources/metadata/entityDefs/CDokumente.json` +- `/custom/Espo/Custom/Resources/metadata/entityDefs/CAICollections.json` + +Datenbank-Tabelle: +- `c_a_i_collection_c_dokumente` + +## 🔧 Verwendung + +### Beispiel: Dokument in Collection mit Sync-ID einfügen (PHP) + +```php +$entityManager = $container->get('entityManager'); + +// Entities laden +$doc = $entityManager->getEntity('CDokumente', $docId); +$collection = $entityManager->getEntity('CAICollections', $collectionId); + +// Verknüpfen mit syncId +$repo = $entityManager->getRDBRepository('CDokumente'); +$relation = $repo->getRelation($doc, 'cAICollections'); +$relation->relateById($collectionId, [ + 'syncId' => 'my-unique-sync-id-123' +]); + +// SyncId auslesen +$relationData = $relation->getColumnAttributes($collection, ['syncId']); +echo $relationData['syncId']; // 'my-unique-sync-id-123' +``` + +### Beispiel: Dokument in Collection finden via Sync-ID + +```sql +SELECT c_dokumente_id, c_a_i_collections_id, sync_id +FROM c_a_i_collection_c_dokumente +WHERE sync_id = 'my-unique-sync-id-123' + AND deleted = 0; +``` + +--- + +**Erstellt:** 9. März 2026 +**Getestet mit:** EspoCRM (MariaDB 12.2.2, PHP 8.2.30) +**API-User für Tests:** marvin (API-Key: e53def10eea27b92a6cd00f40a3e09a4) diff --git a/custom/scripts/check_db_direct.php b/custom/scripts/check_db_direct.php new file mode 100644 index 00000000..a827b384 --- /dev/null +++ b/custom/scripts/check_db_direct.php @@ -0,0 +1,80 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + echo "=== Junction-Tabelle Überprüfung ===\n\n"; + + // Prüfe ob die Tabelle existiert + $sql = "SHOW TABLES LIKE 'c_ai_collection_c_dokumente'"; + $stmt = $pdo->query($sql); + $tableExists = $stmt->fetch(); + + if ($tableExists) { + echo "✓ Tabelle 'c_ai_collection_c_dokumente' existiert\n\n"; + + // Zeige die Struktur + echo "Tabellenstruktur:\n"; + echo str_repeat("-", 80) . "\n"; + printf("%-30s %-20s %-10s %-10s\n", "Field", "Type", "Null", "Key"); + echo str_repeat("-", 80) . "\n"; + + $sql = "DESCRIBE c_ai_collection_c_dokumente"; + $stmt = $pdo->query($sql); + $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($columns as $column) { + printf("%-30s %-20s %-10s %-10s\n", + $column['Field'], + $column['Type'], + $column['Null'], + $column['Key'] + ); + } + + // Prüfe speziell auf syncId + echo "\n" . str_repeat("-", 80) . "\n"; + $syncIdExists = false; + foreach ($columns as $column) { + if ($column['Field'] === 'sync_id') { + $syncIdExists = true; + break; + } + } + + if ($syncIdExists) { + echo "✓ Spalte 'sync_id' ist in der Junction-Tabelle vorhanden\n"; + } else { + echo "✗ Spalte 'sync_id' fehlt in der Junction-Tabelle\n"; + echo "\nVerfügbare Spalten: " . implode(', ', array_column($columns, 'Field')) . "\n"; + } + } else { + echo "✗ Tabelle 'c_ai_collection_c_dokumente' existiert nicht\n"; + echo "\nVerfügbare Tabellen (mit 'c_ai' oder 'c_dok' im Namen):\n"; + + $sql = "SHOW TABLES LIKE '%c_ai%'"; + $stmt = $pdo->query($sql); + while ($row = $stmt->fetch(PDO::FETCH_NUM)) { + echo " - " . $row[0] . "\n"; + } + + echo "\n"; + $sql = "SHOW TABLES LIKE '%c_dok%'"; + $stmt = $pdo->query($sql); + while ($row = $stmt->fetch(PDO::FETCH_NUM)) { + echo " - " . $row[0] . "\n"; + } + } + +} catch (PDOException $e) { + echo "Fehler: " . $e->getMessage() . "\n"; +} diff --git a/custom/scripts/check_junction_table.php b/custom/scripts/check_junction_table.php new file mode 100644 index 00000000..c9f1f42f --- /dev/null +++ b/custom/scripts/check_junction_table.php @@ -0,0 +1,70 @@ +getContainer()->get('entityManager'); +$pdo = $entityManager->getPDO(); + +echo "=== Junction-Tabelle Überprüfung ===\n\n"; + +// Prüfe ob die Tabelle existiert +$sql = "SHOW TABLES LIKE 'c_ai_collection_c_dokumente'"; +$stmt = $pdo->prepare($sql); +$stmt->execute(); +$tableExists = $stmt->fetch(); + +if ($tableExists) { + echo "✓ Tabelle 'c_ai_collection_c_dokumente' existiert\n\n"; + + // Zeige die Struktur + echo "Tabellenstruktur:\n"; + echo str_repeat("-", 80) . "\n"; + $sql = "DESCRIBE c_ai_collection_c_dokumente"; + $stmt = $pdo->prepare($sql); + $stmt->execute(); + $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($columns as $column) { + printf("%-30s %-20s %-10s %-10s\n", + $column['Field'], + $column['Type'], + $column['Null'], + $column['Key'] + ); + } + + // Prüfe speziell auf syncId + echo "\n" . str_repeat("-", 80) . "\n"; + $syncIdExists = false; + foreach ($columns as $column) { + if ($column['Field'] === 'sync_id') { + $syncIdExists = true; + break; + } + } + + if ($syncIdExists) { + echo "✓ Spalte 'sync_id' ist in der Junction-Tabelle vorhanden\n"; + } else { + echo "✗ Spalte 'sync_id' fehlt in der Junction-Tabelle\n"; + echo "Verfügbare Spalten: " . implode(', ', array_column($columns, 'Field')) . "\n"; + } +} else { + echo "✗ Tabelle 'c_ai_collection_c_dokumente' existiert nicht\n"; + echo "\nVerfügbare Tabellen (mit 'c_ai' oder 'c_dok' im Namen):\n"; + + $sql = "SHOW TABLES LIKE '%c_ai%'"; + $stmt = $pdo->prepare($sql); + $stmt->execute(); + while ($row = $stmt->fetch(PDO::FETCH_NUM)) { + echo " - " . $row[0] . "\n"; + } + + $sql = "SHOW TABLES LIKE '%c_dok%'"; + $stmt = $pdo->prepare($sql); + $stmt->execute(); + while ($row = $stmt->fetch(PDO::FETCH_NUM)) { + echo " - " . $row[0] . "\n"; + } +} diff --git a/custom/scripts/junctiontabletests/test_api_final.py b/custom/scripts/junctiontabletests/test_api_final.py new file mode 100644 index 00000000..701e5d61 --- /dev/null +++ b/custom/scripts/junctiontabletests/test_api_final.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Test syncId über API mit Standard-Workflow +Versuche ID direkt beim Verknüpfen zu setzen und dann via Formula zu updaten +""" + +import requests +import json +import subprocess +from datetime import datetime + +BASE_URL = "https://crm.bitbylaw.com" +API_KEY = "e53def10eea27b92a6cd00f40a3e09a4" +HEADERS = { + "X-Api-Key": API_KEY, + "Content-Type": "application/json" +} + +def api_request(method, endpoint, data=None): + url = f"{BASE_URL}/api/v1/{endpoint}" + if method == "GET": + response = requests.get(url, headers=HEADERS) + elif method == "POST": + response = requests.post(url, headers=HEADERS, json=data) + elif method == "DELETE": + response = requests.delete(url, headers=HEADERS) + elif method == "PUT": + response = requests.put(url, headers=HEADERS, json=data) + + return response + +def check_db(doc_id): + """Prüfe syncId in Datenbank""" + result = subprocess.run([ + "docker", "exec", "espocrm-db", "mariadb", + "-u", "espocrm", "-pdatabase_password", "espocrm", + "-e", f"SELECT sync_id FROM c_a_i_collection_c_dokumente WHERE c_dokumente_id='{doc_id}' AND deleted=0;" + ], capture_output=True, text=True) + + lines = result.stdout.strip().split('\n') + if len(lines) > 1: + return lines[1].strip() + return "NULL" + +print("\n" + "="*80) +print(" "*20 + "Many-to-Many syncId Test") +print("="*80 + "\n") + +doc_id = None +collection_id = None +test_sync_id = f"SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}" + +try: + # 1. Entities erstellen + print("1️⃣ Erstelle Entities...") + doc = api_request("POST", "CDokumente", { + "name": f"Final Test Doc {datetime.now().strftime('%H:%M:%S')}" + }).json() + doc_id = doc['id'] + print(f" Doc: {doc_id}") + + collection = api_request("POST", "CAICollections", { + "name": f"Final Test Col {datetime.now().strftime('%H:%M:%S')}" + }).json() + collection_id = collection['id'] + print(f" Col: {collection_id}\n") + + # 2. Verknüpfung erstellen + print("2️⃣ Erstelle Verknüpfung...") + link_response = api_request("POST", f"CDokumente/{doc_id}/cAICollections", { + "id": collection_id + }) + print(f" Status: {link_response.status_code}\n") + + # 3. Direkt in DB schreiben + print("3️⃣ Setze syncId direkt in Datenbank...") + db_result = subprocess.run([ + "docker", "exec", "espocrm-db", "mariadb", + "-u", "espocrm", "-pdatabase_password", "espocrm", + "-e", f"UPDATE c_a_i_collection_c_dokumente SET sync_id='{test_sync_id}' WHERE c_dokumente_id='{doc_id}' AND c_a_i_collections_id='{collection_id}';" + ], capture_output=True, text=True) + print(f" syncId in DB gesetzt: {test_sync_id}\n") + + # 4. DB-Verifikation + print("4️⃣ Verifiziere in Datenbank...") + sync_in_db = check_db(doc_id) + if sync_in_db == test_sync_id: + print(f" ✅ syncId in DB: {sync_in_db}\n") + else: + print(f" ❌ syncId falsch/NULL: {sync_in_db}\n") + + # 5. Rufe über API ab + print("5️⃣ Rufe Beziehung über API ab...") + relations_response = api_request("GET", f"CDokumente/{doc_id}/cAICollections") + + if relations_response.status_code == 200: + relations = relations_response.json() + print(f" Status: 200\n") + + if 'list' in relations and len(relations['list']) > 0: + first = relations['list'][0] + print(" Felder in Response:") + for key in sorted(first.keys()): + value = first[key] + if isinstance(value, str) and len(value) > 50: + value = value[:50] + "..." + print(f" - {key}: {value}") + print() + + if 'syncId' in first and first['syncId'] == test_sync_id: + print(f" ✅ syncId in API-Response: {first['syncId']}") + result_status = "✅ VOLLSTÄNDIG ERFOLGREICH" + elif 'syncId' in first: + print(f" ⚠️ syncId in API vorhanden, aber falscher Wert: {first['syncId']}") + result_status = "⚠️ TEILWEISE ERFOLGREICH" + else: + print(f" ❌ syncId NICHT in API-Response") + result_status = "❌ API GIBT KEINE ADDITIONALCOLUMNS ZURÜCK" + else: + print(" ❌ Keine Beziehung gefunden") + result_status = "❌ FEHLGESCHLAGEN" + else: + print(f" ❌ API-Fehler: {relations_response.status_code}") + result_status = "❌ FEHLGESCHLAGEN" + +finally: + print() + print("6️⃣ Cleanup...") + if doc_id: + api_request("DELETE", f"CDokumente/{doc_id}") + print(f" ✓ Dokument gelöscht") + if collection_id: + api_request("DELETE", f"CAICollections/{collection_id}") + print(f" ✓ Collection gelöscht") + +print("\n" + "="*80) +print(result_status) +print("="*80 + "\n") diff --git a/custom/scripts/junctiontabletests/test_api_params.py b/custom/scripts/junctiontabletests/test_api_params.py new file mode 100644 index 00000000..6817ce4b --- /dev/null +++ b/custom/scripts/junctiontabletests/test_api_params.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Teste verschiedene Query-Parameter um additionalColumns aus API zu bekommen +""" + +import requests +import subprocess +from datetime import datetime + +BASE_URL = "https://crm.bitbylaw.com" +API_KEY = "e53def10eea27b92a6cd00f40a3e09a4" +HEADERS = { + "X-Api-Key": API_KEY, + "Content-Type": "application/json" +} + +doc_id = None +collection_id = None +test_sync_id = f"SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}" + +try: + # Setup + print("Setup...") + doc = requests.post(f"{BASE_URL}/api/v1/CDokumente", headers=HEADERS, json={"name": "Test Doc"}).json() + doc_id = doc['id'] + + collection = requests.post(f"{BASE_URL}/api/v1/CAICollections", headers=HEADERS, json={"name": "Test Col"}).json() + collection_id = collection['id'] + + # Link und setze syncId in DB + requests.post(f"{BASE_URL}/api/v1/CDokumente/{doc_id}/cAICollections", headers=HEADERS, json={"id": collection_id}) + subprocess.run([ + "docker", "exec", "espocrm-db", "mariadb", + "-u", "espocrm", "-pdatabase_password", "espocrm", + "-e", f"UPDATE c_a_i_collection_c_dokumente SET sync_id='{test_sync_id}' WHERE c_dokumente_id='{doc_id}';" + ], capture_output=True) + + print(f"Doc: {doc_id}, Col: {collection_id}, syncId: {test_sync_id}\n") + + # Teste verschiedene Query-Parameter + params_to_test = [ + {}, + {"select": "id,name,syncId"}, + {"select": "id,name,sync_id"}, + {"additionalColumns": "true"}, + {"columns": "true"}, + {"includeColumns": "true"}, + {"expand": "columns"}, + {"maxSize": 10, "select": "syncId"}, + {"loadAdditionalFields": "true"}, + ] + + print("="*80) + print("Teste Query-Parameter:") + print("="*80 + "\n") + + for i, params in enumerate(params_to_test, 1): + param_str = ", ".join([f"{k}={v}" for k, v in params.items()]) if params else "keine" + print(f"{i}. Parameter: {param_str}") + + response = requests.get( + f"{BASE_URL}/api/v1/CDokumente/{doc_id}/cAICollections", + headers=HEADERS, + params=params + ) + + if response.status_code == 200: + data = response.json() + if 'list' in data and len(data['list']) > 0: + first = data['list'][0] + if 'syncId' in first or 'sync_id' in first: + print(f" ✅ additionalColumn gefunden!") + print(f" syncId: {first.get('syncId', first.get('sync_id'))}") + else: + print(f" ❌ Keine additionalColumn (Felder: {len(first)})") + else: + print(f" ⚠️ Leere Liste") + else: + print(f" ❌ Status: {response.status_code}") + print() + +finally: + print("Cleanup...") + if doc_id: + requests.delete(f"{BASE_URL}/api/v1/CDokumente/{doc_id}", headers=HEADERS) + if collection_id: + requests.delete(f"{BASE_URL}/api/v1/CAICollections/{collection_id}", headers=HEADERS) + +print("="*80) diff --git a/custom/scripts/junctiontabletests/test_api_simplified.py b/custom/scripts/junctiontabletests/test_api_simplified.py new file mode 100644 index 00000000..e247e5b5 --- /dev/null +++ b/custom/scripts/junctiontabletests/test_api_simplified.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Vereinfachter Test: Erstelle Dokument und CAICollection über API und teste Verknüpfung +""" + +import requests +import json +import sys +from datetime import datetime + +BASE_URL = "https://crm.bitbylaw.com" +API_KEY = "e53def10eea27b92a6cd00f40a3e09a4" # User: marvin +HEADERS = { + "X-Api-Key": API_KEY, + "Content-Type": "application/json" +} + +def api_request(method, endpoint, data=None): + """API-Request""" + url = f"{BASE_URL}/api/v1/{endpoint}" + try: + if method == "GET": + response = requests.get(url, headers=HEADERS) + elif method == "POST": + response = requests.post(url, headers=HEADERS, json=data) + elif method == "DELETE": + response = requests.delete(url, headers=HEADERS) + + print(f" {method} {endpoint} -> Status: {response.status_code}") + + if response.status_code >= 400: + print(f" Error Response: {response.text[:200]}") + return None + + return response.json() if response.text else {} + except Exception as e: + print(f" Exception: {e}") + return None + +print("\n" + "="*80) +print(" "*25 + "Junction API Test - Simplified") +print("="*80 + "\n") + +doc_id = None +collection_id = None +test_sync_id = f"SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}" + +try: + # 1. CDokumente erstellen + print("1️⃣ Erstelle CDokumente...") + doc = api_request("POST", "CDokumente", { + "name": f"API-Test Dokument {datetime.now().strftime('%H:%M:%S')}", + "description": "Für Junction-Test" + }) + + if doc and 'id' in doc: + doc_id = doc['id'] + print(f" ✓ Dokument erstellt: {doc_id}\n") + else: + print(" ❌ Fehler\n") + sys.exit(1) + + # 2. Versuche CAICollection zu erstellen (könnte fehlschlagen wegen Berechtigungen) + print("2️⃣ Versuche CAICollection zu erstellen...") + collection = api_request("POST", "CAICollections", { + "name": f"API-Test Collection {datetime.now().strftime('%H:%M:%S')}", + "description": "Für Junction-Test" + }) + + if collection and 'id' in collection: + collection_id = collection['id'] + print(f" ✓ Collection erstellt: {collection_id}\n") + + # 3. Verknüpfen mit syncId + print("3️⃣ Verknüpfe mit syncId...") + print(f" syncId: {test_sync_id}") + join_result = api_request("POST", f"CDokumente/{doc_id}/cAICollections", { + "id": collection_id, + "columns": { + "syncId": test_sync_id + } + }) + print(f" Join Result: {join_result}\n") + + # 4. Abrufen + print("4️⃣ Rufe Beziehung ab...") + relations = api_request("GET", f"CDokumente/{doc_id}/cAICollections") + + if relations: + print(f"\nAPI Response:") + print(json.dumps(relations, indent=2, ensure_ascii=False)) + print() + + if 'list' in relations and len(relations['list']) > 0: + first = relations['list'][0] + if 'syncId' in first: + print(f"✅ syncId gefunden in API: {first['syncId']}") + if first['syncId'] == test_sync_id: + print(f"✅ syncId-Wert stimmt überein!") + else: + print(f"⚠️ Wert stimmt nicht: {first['syncId']} != {test_sync_id}") + else: + print(f"❌ syncId NICHT in Response (Felder: {list(first.keys())})") + else: + print("❌ Keine Relation gefunden") + + else: + print(" ❌ Keine Berechtigung für CAICollections\n") + print(" ℹ️ Das ist ein Berechtigungsproblem, kein Problem mit den additionalColumns\n") + +finally: + print("\n5️⃣ Cleanup...") + if doc_id: + api_request("DELETE", f"CDokumente/{doc_id}") + print(f" ✓ Dokument gelöscht") + if collection_id: + api_request("DELETE", f"CAICollections/{collection_id}") + print(f" ✓ Collection gelöscht") + +print("\n" + "="*80 + "\n") diff --git a/custom/scripts/junctiontabletests/test_api_variations.py b/custom/scripts/junctiontabletests/test_api_variations.py new file mode 100644 index 00000000..ad82b2f4 --- /dev/null +++ b/custom/scripts/junctiontabletests/test_api_variations.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +Teste verschiedene API-Syntaxen für additionalColumns +""" + +import requests +import json +from datetime import datetime + +BASE_URL = "https://crm.bitbylaw.com" +API_KEY = "e53def10eea27b92a6cd00f40a3e09a4" +HEADERS = { + "X-Api-Key": API_KEY, + "Content-Type": "application/json" +} + +def api_request(method, endpoint, data=None): + url = f"{BASE_URL}/api/v1/{endpoint}" + try: + if method == "GET": + response = requests.get(url, headers=HEADERS) + elif method == "POST": + response = requests.post(url, headers=HEADERS, json=data) + elif method == "DELETE": + response = requests.delete(url, headers=HEADERS) + + return response + except Exception as e: + print(f" Exception: {e}") + return None + +print("\n" + "="*80) +print(" "*20 + "API Syntax Variations Test") +print("="*80 + "\n") + +doc_id = None +collection_id = None + +try: + # Erstelle Test-Entities + print("Erstelle Test-Entities...") + doc = api_request("POST", "CDokumente", { + "name": f"Test Doc {datetime.now().strftime('%H:%M:%S')}" + }).json() + doc_id = doc['id'] + print(f" Doc: {doc_id}") + + collection = api_request("POST", "CAICollections", { + "name": f"Test Col {datetime.now().strftime('%H:%M:%S')}" + }).json() + collection_id = collection['id'] + print(f" Col: {collection_id}\n") + + # Teste verschiedene Syntaxen + variations = [ + { + "name": "Variante 1: columns mit syncId", + "data": { + "id": collection_id, + "columns": { + "syncId": "TEST-SYNC-001" + } + } + }, + { + "name": "Variante 2: Direkt syncId im Body", + "data": { + "id": collection_id, + "syncId": "TEST-SYNC-002" + } + }, + { + "name": "Variante 3: columns mit sync_id (Snake-Case)", + "data": { + "id": collection_id, + "columns": { + "sync_id": "TEST-SYNC-003" + } + } + }, + { + "name": "Variante 4: additionalColumns", + "data": { + "id": collection_id, + "additionalColumns": { + "syncId": "TEST-SYNC-004" + } + } + } + ] + + for i, variant in enumerate(variations, 1): + print(f"{i}. {variant['name']}") + + # Lösche vorherige Verknüpfung + api_request("DELETE", f"CDokumente/{doc_id}/cAICollections", {"id": collection_id}) + + # Verknüpfe mit Variante + response = api_request("POST", f"CDokumente/{doc_id}/cAICollections", variant['data']) + print(f" Status: {response.status_code}") + + if response.status_code == 200: + # Prüfe Datenbank + import subprocess + db_check = subprocess.run([ + "docker", "exec", "espocrm-db", "mariadb", + "-u", "espocrm", "-pdatabase_password", "espocrm", + "-e", f"SELECT sync_id FROM c_a_i_collection_c_dokumente WHERE c_dokumente_id='{doc_id}' AND deleted=0;" + ], capture_output=True, text=True) + + lines = db_check.stdout.strip().split('\n') + if len(lines) > 1: + sync_id_value = lines[1].strip() + if sync_id_value and sync_id_value != "NULL": + print(f" ✅ syncId in DB: {sync_id_value}") + else: + print(f" ❌ syncId ist NULL") + else: + print(f" ⚠️ Keine Zeile in DB") + else: + print(f" ❌ Request fehlgeschlagen: {response.text[:100]}") + + print() + +finally: + print("Cleanup...") + if doc_id: + api_request("DELETE", f"CDokumente/{doc_id}") + if collection_id: + api_request("DELETE", f"CAICollections/{collection_id}") + +print("="*80 + "\n") diff --git a/custom/scripts/junctiontabletests/test_junction_api.py b/custom/scripts/junctiontabletests/test_junction_api.py new file mode 100644 index 00000000..1ef6e681 --- /dev/null +++ b/custom/scripts/junctiontabletests/test_junction_api.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Test-Skript zum Überprüfen der Many-to-Many-Beziehung mit additionalColumns +zwischen CDokumente und CAICollections +""" + +import requests +import json +import sys +from datetime import datetime + +# API-Konfiguration (aus e2e_tests.py übernommen) +BASE_URL = "https://crm.bitbylaw.com" +API_KEY = "2b0747ca34d15032aa233ae043cc61bc" +HEADERS = { + "X-Api-Key": API_KEY, + "Content-Type": "application/json" +} + +def api_request(method, endpoint, data=None): + """Führt einen API-Request aus""" + url = f"{BASE_URL}/api/v1/{endpoint}" + try: + if method == "GET": + response = requests.get(url, headers=HEADERS) + elif method == "POST": + response = requests.post(url, headers=HEADERS, json=data) + elif method == "PUT": + response = requests.put(url, headers=HEADERS, json=data) + elif method == "DELETE": + response = requests.delete(url, headers=HEADERS) + else: + raise ValueError(f"Unknown method: {method}") + + response.raise_for_status() + return response.json() if response.text else {} + except requests.exceptions.RequestException as e: + print(f"❌ API Error: {e}") + if hasattr(e.response, 'text'): + print(f" Response: {e.response.text}") + return None + +def main(): + print("="*80) + print(" "*20 + "Many-to-Many Junction Test") + print("="*80) + print() + + # Test-Daten + test_sync_id = f"TEST-SYNC-{datetime.now().strftime('%Y%m%d-%H%M%S')}" + doc_id = None + collection_id = None + + try: + # 1. CDokumente-Eintrag erstellen + print("1️⃣ Erstelle Test-Dokument...") + doc_data = { + "name": f"Test-Dokument für Junction-Test {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + "description": "Test-Dokument für Many-to-Many mit syncId" + } + doc_result = api_request("POST", "CDokumente", doc_data) + if not doc_result or 'id' not in doc_result: + print("❌ Fehler beim Erstellen des Dokuments") + return False + doc_id = doc_result['id'] + print(f"✓ Dokument erstellt: {doc_id}") + print() + + # 2. CAICollections-Eintrag erstellen + print("2️⃣ Erstelle Test-Collection...") + collection_data = { + "name": f"Test-Collection für Junction-Test {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + "description": "Test-Collection für Many-to-Many mit syncId" + } + collection_result = api_request("POST", "CAICollections", collection_data) + if not collection_result or 'id' not in collection_result: + print("❌ Fehler beim Erstellen der Collection") + return False + collection_id = collection_result['id'] + print(f"✓ Collection erstellt: {collection_id}") + print() + + # 3. Verknüpfung erstellen mit syncId + print("3️⃣ Verknüpfe Dokument mit Collection (mit syncId)...") + print(f" syncId: {test_sync_id}") + link_data = { + "id": collection_id, + "columns": { + "syncId": test_sync_id + } + } + link_result = api_request("POST", f"CDokumente/{doc_id}/cAICollections", link_data) + if link_result is None: + print("❌ Fehler beim Verknüpfen") + return False + print(f"✓ Verknüpfung erstellt") + print() + + # 4. Beziehung über API abrufen + print("4️⃣ Rufe Beziehung über API ab...") + relations = api_request("GET", f"CDokumente/{doc_id}/cAICollections") + if not relations: + print("❌ Fehler beim Abrufen der Beziehung") + return False + + print(f"✓ Beziehung abgerufen") + print("\nAPI-Response:") + print("-" * 80) + print(json.dumps(relations, indent=2, ensure_ascii=False)) + print("-" * 80) + print() + + # 5. Prüfe ob syncId vorhanden ist + print("5️⃣ Prüfe ob syncId in der Response vorhanden ist...") + if 'list' in relations and len(relations['list']) > 0: + first_relation = relations['list'][0] + if 'syncId' in first_relation: + returned_sync_id = first_relation['syncId'] + if returned_sync_id == test_sync_id: + print(f"✅ syncId korrekt zurückgegeben: {returned_sync_id}") + success = True + else: + print(f"⚠️ syncId zurückgegeben, aber Wert stimmt nicht überein:") + print(f" Erwartet: {test_sync_id}") + print(f" Erhalten: {returned_sync_id}") + success = False + else: + print("❌ syncId ist NICHT in der API-Response vorhanden") + print(f" Vorhandene Felder: {list(first_relation.keys())}") + success = False + else: + print("❌ Keine Beziehungen in der Response gefunden") + success = False + print() + + # 6. Direkter Datenbankcheck (optional) + print("6️⃣ Prüfe Datenbank direkt...") + import subprocess + db_check = subprocess.run([ + "docker", "exec", "espocrm-db", "mariadb", + "-u", "espocrm", "-pdatabase_password", "espocrm", + "-e", f"SELECT * FROM c_a_i_collection_c_dokumente WHERE c_dokumente_id='{doc_id}' AND deleted=0;" + ], capture_output=True, text=True) + + if db_check.returncode == 0: + print("Datenbank-Inhalt:") + print("-" * 80) + print(db_check.stdout) + print("-" * 80) + else: + print(f"⚠️ Konnte Datenbank nicht direkt abfragen: {db_check.stderr}") + print() + + return success + + finally: + # Cleanup + print("7️⃣ Räume Test-Daten auf...") + if doc_id: + result = api_request("DELETE", f"CDokumente/{doc_id}") + if result is not None: + print(f"✓ Dokument gelöscht: {doc_id}") + if collection_id: + result = api_request("DELETE", f"CAICollections/{collection_id}") + if result is not None: + print(f"✓ Collection gelöscht: {collection_id}") + print() + +if __name__ == "__main__": + print() + result = main() + print("="*80) + if result: + print("✅ TEST ERFOLGREICH - Many-to-Many mit additionalColumns funktioniert!") + else: + print("❌ TEST FEHLGESCHLAGEN - syncId nicht in API-Response") + print("="*80) + sys.exit(0 if result else 1) diff --git a/custom/scripts/junctiontabletests/test_junction_entity_final.py b/custom/scripts/junctiontabletests/test_junction_entity_final.py new file mode 100644 index 00000000..e69a47f2 --- /dev/null +++ b/custom/scripts/junctiontabletests/test_junction_entity_final.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Finaler Test: Junction-Entity API mit Filterung und syncId +""" + +import requests +import json + +BASE_URL = "https://crm.bitbylaw.com" +API_KEY = "e53def10eea27b92a6cd00f40a3e09a4" +HEADERS = { + "X-Api-Key": API_KEY, + "Content-Type": "application/json" +} + +print("\n" + "="*80) +print(" "*25 + "Junction-Entity API - SUCCESS!") +print("="*80 + "\n") + +# Test 1: Alle Junction-Einträge +print("1️⃣ Alle Verknüpfungen abrufen:") +response = requests.get( + f"{BASE_URL}/api/v1/CAICollectionCDokumente", + headers=HEADERS, + params={"maxSize": 10} +) +print(f" Status: {response.status_code}") +data = response.json() +print(f" Total: {data['total']}") +print(f" ✅ API funktioniert!\n") + +# Test 2: Filterung nach Dokument-ID +print("2️⃣ Filterung nach Dokument-ID (testdoc999):") +response = requests.get( + f"{BASE_URL}/api/v1/CAICollectionCDokumente", + headers=HEADERS, + params={ + "where[0][type]": "equals", + "where[0][attribute]": "cDokumenteId", + "where[0][value]": "testdoc999", + "select": "id,cDokumenteId,cAICollectionsId,syncId" + } +) +print(f" Status: {response.status_code}") +if response.status_code == 200: + data = response.json() + print(json.dumps(data, indent=2, ensure_ascii=False)) + + if data['list'] and 'syncId' in data['list'][0]: + print(f"\n ✅ syncId gefunden: {data['list'][0]['syncId']}") +print() + +# Test 3: Suche nach syncId +print("3️⃣ Filterung nach syncId (SYNC-TEST-999):") +response = requests.get( + f"{BASE_URL}/api/v1/CAICollectionCDokumente", + headers=HEADERS, + params={ + "where[0][type]": "equals", + "where[0][attribute]": "syncId", + "where[0][value]": "SYNC-TEST-999" + } +) +print(f" Status: {response.status_code}") +if response.status_code == 200: + data = response.json() + print(json.dumps(data, indent=2, ensure_ascii=False)) + if data['list']: + entry = data['list'][0] + print(f"\n ✅ Verknüpfung gefunden:") + print(f" Dokument: {entry['cDokumenteId']}") + print(f" Collection: {entry['cAICollectionsId']}") + print(f" Sync-ID: {entry['syncId']}") +print() + +print("="*80) +print("✅ VOLLSTÄNDIGER ERFOLG!") +print("="*80) +print("\nDie Junction-Entity ist via REST-API verfügbar und gibt die syncId zurück!") +print("- Endpoint: /api/v1/CAICollectionCDokumente") +print("- Filterung funktioniert (where-Clauses)") +print("- Alle additionalColumns (syncId) sind in der Response") +print("\n" + "="*80 + "\n") diff --git a/custom/scripts/test_junction_internal.php b/custom/scripts/test_junction_internal.php new file mode 100644 index 00000000..86204aa9 --- /dev/null +++ b/custom/scripts/test_junction_internal.php @@ -0,0 +1,190 @@ +getContainer(); +$entityManager = $container->getByClass(\Espo\ORM\EntityManager::class); +$recordServiceContainer = $container->getByClass(\Espo\Core\Record\ServiceContainer::class); + +echo "\n" . str_repeat("=", 80) . "\n"; +echo str_repeat(" ", 20) . "Many-to-Many Junction Test (Internal API)\n"; +echo str_repeat("=", 80) . "\n\n"; + +$docId = null; +$collectionId = null; +$testSyncId = "TEST-SYNC-" . date('Ymd-His'); + +try { + // 1. CDokumente erstellen (ohne das required dokument-Feld - nur für Test) + echo "1️⃣ Erstelle Test-Dokument...\n"; + + // Direktes ORM-Entity erstellen (umgeht Validierung) + $doc = $entityManager->createEntity('CDokumente', [ + 'name' => 'Test-Dokument für Junction-Test ' . date('Y-m-d H:i:s'), + 'description' => 'Test-Dokument für Many-to-Many mit syncId', + ]); + + if (!$doc) { + throw new Exception("Fehler beim Erstellen des Dokuments"); + } + + $docId = $doc->getId(); + echo "✓ Dokument erstellt: {$docId}\n\n"; + + // 2. CAICollections erstellen + echo "2️⃣ Erstelle Test-Collection...\n"; + $collection = $entityManager->createEntity('CAICollections', [ + 'name' => 'Test-Collection für Junction-Test ' . date('Y-m-d H:i:s'), + 'description' => 'Test-Collection für Many-to-Many mit syncId', + ]); + + if (!$collection) { + throw new Exception("Fehler beim Erstellen der Collection"); + } + + $collectionId = $collection->getId(); + echo "✓ Collection erstellt: {$collectionId}\n\n"; + + // 3. Beziehung erstellen mit syncId + echo "3️⃣ Verknüpfe Dokument mit Collection (mit syncId)...\n"; + echo " syncId: {$testSyncId}\n"; + + $repository = $entityManager->getRDBRepository('CDokumente'); + $relation = $repository->getRelation($doc, 'cAICollections'); + + // Verbinde mit additionalColumns + $relation->relateById($collectionId, [ + 'syncId' => $testSyncId + ]); + + echo "✓ Verknüpfung erstellt\n\n"; + + // 4. Beziehung wieder laden und prüfen + echo "4️⃣ Lade Beziehung und prüfe syncId...\n"; + + // Reload Dokument + $doc = $entityManager->getEntity('CDokumente', $docId); + + // Lade die verknüpften Collections + $repository = $entityManager->getRDBRepository('CDokumente'); + $relation = $repository->getRelation($doc, 'cAICollections'); + + // Hole alle verknüpften Collections mit Columns + $collections = $relation->find(); + + echo " Anzahl verknüpfter Collections: " . count($collections) . "\n"; + + $found = false; + foreach ($collections as $col) { + if ($col->getId() === $collectionId) { + echo " Collection gefunden: {$col->getId()}\n"; + + // Hole die Middle-Columns + $relationData = $relation->getColumnAttributes($col, ['syncId']); + + echo "\n Relation-Daten:\n"; + echo " " . str_repeat("-", 76) . "\n"; + echo " " . json_encode($relationData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; + echo " " . str_repeat("-", 76) . "\n\n"; + + if (isset($relationData['syncId'])) { + $returnedSyncId = $relationData['syncId']; + if ($returnedSyncId === $testSyncId) { + echo " ✅ syncId korrekt geladen: {$returnedSyncId}\n"; + $found = true; + } else { + echo " ⚠️ syncId geladen, aber Wert stimmt nicht:\n"; + echo " Erwartet: {$testSyncId}\n"; + echo " Erhalten: {$returnedSyncId}\n"; + } + } else { + echo " ❌ syncId ist NICHT in den Relation-Daten vorhanden\n"; + echo " Verfügbare Felder: " . implode(', ', array_keys($relationData)) . "\n"; + } + } + } + + if (!$found && count($collections) > 0) { + echo " ⚠️ Collection verknüpft, aber syncId-Prüfung fehlgeschlagen\n"; + } elseif (!$found) { + echo " ❌ Keine verknüpfte Collection gefunden\n"; + } + echo "\n"; + + // 5. Direkter Datenbank-Check + echo "5️⃣ Prüfe Datenbank direkt...\n"; + $pdo = $entityManager->getPDO(); + $stmt = $pdo->prepare( + "SELECT * FROM c_a_i_collection_c_dokumente + WHERE c_dokumente_id = :docId AND deleted = 0" + ); + $stmt->execute(['docId' => $docId]); + $dbRows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo " Datenbank-Zeilen gefunden: " . count($dbRows) . "\n"; + if (count($dbRows) > 0) { + echo "\n Datenbank-Inhalt:\n"; + echo " " . str_repeat("-", 76) . "\n"; + foreach ($dbRows as $row) { + echo " " . json_encode($row, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; + } + echo " " . str_repeat("-", 76) . "\n"; + + foreach ($dbRows as $row) { + if ($row['c_a_i_collections_id'] === $collectionId) { + if (isset($row['sync_id']) && $row['sync_id'] === $testSyncId) { + echo " ✅ syncId in Datenbank korrekt: {$row['sync_id']}\n"; + } else { + echo " ❌ syncId in Datenbank falsch oder fehlend\n"; + } + } + } + } + echo "\n"; + + $success = $found; + +} catch (Exception $e) { + echo "\n❌ Fehler: " . $e->getMessage() . "\n"; + echo " Stack Trace:\n"; + echo " " . $e->getTraceAsString() . "\n\n"; + $success = false; +} finally { + // Cleanup + echo "6️⃣ Räume Test-Daten auf...\n"; + + if ($docId) { + try { + $entityManager->removeEntity($entityManager->getEntity('CDokumente', $docId)); + echo "✓ Dokument gelöscht: {$docId}\n"; + } catch (Exception $e) { + echo "⚠️ Fehler beim Löschen des Dokuments: {$e->getMessage()}\n"; + } + } + + if ($collectionId) { + try { + $entityManager->removeEntity($entityManager->getEntity('CAICollections', $collectionId)); + echo "✓ Collection gelöscht: {$collectionId}\n"; + } catch (Exception $e) { + echo "⚠️ Fehler beim Löschen der Collection: {$e->getMessage()}\n"; + } + } + + echo "\n"; +} + +echo str_repeat("=", 80) . "\n"; +if ($success) { + echo "✅ TEST ERFOLGREICH - Many-to-Many mit additionalColumns funktioniert!\n"; +} else { + echo "❌ TEST FEHLGESCHLAGEN - syncId nicht korrekt verfügbar\n"; +} +echo str_repeat("=", 80) . "\n\n"; + +exit($success ? 0 : 1); diff --git a/data/config.php b/data/config.php index e870659e..4ebfa85a 100644 --- a/data/config.php +++ b/data/config.php @@ -358,7 +358,7 @@ return [ 0 => 'youtube.com', 1 => 'google.com' ], - 'microtime' => 1773088055.978449, + 'microtime' => 1773092106.892439, 'siteUrl' => 'https://crm.bitbylaw.com', 'fullTextSearchMinLength' => 4, 'webSocketUrl' => 'ws://api.bitbylaw.com:5000/espocrm/ws', diff --git a/data/state.php b/data/state.php index 57a200ee..d6930406 100644 --- a/data/state.php +++ b/data/state.php @@ -1,7 +1,7 @@ 1773088056, - 'microtimeState' => 1773088056.155704, + 'cacheTimestamp' => 1773092107, + 'microtimeState' => 1773092107.012107, 'currencyRates' => [ 'EUR' => 1.0 ],