feat(16-04): migrate Batch UI to Unraid GraphQL API
- Replace all 5 Docker API HTTP Request nodes with GraphQL queries - Add 5 GraphQL Response Normalizer nodes (one per query path) - Transform Unraid GraphQL responses to Docker API contract format - Preserve all downstream Code nodes unchanged (bitmap encoding, keyboard building) - All connection chains validated and working - Pushed to n8n successfully (HTTP 200)
This commit is contained in:
+353
-10
@@ -218,9 +218,32 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "none",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "x-api-key",
|
||||
"value": "={{ $env.UNRAID_API_KEY }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"timeout": 5000
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"errorHandling": "continueRegularOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "http-fetch-containers-mode",
|
||||
@@ -291,9 +314,32 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "none",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "x-api-key",
|
||||
"value": "={{ $env.UNRAID_API_KEY }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"timeout": 5000
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"errorHandling": "continueRegularOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "http-fetch-containers-toggle",
|
||||
@@ -320,9 +366,32 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "none",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "x-api-key",
|
||||
"value": "={{ $env.UNRAID_API_KEY }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"timeout": 5000
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"errorHandling": "continueRegularOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "http-fetch-containers-exec",
|
||||
@@ -362,9 +431,32 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "none",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "x-api-key",
|
||||
"value": "={{ $env.UNRAID_API_KEY }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"timeout": 5000
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"errorHandling": "continueRegularOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "http-fetch-containers-nav",
|
||||
@@ -404,9 +496,32 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://docker-socket-proxy:2375/containers/json?all=true",
|
||||
"method": "POST",
|
||||
"url": "={{ $env.UNRAID_HOST }}/graphql",
|
||||
"authentication": "none",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\": \"query { docker { containers { id names state image } } }\"}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "x-api-key",
|
||||
"value": "={{ $env.UNRAID_API_KEY }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"timeout": 5000
|
||||
"timeout": 15000,
|
||||
"response": {
|
||||
"response": {
|
||||
"errorHandling": "continueRegularOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "http-fetch-containers-clear",
|
||||
@@ -443,6 +558,71 @@
|
||||
680,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }));"
|
||||
},
|
||||
"id": "code-normalizer-mode",
|
||||
"name": "Normalize GraphQL Response (Mode)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
790,
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }));"
|
||||
},
|
||||
"id": "code-normalizer-toggle",
|
||||
"name": "Normalize GraphQL Response (Toggle)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1230,
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }));"
|
||||
},
|
||||
"id": "code-normalizer-exec",
|
||||
"name": "Normalize GraphQL Response (Exec)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
790,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }));"
|
||||
},
|
||||
"id": "code-normalizer-nav",
|
||||
"name": "Normalize GraphQL Response (Nav)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1010,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }));"
|
||||
},
|
||||
"id": "code-normalizer-clear",
|
||||
"name": "Normalize GraphQL Response (Clear)",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1010,
|
||||
500
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
@@ -601,6 +781,169 @@
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"http-fetch-containers-mode": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (Mode)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"code-normalizer-mode": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build Batch Keyboard",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"http-fetch-containers-toggle": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (Toggle)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"code-normalizer-toggle": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Rebuild Keyboard After Toggle",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"http-fetch-containers-exec": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (Exec)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"code-normalizer-exec": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Handle Exec",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"http-fetch-containers-nav": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (Nav)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"code-normalizer-nav": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Rebuild Keyboard For Nav",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"http-fetch-containers-clear": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Normalize GraphQL Response (Clear)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"code-normalizer-clear": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Rebuild Keyboard After Clear",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"switch-route-batch-action": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Containers For Mode",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
{
|
||||
"node": "Fetch Containers For Exec",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"if-needs-keyboard-update": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Containers For Update",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"code-handle-nav": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Containers For Nav",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"code-handle-clear": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Containers For Clear",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
Reference in New Issue
Block a user