From a3a066034d45c6c63853b7317943715634cfb22d Mon Sep 17 00:00:00 2001 From: Eric F Date: Mon, 15 Jun 2026 20:27:41 -0400 Subject: [PATCH] fix: Hasura camelCase relations + UI patch + NodeRED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Evses.evseId type (varchar→integer) for FK compatibility - Recreate all Hasura relations with camelCase names matching UI expectations - Patch UI .next JS files: PascalCase→camelCase (Evses, Connectors, etc.) - Add fix-hasura-relations.py script for future maintenance - NodeRED accessible at nodered.digitribe.fr - All 15 stations online with working map and detail pages --- scripts/fix-hasura-relations.py | 241 ++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 scripts/fix-hasura-relations.py diff --git a/scripts/fix-hasura-relations.py b/scripts/fix-hasura-relations.py new file mode 100644 index 0000000..a69ee77 --- /dev/null +++ b/scripts/fix-hasura-relations.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +""" +Script pour recréer toutes les relations Hasura avec les noms camelCase. +Utilise l'API metadata Hasura pour créer les relations manquantes. +""" +import json +import subprocess +import sys + +ADMIN_SECRET = 'Digitribe972' +METADATA_URL = 'http://localhost:8082/v1/metadata' +GRAPHQL_URL = 'http://localhost:8082/v1/graphql' + +def graphql_query(query): + """Execute a GraphQL query and return the result.""" + r = subprocess.run( + ['curl', '-s', '-X', 'POST', GRAPHQL_URL, + '-H', f'x-hasura-admin-secret: {ADMIN_SECRET}', + '-H', 'Content-Type: application/json', + '-d', json.dumps({"query": query})], + capture_output=True, text=True, timeout=15 + ) + return json.loads(r.stdout) + +def metadata_command(payload): + """Execute a metadata command and return the result.""" + r = subprocess.run( + ['curl', '-s', '-X', 'POST', METADATA_URL, + '-H', f'x-hasura-admin-secret: {ADMIN_SECRET}', + '-H', 'Content-Type: application/json', + '-d', json.dumps(payload)], + capture_output=True, text=True, timeout=15 + ) + return json.loads(r.stdout) + +def test_relation(table, relation, fields='{ id }'): + """Test if a relation works by querying it.""" + query = f'{{ {table}(where: {{id: {{_eq: "CP001"}}}}) {{ id {relation} {fields} }} }}' + result = graphql_query(query) + if 'errors' in result: + return False, result['errors'][0]['message'][:100] + return True, "OK" + +def create_relation(table, name, rel_type, remote_table, col_map): + """Create a relationship in Hasura.""" + if rel_type == 'object': + payload = { + "type": "pg_create_object_relationship", + "args": { + "source": "default", + "table": {"schema": "public", "name": table}, + "name": name, + "using": { + "manual_configuration": { + "remote_table": {"schema": "public", "name": remote_table}, + "column_mapping": col_map + } + } + } + } + else: + payload = { + "type": "pg_create_array_relationship", + "args": { + "source": "default", + "table": {"schema": "public", "name": table}, + "name": name, + "using": { + "manual_configuration": { + "remote_table": {"schema": "public", "name": remote_table}, + "column_mapping": col_map + } + } + } + } + + result = metadata_command(payload) + if 'message' in result and result['message'] == 'success': + return True + elif 'error' in result: + if 'already exists' in result['error']: + return True # Already exists, that's OK + print(f" ERROR: {result['error'][:200]}") + return False + return False + +def main(): + print("=== Diagnostic des relations Hasura ===\n") + + # Test critical relations + critical_relations = [ + ("ChargingStations", "location", "{ id name }"), + ("ChargingStations", "evses", "{ id evseId }"), + ("ChargingStations", "connectors", "{ id status }"), + ("ChargingStations", "transactions", "{ id }"), + ("ChargingStations", "latestStatusNotifications", "{ statusNotification { connectorStatus } }"), + ("ChargingStations", "tenant", "{ id name }"), + ("Evses", "connectors", "{ id status }"), + ("Transactions", "chargingStation", "{ id }"), + ("Transactions", "evse", "{ id }"), + ("Transactions", "connector", "{ id }"), + ("Transactions", "location", "{ id name }"), + ("LatestStatusNotifications", "statusNotification", "{ connectorStatus }"), + ] + + print("Test des relations critiques:") + all_ok = True + for table, relation, fields in critical_relations: + ok, msg = test_relation(table, relation, fields) + status = "OK" if ok else "FAIL" + print(f" {status}: {table}.{relation}") + if not ok: + all_ok = False + print(f" -> {msg}") + + if all_ok: + print("\n=== Toutes les relations fonctionnent ! ===") + return + + print("\n=== Recréation des relations manquantes ===\n") + + # Define all relations to create + relations = [ + # ChargingStations + ("ChargingStations", "location", "object", "Locations", {"locationId": "id"}), + ("ChargingStations", "tenant", "object", "Tenants", {"tenantId": "id"}), + ("ChargingStations", "evses", "array", "Evses", {"id": "stationId"}), + ("ChargingStations", "connectors", "array", "Connectors", {"id": "stationId"}), + ("ChargingStations", "transactions", "array", "Transactions", {"id": "stationId"}), + ("ChargingStations", "latestStatusNotifications", "array", "LatestStatusNotifications", {"id": "stationId"}), + ("ChargingStations", "variableAttributes", "array", "VariableAttributes", {"id": "stationPkId"}), + + # Evses + ("Evses", "connectors", "array", "Connectors", {"evseId": "evseId"}), + ("Evses", "transactions", "array", "Transactions", {"evseId": "evseId"}), + ("Evses", "chargingStation", "object", "ChargingStations", {"stationId": "id"}), + + # Connectors + ("Connectors", "evse", "object", "Evses", {"evseId": "id"}), + ("Connectors", "chargingStation", "object", "ChargingStations", {"stationId": "id"}), + + # Transactions + ("Transactions", "chargingStation", "object", "ChargingStations", {"stationId": "id"}), + ("Transactions", "evse", "object", "Evses", {"evseId": "id"}), + ("Transactions", "connector", "object", "Connectors", {"connectorId": "id"}), + ("Transactions", "authorization", "object", "Authorizations", {"authorizationId": "id"}), + ("Transactions", "location", "object", "Locations", {"locationId": "id"}), + + # LatestStatusNotifications + ("LatestStatusNotifications", "statusNotification", "object", "StatusNotifications", {"statusNotificationId": "id"}), + ("LatestStatusNotifications", "chargingStation", "object", "ChargingStations", {"stationId": "id"}), + + # Locations + ("Locations", "chargingStations", "array", "ChargingStations", {"locationId": "id"}), + + # StatusNotifications + ("StatusNotifications", "chargingStation", "object", "ChargingStations", {"stationId": "id"}), + ("StatusNotifications", "evse", "object", "Evses", {"evseId": "id"}), + ("StatusNotifications", "connector", "object", "Connectors", {"connectorId": "id"}), + + # Authorizations + ("Authorizations", "transactions", "array", "Transactions", {"authorizationId": "id"}), + ("Authorizations", "chargingStation", "object", "ChargingStations", {"stationId": "id"}), + + # Tenants + ("Tenants", "chargingStations", "array", "ChargingStations", {"tenantId": "id"}), + ("Tenants", "evses", "array", "Evses", {"tenantId": "id"}), + ("Tenants", "connectors", "array", "Connectors", {"tenantId": "id"}), + ("Tenants", "transactions", "array", "Transactions", {"tenantId": "id"}), + ("Tenants", "locations", "array", "Locations", {"tenantId": "id"}), + ("Tenants", "authorizations", "array", "Authorizations", {"tenantId": "id"}), + ("Tenants", "tenantPartners", "array", "TenantPartners", {"tenantId": "id"}), + ("Tenants", "statusNotifications", "array", "StatusNotifications", {"tenantId": "id"}), + ("Tenants", "latestStatusNotifications", "array", "LatestStatusNotifications", {"tenantId": "id"}), + ("Tenants", "variableAttributes", "array", "VariableAttributes", {"tenantId": "id"}), + ("Tenants", "chargingProfiles", "array", "ChargingProfiles", {"tenantId": "id"}), + ("Tenants", "chargingSchedules", "array", "ChargingSchedules", {"tenantId": "id"}), + ("Tenants", "salesTariffs", "array", "SalesTariffs", {"tenantId": "id"}), + ("Tenants", "messageInfos", "array", "MessageInfos", {"tenantId": "id"}), + ("Tenants", "securityEvents", "array", "SecurityEvents", {"tenantId": "id"}), + ("Tenants", "certificates", "array", "Certificates", {"tenantId": "id"}), + ("Tenants", "boots", "array", "Boots", {"tenantId": "id"}), + ("Tenants", "asyncJobStatuses", "array", "AsyncJobStatuses", {"tenantId": "id"}), + ("Tenants", "eventData", "array", "EventData", {"tenantId": "id"}), + ("Tenants", "subscriptions", "array", "Subscriptions", {"tenantId": "id"}), + ("Tenants", "reservations", "array", "Reservations", {"tenantId": "id"}), + ("Tenants", "transactionEvents", "array", "TransactionEvents", {"tenantId": "id"}), + ("Tenants", "startTransactions", "array", "StartTransactions", {"tenantId": "id"}), + ("Tenants", "stopTransactions", "array", "StopTransactions", {"tenantId": "id"}), + ("Tenants", "meterValues", "array", "MeterValues", {"tenantId": "id"}), + ("Tenants", "chargingStationNetworkProfiles", "array", "ChargingStationNetworkProfiles", {"tenantId": "id"}), + ("Tenants", "serverNetworkProfiles", "array", "ServerNetworkProfiles", {"tenantId": "id"}), + ("Tenants", "componentVariables", "array", "ComponentVariables", {"tenantId": "id"}), + ("Tenants", "components", "array", "Components", {"tenantId": "id"}), + ("Tenants", "variables", "array", "Variables", {"tenantId": "id"}), + ("Tenants", "evseTypes", "array", "EvseTypes", {"tenantId": "id"}), + ("Tenants", "compositeSchedules", "array", "CompositeSchedules", {"tenantId": "id"}), + ("Tenants", "changeConfigurations", "array", "ChangeConfigurations", {"tenantId": "id"}), + ("Tenants", "chargingStationSequences", "array", "ChargingStationSequences", {"tenantId": "id"}), + ("Tenants", "chargingStationSecurityInfos", "array", "ChargingStationSecurityInfos", {"tenantId": "id"}), + ("Tenants", "localListAuthorizations", "array", "LocalListAuthorizations", {"tenantId": "id"}), + ("Tenants", "localListVersionAuthorizations", "array", "LocalListVersionAuthorizations", {"tenantId": "id"}), + ("Tenants", "localListVersions", "array", "LocalListVersions", {"tenantId": "id"}), + ("Tenants", "sendLocalListAuthorizations", "array", "SendLocalListAuthorizations", {"tenantId": "id"}), + ("Tenants", "sendLocalLists", "array", "SendLocalLists", {"tenantId": "id"}), + ("Tenants", "deleteCertificateAttempts", "array", "DeleteCertificateAttempts", {"tenantId": "id"}), + ("Tenants", "installCertificateAttempts", "array", "InstallCertificateAttempts", {"tenantId": "id"}), + ("Tenants", "installedCertificates", "array", "InstalledCertificates", {"tenantId": "id"}), + ] + + created = 0 + failed = 0 + + for table, name, rel_type, remote_table, col_map in relations: + print(f"Creating {table}.{name}...", end=" ") + if create_relation(table, name, rel_type, remote_table, col_map): + print("OK") + created += 1 + else: + print("FAIL") + failed += 1 + + print(f"\n=== Résultat: {created} créées, {failed} échouées ===") + + # Retest critical relations + print("\n=== Vérification finale ===\n") + all_ok = True + for table, relation, fields in critical_relations: + ok, msg = test_relation(table, relation, fields) + status = "OK" if ok else "FAIL" + print(f" {status}: {table}.{relation}") + if not ok: + all_ok = False + + if all_ok: + print("\n🎉 Toutes les relations fonctionnent !") + else: + print("\n❌ Certaines relations ne fonctionnent pas encore.") + +if __name__ == '__main__': + main()