feat(16-05): replace 6 Docker API queries with Unraid GraphQL

- Migrated Get Container For Action to GraphQL
- Migrated Get Container For Cancel to GraphQL
- Migrated Get All Containers For Update All to GraphQL (with imageId)
- Migrated Fetch Containers For Update All Exec to GraphQL (with imageId)
- Migrated Get Container For Callback Update to GraphQL
- Migrated Fetch Containers For Bitmap Stop to GraphQL

Added 6 GraphQL Response Normalizer nodes and 6 Container ID Registry update nodes.
All nodes use $env.UNRAID_HOST and $env.UNRAID_API_KEY for authentication.
15-second timeout for myunraid.net cloud relay.
Workflow pushed to n8n successfully (HTTP 200).
This commit is contained in:
Lucas Berger
2026-02-09 10:34:20 -05:00
parent 0610f05dc8
commit ed1a114d74
+460 -15
View File
@@ -1802,9 +1802,34 @@
},
{
"parameters": {
"method": "GET",
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
"options": {}
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $env.UNRAID_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
},
"id": "http-get-container-for-action",
"name": "Get Container For Action",
@@ -1918,9 +1943,34 @@
},
{
"parameters": {
"method": "GET",
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
"options": {}
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $env.UNRAID_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
},
"id": "http-get-container-for-cancel",
"name": "Get Container For Cancel",
@@ -2683,8 +2733,34 @@
},
{
"parameters": {
"url": "http://docker-socket-proxy:2375/containers/json?all=false",
"options": {}
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $env.UNRAID_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image imageId } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
},
"id": "http-get-all-containers-update-all",
"name": "Get All Containers For Update All",
@@ -2974,8 +3050,34 @@
},
{
"parameters": {
"url": "http://docker-socket-proxy:2375/containers/json?all=false",
"options": {}
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $env.UNRAID_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image imageId } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
},
"id": "http-fetch-containers-update-all-exec",
"name": "Fetch Containers For Update All Exec",
@@ -3080,9 +3182,34 @@
},
{
"parameters": {
"method": "GET",
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
"options": {}
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $env.UNRAID_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
},
"id": "http-get-container-callback",
"name": "Get Container For Callback Update",
@@ -4751,9 +4878,33 @@
},
{
"parameters": {
"url": "http://docker-socket-proxy:2375/containers/json?all=true",
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $env.UNRAID_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image } } }\"} }}",
"options": {
"timeout": 5000
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
},
"id": "http-fetch-containers-bitmap-stop",
@@ -4984,6 +5135,162 @@
1000,
2400
]
},
{
"parameters": {
"jsCode": "// GraphQL Response Normalizer - Transform Unraid API to Docker API contract\nconst response = $input.item.json;\n\n// Validate GraphQL response\nif (response.errors) {\n throw new Error(`GraphQL Error: ${response.errors[0].message}`);\n}\n\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure');\n}\n\nconst containers = response.data.docker.containers;\n\n// Transform to Docker API format\nconst normalized = containers.map(container => {\n // State mapping: RUNNING\u2192running, STOPPED\u2192exited, PAUSED\u2192paused\n const stateLower = container.state ? container.state.toLowerCase() : 'unknown';\n \n return {\n Id: container.id, // Full 129-char PrefixedID\n Names: container.names || [container.name ? `/${container.name}` : '/unknown'],\n State: stateLower === 'stopped' ? 'exited' : stateLower,\n Status: container.status || stateLower,\n Image: container.image || '',\n ImageId: container.imageId || ''\n };\n});\n\nreturn normalized.map(container => ({ json: container }));\n"
},
"id": "normalize-graphql-action",
"name": "Normalize GraphQL Response (Action)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2300,
1100
]
},
{
"parameters": {
"jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();\n"
},
"id": "registry-update-action",
"name": "Update Container Registry (Action)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2600,
1100
]
},
{
"parameters": {
"jsCode": "// GraphQL Response Normalizer - Transform Unraid API to Docker API contract\nconst response = $input.item.json;\n\n// Validate GraphQL response\nif (response.errors) {\n throw new Error(`GraphQL Error: ${response.errors[0].message}`);\n}\n\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure');\n}\n\nconst containers = response.data.docker.containers;\n\n// Transform to Docker API format\nconst normalized = containers.map(container => {\n // State mapping: RUNNING\u2192running, STOPPED\u2192exited, PAUSED\u2192paused\n const stateLower = container.state ? container.state.toLowerCase() : 'unknown';\n \n return {\n Id: container.id, // Full 129-char PrefixedID\n Names: container.names || [container.name ? `/${container.name}` : '/unknown'],\n State: stateLower === 'stopped' ? 'exited' : stateLower,\n Status: container.status || stateLower,\n Image: container.image || '',\n ImageId: container.imageId || ''\n };\n});\n\nreturn normalized.map(container => ({ json: container }));\n"
},
"id": "normalize-graphql-cancel",
"name": "Normalize GraphQL Response (Cancel)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2080,
1800
]
},
{
"parameters": {
"jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();\n"
},
"id": "registry-update-cancel",
"name": "Update Container Registry (Cancel)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2380,
1800
]
},
{
"parameters": {
"jsCode": "// GraphQL Response Normalizer - Transform Unraid API to Docker API contract\nconst response = $input.item.json;\n\n// Validate GraphQL response\nif (response.errors) {\n throw new Error(`GraphQL Error: ${response.errors[0].message}`);\n}\n\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure');\n}\n\nconst containers = response.data.docker.containers;\n\n// Transform to Docker API format\nconst normalized = containers.map(container => {\n // State mapping: RUNNING\u2192running, STOPPED\u2192exited, PAUSED\u2192paused\n const stateLower = container.state ? container.state.toLowerCase() : 'unknown';\n \n return {\n Id: container.id, // Full 129-char PrefixedID\n Names: container.names || [container.name ? `/${container.name}` : '/unknown'],\n State: stateLower === 'stopped' ? 'exited' : stateLower,\n Status: container.status || stateLower,\n Image: container.image || '',\n ImageId: container.imageId || ''\n };\n});\n\nreturn normalized.map(container => ({ json: container }));\n"
},
"id": "normalize-graphql-update-all",
"name": "Normalize GraphQL Response (Update All)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1500,
2200
]
},
{
"parameters": {
"jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();\n"
},
"id": "registry-update-update-all",
"name": "Update Container Registry (Update All)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1800,
2200
]
},
{
"parameters": {
"jsCode": "// GraphQL Response Normalizer - Transform Unraid API to Docker API contract\nconst response = $input.item.json;\n\n// Validate GraphQL response\nif (response.errors) {\n throw new Error(`GraphQL Error: ${response.errors[0].message}`);\n}\n\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure');\n}\n\nconst containers = response.data.docker.containers;\n\n// Transform to Docker API format\nconst normalized = containers.map(container => {\n // State mapping: RUNNING\u2192running, STOPPED\u2192exited, PAUSED\u2192paused\n const stateLower = container.state ? container.state.toLowerCase() : 'unknown';\n \n return {\n Id: container.id, // Full 129-char PrefixedID\n Names: container.names || [container.name ? `/${container.name}` : '/unknown'],\n State: stateLower === 'stopped' ? 'exited' : stateLower,\n Status: container.status || stateLower,\n Image: container.image || '',\n ImageId: container.imageId || ''\n };\n});\n\nreturn normalized.map(container => ({ json: container }));\n"
},
"id": "normalize-graphql-update-all-exec",
"name": "Normalize GraphQL Response (Update All Exec)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2500,
2600
]
},
{
"parameters": {
"jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();\n"
},
"id": "registry-update-update-all-exec",
"name": "Update Container Registry (Update All Exec)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2800,
2600
]
},
{
"parameters": {
"jsCode": "// GraphQL Response Normalizer - Transform Unraid API to Docker API contract\nconst response = $input.item.json;\n\n// Validate GraphQL response\nif (response.errors) {\n throw new Error(`GraphQL Error: ${response.errors[0].message}`);\n}\n\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure');\n}\n\nconst containers = response.data.docker.containers;\n\n// Transform to Docker API format\nconst normalized = containers.map(container => {\n // State mapping: RUNNING\u2192running, STOPPED\u2192exited, PAUSED\u2192paused\n const stateLower = container.state ? container.state.toLowerCase() : 'unknown';\n \n return {\n Id: container.id, // Full 129-char PrefixedID\n Names: container.names || [container.name ? `/${container.name}` : '/unknown'],\n State: stateLower === 'stopped' ? 'exited' : stateLower,\n Status: container.status || stateLower,\n Image: container.image || '',\n ImageId: container.imageId || ''\n };\n});\n\nreturn normalized.map(container => ({ json: container }));\n"
},
"id": "normalize-graphql-callback",
"name": "Normalize GraphQL Response (Callback)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2740,
1650
]
},
{
"parameters": {
"jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();\n"
},
"id": "registry-update-callback",
"name": "Update Container Registry (Callback)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3040,
1650
]
},
{
"parameters": {
"jsCode": "// GraphQL Response Normalizer - Transform Unraid API to Docker API contract\nconst response = $input.item.json;\n\n// Validate GraphQL response\nif (response.errors) {\n throw new Error(`GraphQL Error: ${response.errors[0].message}`);\n}\n\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure');\n}\n\nconst containers = response.data.docker.containers;\n\n// Transform to Docker API format\nconst normalized = containers.map(container => {\n // State mapping: RUNNING\u2192running, STOPPED\u2192exited, PAUSED\u2192paused\n const stateLower = container.state ? container.state.toLowerCase() : 'unknown';\n \n return {\n Id: container.id, // Full 129-char PrefixedID\n Names: container.names || [container.name ? `/${container.name}` : '/unknown'],\n State: stateLower === 'stopped' ? 'exited' : stateLower,\n Status: container.status || stateLower,\n Image: container.image || '',\n ImageId: container.imageId || ''\n };\n});\n\nreturn normalized.map(container => ({ json: container }));\n"
},
"id": "normalize-graphql-bitmap-stop",
"name": "Normalize GraphQL Response (Bitmap Stop)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2300,
700
]
},
{
"parameters": {
"jsCode": "// Update Container ID Registry - Store name\u2192PrefixedID mappings\nconst containers = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\n// Parse existing registry\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Update registry with current containers\ncontainers.forEach(item => {\n const container = item.json;\n const name = container.Names?.[0]?.substring(1) || 'unknown'; // Remove leading /\n \n registry[name] = {\n name: name,\n unraidId: container.Id, // Full PrefixedID\n prefixedId: container.Id, // Alias for consistency\n lastSeen: Date.now()\n };\n});\n\n// Write back to static data (top-level assignment for persistence)\nstaticData._containerIdRegistry = JSON.stringify(registry);\nstaticData._containerRegistryLastUpdate = Date.now();\n\n// Pass through all container data\nreturn $input.all();\n"
},
"id": "registry-update-bitmap-stop",
"name": "Update Container Registry (Bitmap Stop)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2600,
700
]
}
],
"connections": {
@@ -6887,7 +7194,145 @@
}
]
]
},
"http-get-container-for-action": {
"main": [
[
{
"node": "normalize-graphql-action",
"type": "main",
"index": 0
}
]
]
},
"normalize-graphql-action": {
"main": [
[
{
"node": "registry-update-action",
"type": "main",
"index": 0
}
]
]
},
"registry-update-action": {},
"http-get-container-for-cancel": {
"main": [
[
{
"node": "normalize-graphql-cancel",
"type": "main",
"index": 0
}
]
]
},
"normalize-graphql-cancel": {
"main": [
[
{
"node": "registry-update-cancel",
"type": "main",
"index": 0
}
]
]
},
"registry-update-cancel": {},
"http-get-all-containers-update-all": {
"main": [
[
{
"node": "normalize-graphql-update-all",
"type": "main",
"index": 0
}
]
]
},
"normalize-graphql-update-all": {
"main": [
[
{
"node": "registry-update-update-all",
"type": "main",
"index": 0
}
]
]
},
"registry-update-update-all": {},
"http-fetch-containers-update-all-exec": {
"main": [
[
{
"node": "normalize-graphql-update-all-exec",
"type": "main",
"index": 0
}
]
]
},
"normalize-graphql-update-all-exec": {
"main": [
[
{
"node": "registry-update-update-all-exec",
"type": "main",
"index": 0
}
]
]
},
"registry-update-update-all-exec": {},
"http-get-container-callback": {
"main": [
[
{
"node": "normalize-graphql-callback",
"type": "main",
"index": 0
}
]
]
},
"normalize-graphql-callback": {
"main": [
[
{
"node": "registry-update-callback",
"type": "main",
"index": 0
}
]
]
},
"registry-update-callback": {},
"http-fetch-containers-bitmap-stop": {
"main": [
[
{
"node": "normalize-graphql-bitmap-stop",
"type": "main",
"index": 0
}
]
]
},
"normalize-graphql-bitmap-stop": {
"main": [
[
{
"node": "registry-update-bitmap-stop",
"type": "main",
"index": 0
}
]
]
},
"registry-update-bitmap-stop": {}
},
"pinData": {},
"settings": {