feat(16-06): migrate text command paths to GraphQL API

Replaced 3 Execute Command nodes (docker-socket-proxy curl) with GraphQL query chains:
- Query Containers for Action/Update/Batch (HTTP Request nodes)
- Normalize Action/Update/Batch Containers (GraphQL normalizers)
- Update Registry (Action/Update/Batch) (Container ID Registry)

Updated Prepare Match Input nodes to consume normalized container arrays.

Changes:
- Node count: 181 -> 187 (+9 new, -3 removed)
- Zero Execute Command nodes remain
- All text command entry points now use GraphQL
- Only docker-socket-proxy reference is infra exclusion filter (Phase 17)
- All connections use node names (not IDs)
- All HTTP nodes use Header Auth credential

Verified: Workflow pushed successfully to n8n (HTTP 200)
This commit is contained in:
Lucas Berger
2026-02-09 11:36:17 -05:00
parent fcf87b611d
commit e8ec62ed43
+315 -84
View File
@@ -415,20 +415,6 @@
400 400
] ]
}, },
{
"parameters": {
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
"options": {}
},
"id": "exec-docker-list-action",
"name": "Docker List for Action",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1120,
400
]
},
{ {
"parameters": { "parameters": {
"resource": "message", "resource": "message",
@@ -1256,20 +1242,6 @@
1000 1000
] ]
}, },
{
"parameters": {
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
"options": {}
},
"id": "exec-docker-list-update",
"name": "Docker List for Update",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1120,
1000
]
},
{ {
"parameters": { "parameters": {
"resource": "message", "resource": "message",
@@ -2054,20 +2026,6 @@
-100 -100
] ]
}, },
{
"parameters": {
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
"options": {}
},
"id": "exec-docker-list-batch",
"name": "Get Containers for Batch",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1340,
-300
]
},
{ {
"parameters": { "parameters": {
"method": "POST", "method": "POST",
@@ -4245,14 +4203,14 @@
}, },
{ {
"parameters": { "parameters": {
"jsCode": "// Prepare input for matching sub-workflow (action commands)\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\nreturn {\n json: {\n action: \"match_action\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};" "jsCode": "// Prepare input for matching sub-workflow (action commands)\nconst containers = $input.all().map(item => item.json);\nconst dockerOutput = JSON.stringify(containers);\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\nreturn {\n json: {\n action: \"match_action\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};"
}, },
"id": "code-prepare-action-match-input", "id": "code-prepare-action-match-input",
"name": "Prepare Action Match Input", "name": "Prepare Action Match Input",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1340, 1450,
400 400
] ]
}, },
@@ -4414,14 +4372,14 @@
}, },
{ {
"parameters": { "parameters": {
"jsCode": "// Prepare input for matching sub-workflow (update commands)\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\nreturn {\n json: {\n action: \"match_update\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};" "jsCode": "// Prepare input for matching sub-workflow (update commands)\nconst containers = $input.all().map(item => item.json);\nconst dockerOutput = JSON.stringify(containers);\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\nreturn {\n json: {\n action: \"match_update\",\n containerList: dockerOutput,\n searchTerm: containerQuery,\n selectedContainers: \"\",\n chatId: chatId,\n messageId: 0,\n correlationId: $input.item.json.correlationId || ''\n }\n};"
}, },
"id": "code-prepare-update-match-input", "id": "code-prepare-update-match-input",
"name": "Prepare Update Match Input", "name": "Prepare Update Match Input",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1340, 1450,
1000 1000
] ]
}, },
@@ -4560,14 +4518,14 @@
}, },
{ {
"parameters": { "parameters": {
"jsCode": "// Prepare input for matching sub-workflow (batch commands)\nconst dockerOutput = $input.item.json.stdout;\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Convert containerNames array to CSV for sub-workflow input\nconst selectedContainers = Array.isArray(containerNames) ? containerNames.join(',') : containerNames;\n\nreturn {\n json: {\n action: \"match_batch\",\n containerList: dockerOutput,\n searchTerm: \"\",\n selectedContainers: selectedContainers,\n chatId: chatId,\n messageId: messageId,\n correlationId: $input.item.json.correlationId || ''\n }\n};" "jsCode": "// Prepare input for matching sub-workflow (batch commands)\nconst containers = $input.all().map(item => item.json);\nconst dockerOutput = JSON.stringify(containers);\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Convert containerNames array to CSV for sub-workflow input\nconst selectedContainers = Array.isArray(containerNames) ? containerNames.join(',') : containerNames;\n\nreturn {\n json: {\n action: \"match_batch\",\n containerList: dockerOutput,\n searchTerm: \"\",\n selectedContainers: selectedContainers,\n chatId: chatId,\n messageId: messageId,\n correlationId: $input.item.json.correlationId || ''\n }\n};"
}, },
"id": "code-prepare-batch-match-input", "id": "code-prepare-batch-match-input",
"name": "Prepare Batch Match Input", "name": "Prepare Batch Match Input",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2, "typeVersion": 2,
"position": [ "position": [
1560, 1670,
-300 -300
] ]
}, },
@@ -5262,6 +5220,213 @@
"name": "Telegram account" "name": "Telegram account"
} }
} }
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "genericCredentialType",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image status } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
},
"genericAuthType": "httpHeaderAuth"
},
"id": "http-query-containers-action",
"name": "Query Containers for Action",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1120,
400
],
"credentials": {
"httpHeaderAuth": {
"id": "unraid-api-key-credential-id",
"name": "Unraid API Key"
}
}
},
{
"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": "code-normalize-action-containers",
"name": "Normalize Action Containers",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1230,
400
]
},
{
"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();"
},
"id": "code-registry-update-action-text",
"name": "Update Registry (Action)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
400
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "genericCredentialType",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image status } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
},
"genericAuthType": "httpHeaderAuth"
},
"id": "http-query-containers-update",
"name": "Query Containers for Update",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1120,
1000
],
"credentials": {
"httpHeaderAuth": {
"id": "unraid-api-key-credential-id",
"name": "Unraid API Key"
}
}
},
{
"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": "code-normalize-update-containers",
"name": "Normalize Update Containers",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1230,
1000
]
},
{
"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();"
},
"id": "code-registry-update-update-text",
"name": "Update Registry (Update)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
1000
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "genericCredentialType",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": \"query { docker { containers { id names state image status } } }\"} }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
},
"genericAuthType": "httpHeaderAuth"
},
"id": "http-query-containers-batch",
"name": "Query Containers for Batch",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
-300
],
"credentials": {
"httpHeaderAuth": {
"id": "unraid-api-key-credential-id",
"name": "Unraid API Key"
}
}
},
{
"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": "code-normalize-batch-containers",
"name": "Normalize Batch Containers",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1450,
-300
]
},
{
"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();"
},
"id": "code-registry-update-batch-text",
"name": "Update Registry (Batch)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
-300
]
} }
], ],
"connections": { "connections": {
@@ -5516,17 +5681,6 @@
] ]
] ]
}, },
"Docker List for Action": {
"main": [
[
{
"node": "Prepare Action Match Input",
"type": "main",
"index": 0
}
]
]
},
"Build Batch Keyboard": { "Build Batch Keyboard": {
"main": [ "main": [
[ [
@@ -5542,18 +5696,7 @@
"main": [ "main": [
[ [
{ {
"node": "Docker List for Update", "node": "Query Containers for Update",
"type": "main",
"index": 0
}
]
]
},
"Docker List for Update": {
"main": [
[
{
"node": "Prepare Update Match Input",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -5671,7 +5814,7 @@
"main": [ "main": [
[ [
{ {
"node": "Get Containers for Batch", "node": "Query Containers for Batch",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -5703,17 +5846,6 @@
] ]
] ]
}, },
"Get Containers for Batch": {
"main": [
[
{
"node": "Prepare Batch Match Input",
"type": "main",
"index": 0
}
]
]
},
"Route Batch Action": { "Route Batch Action": {
"main": [ "main": [
[ [
@@ -5812,7 +5944,7 @@
"main": [ "main": [
[ [
{ {
"node": "Docker List for Action", "node": "Query Containers for Action",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -7293,6 +7425,105 @@
} }
] ]
] ]
},
"Query Containers for Action": {
"main": [
[
{
"node": "Normalize Action Containers",
"type": "main",
"index": 0
}
]
]
},
"Normalize Action Containers": {
"main": [
[
{
"node": "Update Registry (Action)",
"type": "main",
"index": 0
}
]
]
},
"Update Registry (Action)": {
"main": [
[
{
"node": "Prepare Action Match Input",
"type": "main",
"index": 0
}
]
]
},
"Query Containers for Update": {
"main": [
[
{
"node": "Normalize Update Containers",
"type": "main",
"index": 0
}
]
]
},
"Normalize Update Containers": {
"main": [
[
{
"node": "Update Registry (Update)",
"type": "main",
"index": 0
}
]
]
},
"Update Registry (Update)": {
"main": [
[
{
"node": "Prepare Update Match Input",
"type": "main",
"index": 0
}
]
]
},
"Query Containers for Batch": {
"main": [
[
{
"node": "Normalize Batch Containers",
"type": "main",
"index": 0
}
]
]
},
"Normalize Batch Containers": {
"main": [
[
{
"node": "Update Registry (Batch)",
"type": "main",
"index": 0
}
]
]
},
"Update Registry (Batch)": {
"main": [
[
{
"node": "Prepare Batch Match Input",
"type": "main",
"index": 0
}
]
]
} }
}, },
"pinData": {}, "pinData": {},