feat(15-02): add GraphQL Response Normalizer utility node
- Transforms Unraid GraphQL response to Docker API contract - Maps id->Id, state (UPPERCASE)->State (lowercase), names->Names - STOPPED->exited conversion (Docker convention) - Validates response.errors[] and data.docker.containers structure - Standalone utility node at [200, 2600] for Phase 16 wiring
This commit is contained in:
+31
-1
@@ -4875,6 +4875,36 @@
|
||||
"name": "Telegram account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// GraphQL Response Normalizer - Transform Unraid GraphQL to Docker API contract\n// Input: $input.item.json = raw GraphQL response\n// Output: Array of normalized containers\n\nconst response = $input.item.json;\n\n// Validation: Check for GraphQL errors\nif (response.errors && response.errors.length > 0) {\n const messages = response.errors.map(e => e.message).join('; ');\n throw new Error(`GraphQL error: ${messages}`);\n}\n\n// Validation: Check response structure\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure: missing data.docker.containers');\n}\n\n// State mapping: Unraid UPPERCASE -> Docker lowercase\nconst stateMap = {\n 'RUNNING': 'running',\n 'STOPPED': 'exited', // Docker convention: stopped = exited\n 'PAUSED': 'paused'\n};\n\nfunction normalizeState(unraidState) {\n return stateMap[unraidState] || unraidState.toLowerCase();\n}\n\n// Transform each container\nconst containers = response.data.docker.containers;\nconst normalized = containers.map(container => {\n const dockerState = normalizeState(container.state);\n \n return {\n // Core fields matching Docker API contract\n Id: container.id, // Keep full PrefixedID (registry handles translation)\n Names: container.names, // Already has '/' prefix (Phase 14 verified)\n State: dockerState, // Normalized lowercase state\n Status: dockerState, // Docker has separate Status field\n Image: '', // Not available in basic query\n \n // Debug field: preserve original Unraid ID\n _unraidId: container.id\n };\n});\n\n// Return as array of items (n8n multi-item output format)\nreturn normalized.map(container => ({ json: container }));\n"
|
||||
},
|
||||
"id": "code-graphql-normalizer",
|
||||
"name": "GraphQL Response Normalizer",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
200,
|
||||
2600
|
||||
],
|
||||
"notes": "UTILITY: Transform Unraid GraphQL response to Docker API contract. Not connected - Phase 16 will wire this in."
|
||||
},
|
||||
{
|
||||
"id": "code-container-id-registry",
|
||||
"name": "Container ID Registry",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
200,
|
||||
2400
|
||||
],
|
||||
"notesDisplayMode": "show",
|
||||
"notes": "UTILITY: Container name <-> Unraid PrefixedID translation. Not connected - Phase 16 will wire this in.",
|
||||
"parameters": {
|
||||
"mode": "runOnceForAllItems",
|
||||
"jsCode": "// Container ID Registry\n// Maps container names to Unraid PrefixedID format (129-char server_hash:container_hash)\n\n// Initialize registry using static data with JSON serialization pattern\n// CRITICAL: Must use top-level assignment per CLAUDE.md\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\nif (!registry._lastRefresh) {\n registry._lastRefresh = 0;\n}\n\nconst containerMap = JSON.parse(registry._containerIdMap);\n\n/**\n * Update registry with fresh container data from Unraid GraphQL API\n * @param {Array} containers - Array of Unraid container objects {id, names[], state}\n * @returns {Object} Updated container map\n */\nfunction updateRegistry(containers) {\n const newMap = {};\n \n for (const container of containers) {\n // Extract container name: strip leading '/', lowercase\n const rawName = container.names[0];\n const name = rawName.startsWith('/') ? rawName.substring(1).toLowerCase() : rawName.toLowerCase();\n \n // Map name -> {name, unraidId}\n // The Unraid 'id' field IS the PrefixedID (129-char format)\n newMap[name] = {\n name: name,\n unraidId: container.id\n };\n }\n \n // Store timestamp\n registry._lastRefresh = Date.now();\n \n // Serialize (top-level assignment - this is what n8n persists)\n registry._containerIdMap = JSON.stringify(newMap);\n \n return newMap;\n}\n\n/**\n * Get Unraid PrefixedID for a container name\n * @param {string} containerName - Container name (e.g., 'plex', '/plex')\n * @returns {string} Unraid PrefixedID (129-char)\n * @throws {Error} If container not found\n */\nfunction getUnraidId(containerName) {\n // Normalize: strip leading '/', lowercase\n const name = containerName.startsWith('/') \n ? containerName.substring(1).toLowerCase() \n : containerName.toLowerCase();\n \n const entry = containerMap[name];\n \n if (!entry) {\n const registryAge = Date.now() - registry._lastRefresh;\n const isStale = registryAge > 60000; // 60 seconds\n \n if (isStale) {\n throw new Error(`Container \"${name}\" not found. Registry may be stale - try \"status\" to refresh.`);\n } else {\n throw new Error(`Container \"${name}\" not found. Check spelling or run \"status\" to see all containers.`);\n }\n }\n \n return entry.unraidId;\n}\n\n/**\n * Get full container entry by name\n * @param {string} containerName - Container name\n * @returns {Object} Container entry {name, unraidId}\n * @throws {Error} If container not found\n */\nfunction getContainerByName(containerName) {\n // Normalize name\n const name = containerName.startsWith('/') \n ? containerName.substring(1).toLowerCase() \n : containerName.toLowerCase();\n \n const entry = containerMap[name];\n \n if (!entry) {\n const registryAge = Date.now() - registry._lastRefresh;\n const isStale = registryAge > 60000; // 60 seconds\n \n if (isStale) {\n throw new Error(`Container \"${name}\" not found. Registry may be stale - try \"status\" to refresh.`);\n } else {\n throw new Error(`Container \"${name}\" not found. Check spelling or run \"status\" to see all containers.`);\n }\n }\n \n return entry;\n}\n\n// Detect mode based on input\nconst input = $input.item.json;\n\nif (input.containers && Array.isArray(input.containers)) {\n // Update mode: refresh registry with new container data\n const updatedMap = updateRegistry(input.containers);\n return {\n mode: 'update',\n registrySize: Object.keys(updatedMap).length,\n lastRefresh: new Date(registry._lastRefresh).toISOString(),\n containers: Object.keys(updatedMap)\n };\n} else if (input.containerName) {\n // Lookup mode: resolve container name to Unraid ID\n const unraidId = getUnraidId(input.containerName);\n const entry = getContainerByName(input.containerName);\n \n return {\n mode: 'lookup',\n containerName: entry.name,\n unraidId: unraidId\n };\n} else {\n throw new Error('Invalid input: provide either \"containers\" array or \"containerName\" string');\n}\n"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
@@ -6788,4 +6818,4 @@
|
||||
"tags": [],
|
||||
"triggerCount": 1,
|
||||
"active": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user