The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"createdAt": "2025-10-12T12:32:03.646Z",
"updatedAt": "2025-10-30T16:24:10.000Z",
"id": "GPIoYMXINj8bnfIw",
"name": "Components",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"jsCode": "// Get all incoming items\nconst items = $input.all();\n\n// Extract query from the first item, or use an empty object\nconst q = (items[0] && items[0].json && items[0].json.query) ? items[0].json.query : {};\n\n// Safely parse and default parameters\n// CHANGED: Use 'search' instead of 'name' to match frontend\nconst search = q.search || q.name || \"\";\nconst limit = Number.isInteger(parseInt(q.itemsPerPage || q.pageSize)) ? parseInt(q.itemsPerPage || q.pageSize) : 10;\nconst pageNo = Number.isInteger(parseInt(q.page || q.pageNo)) ? parseInt(q.page || q.pageNo) : 1;\nconst offset = (pageNo - 1) * limit;\n\n// Return a single item with params for the next node\nreturn [\n {\n json: {\n params: {\n search,\n limit,\n offset\n }\n }\n }\n];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
48,
96
],
"id": "476a6959-7d4e-485b-ad55-b283ed3d91a9",
"name": "Parse Params"
},
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-176,
192
],
"id": "128d2ea7-adf6-4710-9118-b243570bcd01",
"name": "When clicking \u2018Execute workflow\u2019"
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH purchased_cte AS (\n SELECT component_id, SUM(quantity_ordered) AS purchased\n FROM order_parts\n GROUP BY component_id\n),\nused_cte AS (\n SELECT component_id, SUM(quantity_used) AS used\n FROM (\n SELECT component_id, quantity_used FROM project_components\n UNION ALL\n SELECT component_id, quantity_used FROM general_usage\n ) AS combined_usage\n GROUP BY component_id\n),\navg_price_cte AS (\n SELECT component_id, AVG(unit_cost) AS avg_unit_price\n FROM order_parts\n GROUP BY component_id\n),\ntotal_items_cte AS (\n SELECT COUNT(1) AS total_items\n FROM components c\n LEFT JOIN categories ct ON ct.category_id = c.category_id\n LEFT JOIN boxes b ON b.box_id = c.box_id\n WHERE \n c.name ILIKE '%{{ $json.params.search }}%'\n OR ct.category_name ILIKE '%{{ $json.params.search }}%'\n OR b.box_label ILIKE '%{{ $json.params.search }}%'\n)\nSELECT \n c.name,\n c.component_id AS ID,\n c.description,\n c.date_added,\n c.image_content,\n c.image_type,\n b.box_code,\n c.category_id,\n b.box_label,\n ct.category_name,\n b.box_id,\n COALESCE(purchased_cte.purchased, 0) AS purchased,\n COALESCE(used_cte.used, 0) AS used,\n COALESCE(purchased_cte.purchased, 0) - COALESCE(used_cte.used, 0) AS stock,\n total_items_cte.total_items AS totalItems,\n COALESCE(avg_price_cte.avg_unit_price, 0) AS avg_unit_price,\n CASE \n WHEN COALESCE(purchased_cte.purchased, 0) - COALESCE(used_cte.used, 0) > 0 \n THEN 'In Stock' \n ELSE 'No Stock' \n END AS status\nFROM \n components c\nLEFT JOIN purchased_cte ON purchased_cte.component_id = c.component_id\nLEFT JOIN used_cte ON used_cte.component_id = c.component_id\nLEFT JOIN avg_price_cte ON avg_price_cte.component_id = c.component_id\nLEFT JOIN boxes b ON b.box_id = c.box_id\nLEFT JOIN categories ct ON ct.category_id = c.category_id\nCROSS JOIN total_items_cte\nWHERE \n c.name ILIKE '%{{ $json.params.search }}%'\n OR ct.category_name ILIKE '%{{ $json.params.search }}%'\n OR b.box_label ILIKE '%{{ $json.params.search }}%'\nORDER BY c.name\nLIMIT {{ $json.params.limit }}\nOFFSET {{ $json.params.offset }}\n",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
272,
96
],
"id": "03b6398e-9237-42d4-a21e-cb150f3e3e54",
"name": "Execute a SQL query",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const keysToRemove = ['image_content', 'image_type'];\n\nreturn $input.all().map(item => {\n const json = {...item.json};\n keysToRemove.forEach(k => delete json[k]);\n \n // Optionally recalculate or add the filename from existing properties\n json.filename = `http://192.168.0.7:5678/webhook/image?filename=component_${item.json.id}.${item.json.image_type || 'jpeg'}`;\n\n return { json };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
496,
0
],
"id": "3c9ee4fa-51bf-4b9f-b98f-120c9aecab52",
"name": "Remove Image Info"
},
{
"parameters": {
"jsCode": "return $input.all().map(item => {\n const data = item.json;\n const imageBuffer = Buffer.from(data.image_content.data);\n const imageType = data.image_type || 'jpeg';\n const base64Image = imageBuffer.toString('base64');\n\n // Generate filename\n const filename = `component_${data.id}.${imageType}`;\n\n return {\n json: { // 'data' is the standard expected binary property name\n data: base64Image,\n mimeType: `image/${imageType}`,\n fileName: filename\n }\n };\n});\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
496,
192
],
"id": "5cfce6ac-ccfb-4e05-8083-6d195288feb8",
"name": "Image"
},
{
"parameters": {
"operation": "toBinary",
"sourceProperty": "data",
"options": {
"fileName": "={{ $json.fileName }}",
"mimeType": "={{ $json.mimeType }}"
}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [
720,
192
],
"id": "3f4f68e0-9fbd-4182-acb0-87c707d680ef",
"name": "Convert to File"
},
{
"parameters": {
"operation": "write",
"fileName": "=/images/components/{{ $('Image').item.json.fileName }}",
"options": {}
},
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
944,
192
],
"id": "2c08fb0f-487a-4701-a775-e183dfa9d071",
"name": "Read/Write Files from Disk"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
720,
0
],
"id": "7bf0ffa2-909a-444b-b200-c75e41c4b36b",
"name": "Respond to Webhook"
},
{
"parameters": {
"path": "api/components/stocks",
"responseMode": "responseNode",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-176,
0
],
"id": "17c54329-4efb-4522-81d8-6e6e4c1540d5",
"name": "api/components/stocks"
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH purchased_cte AS (\n SELECT component_id, SUM(quantity_ordered) AS purchased\n FROM order_parts\n GROUP BY component_id\n),\nused_cte AS (\n SELECT component_id, SUM(quantity_used) AS used\n FROM (\n SELECT component_id, quantity_used FROM project_components\n UNION ALL\n SELECT component_id, quantity_used FROM general_usage\n ) AS combined_usage\n GROUP BY component_id\n),\nstock_cte AS (\n SELECT\n c.component_id,\n COALESCE(purchased_cte.purchased, 0) - COALESCE(used_cte.used, 0) AS stock\n FROM components c\n LEFT JOIN purchased_cte ON purchased_cte.component_id = c.component_id\n LEFT JOIN used_cte ON used_cte.component_id = c.component_id\n)\nSELECT\n COUNT(*) AS total_items,\n COUNT(CASE WHEN stock > 0 THEN 1 END) AS in_stock,\n COUNT(CASE WHEN stock > 0 AND stock < 10 THEN 1 END) AS low_stock,\n COUNT(CASE WHEN stock <= 0 THEN 1 END) AS no_stock\nFROM stock_cte;\n",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
48,
416
],
"id": "4a6c4bbd-1a61-46d4-8c67-95048e61949f",
"name": "Execute a SQL query1",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
272,
416
],
"id": "3d20b408-b9f5-421b-9acc-dfe7c2196c6a",
"name": "Respond to Webhook1"
},
{
"parameters": {
"path": "api/components/gist",
"responseMode": "responseNode",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-176,
416
],
"id": "a32d52e6-0010-4098-afc3-b2f636274d7f",
"name": "api/components/gist"
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/stocks/detail",
"responseMode": "responseNode",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"name": "Stock Detail Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-176,
640
],
"id": "56e0c661-0dc7-4d2f-87e9-5ac494ca59f3"
},
{
"parameters": {
"jsCode": "return [ { json: { id: $json.body.id } } ];"
},
"name": "Extract ID From Body",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
48,
640
],
"id": "d68a7cbe-b420-4eae-bfad-5af82748f002"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n c.name,\n c.component_id AS id,\n c.description,\n c.date_added,\n c.image_content,\n c.image_type,\n b.box_code,\n c.category_id,\n b.box_label,\n ct.category_name,\n b.box_id,\n COALESCE(purchased.purchased, 0) AS total_purchased,\n COALESCE(used.used, 0) AS total_used,\n COALESCE(purchased.purchased, 0) - COALESCE(used.used, 0) AS stock,\n COALESCE(avg_price.avg_unit_price, 0) AS avg_unit_price,\n CASE WHEN COALESCE(purchased.purchased, 0) - COALESCE(used.used, 0) > 0 THEN 'In Stock' ELSE 'No Stock' END AS status,\n -- Purchase history\n (SELECT json_agg(p)\n FROM (\n SELECT order_id, quantity_ordered, unit_cost, url\n FROM order_parts\n WHERE component_id = c.component_id\n ORDER BY order_id DESC\n LIMIT 10\n ) p) AS purchase_history,\n -- Project usage history always as array\n COALESCE(\n (SELECT json_agg(pr)\n FROM (\n SELECT project_id, quantity_used, date\n FROM project_components\n WHERE component_id = c.component_id\n ORDER BY date DESC\n LIMIT 10\n ) pr),\n '[]'::json\n ) AS project_usage_history,\n -- General usage history always as array\n COALESCE(\n (SELECT json_agg(gu)\n FROM (\n SELECT usage_id, quantity_used, usage_date, purpose\n FROM general_usage\n WHERE component_id = c.component_id\n ORDER BY usage_date DESC\n LIMIT 10\n ) gu),\n '[]'::json\n ) AS general_usage_history\nFROM components c\nLEFT JOIN boxes b ON b.box_id = c.box_id\nLEFT JOIN categories ct ON ct.category_id = c.category_id\nLEFT JOIN (\n SELECT component_id, SUM(quantity_ordered) AS purchased\n FROM order_parts\n GROUP BY component_id\n) purchased ON purchased.component_id = c.component_id\nLEFT JOIN (\n SELECT component_id, SUM(quantity_used) AS used\n FROM (\n SELECT component_id, quantity_used FROM project_components\n UNION ALL\n SELECT component_id, quantity_used FROM general_usage\n ) usage_all\n GROUP BY component_id\n) used ON used.component_id = c.component_id\nLEFT JOIN (\n SELECT component_id, AVG(unit_cost) AS avg_unit_price\n FROM order_parts\n GROUP BY component_id\n) avg_price ON avg_price.component_id = c.component_id\nWHERE c.component_id = {{ $json.id }};\n",
"options": {}
},
"name": "Get Stock Detail Query",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
272,
640
],
"id": "30bc1e38-0be4-412f-bc42-e71d9a39853c",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\nif (!item) {\n return [{ json: { success: false, message: 'Component not found', data: null } }];\n}\nreturn [{ json: { success: true, message: 'Component found', data: item } }];"
},
"name": "Format Stock Detail Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
640
],
"id": "e5148165-cb2b-4b23-acf0-ed41d73c16c7"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"name": "Respond Stock Detail",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
944,
640
],
"id": "7ea8c4c7-455e-48c5-af77-ac10b9879891"
},
{
"parameters": {
"jsCode": "const keysToRemove = ['image_content', 'image_type'];\n\nreturn $input.all().map(item => {\n const json = {...item.json};\n keysToRemove.forEach(k => delete json[k]);\n \n // Optionally recalculate or add the filename from existing properties\n json.filename = `http://192.168.0.7:5678/webhook/image?filename=component_${item.json.id}.${item.json.image_type || 'jpeg'}`;\n\n return { json };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
496,
640
],
"id": "ae9db593-b463-43ee-bd85-54a4ff58e5cf",
"name": "Remove Image Info1"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n c.name,\n c.component_id AS ID,\n c.description,\n c.date_added,\n c.image_content,\n c.image_type,\n ct.category_name\nFROM components c\nLEFT JOIN categories ct ON ct.category_id = c.category_id",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
48,
864
],
"id": "948ac8cf-b1a8-4492-b430-b9242a75daf5",
"name": "Execute a SQL query2",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const keysToRemove = ['image_content', 'image_type'];\n\nreturn $input.all().map(item => {\n const json = {...item.json};\n keysToRemove.forEach(k => delete json[k]);\n \n // Optionally recalculate or add the filename from existing properties\n json.filename = `http://192.168.0.7:5678/webhook/image?filename=component_${item.json.id}.${item.json.image_type || 'jpeg'}`;\n\n return { json };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
272,
864
],
"id": "223ec271-a387-4106-83c3-b49655d7c1be",
"name": "Remove Image Info2"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
496,
864
],
"id": "8e509b0a-898f-4709-922d-0bc562f52904",
"name": "Respond to Webhook2"
},
{
"parameters": {
"path": "api/components/all",
"responseMode": "responseNode",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-176,
864
],
"id": "14fb14da-d7eb-4b53-8db4-c3a025b2bb58",
"name": "api/components/stocks1"
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/purchase-history",
"responseMode": "responseNode",
"options": {}
},
"id": "dcc90e64-ec63-4296-8304-0547e7321131",
"name": "Purchase History",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-176,
1088
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n op.order_part_id,\n op.order_id,\n op.quantity_ordered,\n op.unit_cost,\n op.url,\n o.order_date,\n o.delivery_date,\n o.status as order_status,\n v.vendor_id,\n v.vendor_name,\n v.contact_email,\n v.phone_number,\n (op.quantity_ordered * op.unit_cost) as total_cost\nFROM order_parts op\nINNER JOIN orders o ON op.order_id = o.order_id\nINNER JOIN vendors v ON o.vendor_id = v.vendor_id\nWHERE op.component_id = {{ $json.body.component_id }}\nORDER BY o.order_date DESC",
"options": {}
},
"id": "cbb77840-889f-497c-b4b4-d5dd36156b07",
"name": "Postgres",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
48,
1088
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Collect all items into an array\nconst items = [];\nfor (const item of $input.all()) {\n items.push(item.json);\n}\n\n// Wrap the array in an object with a 'data' property\nreturn { items };\n"
},
"id": "7b3cf21b-4d37-4c46-b4cb-a42cf205e142",
"name": "Format as Array",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
272,
1088
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "2effc97b-95e9-43cf-a2ae-d277f2166044",
"name": "Respond to Webhook3",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
496,
1088
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/project-usage",
"responseMode": "responseNode",
"options": {}
},
"id": "b60559c5-3282-489a-a1a4-be1b97ba4cf7",
"name": "Project Usage Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-176,
1312
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n pc.project_id,\n pc.component_id,\n pc.quantity_used,\n pc.date,\n p.project_name,\n p.status as project_status,\n p.start_date,\n p.end_date,\n p.is_yt_project,\n p.git_repository\nFROM project_components pc\nINNER JOIN projects p ON pc.project_id = p.project_id\nWHERE pc.component_id = {{ $json.body.component_id }}\nORDER BY pc.date DESC",
"options": {}
},
"id": "adf36c0f-9ae9-4450-b2c6-1ce94c378c5b",
"name": "Postgres1",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
48,
1312
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ JSON.stringify($input.all().map(item => item.json)) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "47b420d3-3548-4218-ba42-24a58c754e5d",
"name": "Respond to Webhook4",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
272,
1312
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/general-usage",
"responseMode": "responseNode",
"options": {}
},
"id": "fe5353f0-acc2-4851-ba56-0857b29822d2",
"name": "General Usage Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-176,
1536
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT \n usage_id,\n component_id,\n quantity_used,\n usage_date,\n purpose\nFROM general_usage\nWHERE component_id = {{ $json.body.component_id }}\nORDER BY usage_date DESC",
"options": {}
},
"id": "f3038509-67dc-4264-a564-154fc28f36cc",
"name": "Postgres2",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
48,
1536
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ JSON.stringify($input.all().map(item => item.json)) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "48992505-fa26-4c70-ace2-7a748da34720",
"name": "Respond to Webhook5",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
272,
1536
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "{{ $json.sql }}",
"options": {}
},
"id": "d6e0f7b1-d033-4e36-ad37-98bc14febba9",
"name": "Postgres3",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
272,
1760
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ JSON.stringify($input.all().map(item => item.json)) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "374e5b69-2039-456c-b1a7-fef6294c760d",
"name": "Respond to Webhook6",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
720,
1760
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/update",
"responseMode": "responseNode",
"options": {}
},
"id": "ba2b7d71-85c5-4991-98c8-c6891752e09c",
"name": "Component Update",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-176,
1760
]
},
{
"parameters": {
"jsCode": "const componentId = $json.body.component_id;\nconst updates = [];\n\n// Only add fields that are actually present in the request\nif ($json.body.name !== undefined && $json.body.name !== null) {\n const sanitized = String($json.body.name).replace(/'/g, \"''\");\n updates.push(`name = '${sanitized}'`);\n}\n\nif ($json.body.category_id !== undefined && $json.body.category_id !== null) {\n updates.push(`category_id = ${$json.body.category_id}`);\n}\n\nif ($json.body.box_id !== undefined && $json.body.box_id !== null) {\n updates.push(`box_id = ${$json.body.box_id}`);\n}\n\nif ($json.body.description !== undefined && $json.body.description !== null) {\n const sanitized = String($json.body.description).replace(/'/g, \"''\");\n updates.push(`description = '${sanitized}'`);\n}\n\n// Handle image update - only if all three are present\nif ($json.body.image_content && $json.body.image_type && $json.body.imaeg_name) {\n updates.push(`image_content = decode('${$json.body.image_content}', 'base64')`);\n updates.push(`image_type = '${$json.body.image_type}'`);\n const sanitizedName = String($json.body.imaeg_name).replace(/'/g, \"''\");\n updates.push(`imaeg_name = '${sanitizedName}'`);\n}\n\n// Check if there are any updates\nif (updates.length === 0) {\n throw new Error('No fields to update');\n}\n\n// Build SQL - FIXED: Construct proper data URL with image/ prefix\nconst sql = `UPDATE components \nSET ${updates.join(', ')}\nWHERE component_id = ${componentId}\nRETURNING \n component_id, \n name, \n category_id, \n box_id, \n description, \n date_added,\n 'data:image/' || image_type || ';base64,' || encode(image_content, 'base64') as filename,\n image_type, \n imaeg_name`;\n\nreturn { json: { sql } };\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
48,
1760
],
"id": "02b4b034-47d6-4244-8e78-9a48a7d68d74",
"name": "Code in JavaScript"
},
{
"parameters": {
"jsCode": "const result = $input.all()[0].json;\n\nconsole.log('Database result:', result);\n\n// Return the updated component\nreturn {\n json: result[0] || result\n};\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
496,
1760
],
"id": "21c6a0e7-3525-4971-84dd-556af25ae8fe",
"name": "Code in JavaScript1"
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ $json.sql }}",
"options": {}
},
"id": "951ea4f0-707b-453e-aaa9-e344a7b85d4c",
"name": "Postgres - Create Component",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
272,
1984
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/create",
"responseMode": "responseNode",
"options": {}
},
"id": "adc8026c-1a93-49a5-8432-6b060ae096c5",
"name": "Component Create Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-176,
1984
]
},
{
"parameters": {
"jsCode": "// Extract data from request body\nconst body = $json.body;\n\n// Validate required fields\nif (!body.name || !body.category_id) {\n throw new Error('Required fields missing: name and category_id are mandatory');\n}\n\n// Sanitize string values to prevent SQL injection\nconst sanitizeName = String(body.name).replace(/'/g, \"''\");\nconst sanitizeDescription = body.description ? String(body.description).replace(/'/g, \"''\") : '';\n\n// Build INSERT query with RETURNING clause\nlet sql = `INSERT INTO components (\n name,\n category_id,\n box_id,\n description`;\n\n// Add image columns if image data is provided\nif (body.image_content && body.image_type && body.imaeg_name) {\n sql += `,\n image_content,\n image_type,\n imaeg_name`;\n}\n\nsql += `\n) VALUES (\n '${sanitizeName}',\n ${body.category_id},\n ${body.box_id || 'NULL'},\n '${sanitizeDescription}'`;\n\n// Add image values if provided\nif (body.image_content && body.image_type && body.imaeg_name) {\n const sanitizeImageName = String(body.imaeg_name).replace(/'/g, \"''\");\n sql += `,\n decode('${body.image_content}', 'base64'),\n '${body.image_type}',\n '${sanitizeImageName}'`;\n}\n\nsql += `\n)\nRETURNING \n component_id,\n name,\n category_id,\n box_id,\n description,\n date_added,\n CASE \n WHEN image_content IS NOT NULL THEN 'data:image/' || image_type || ';base64,' || encode(image_content, 'base64')\n ELSE NULL\n END as filename,\n image_type,\n imaeg_name`;\n\nreturn { json: { sql } };"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
48,
1984
],
"id": "f7418230-ec96-4068-bd96-402769b5bbae",
"name": "Build Create SQL"
},
{
"parameters": {
"jsCode": "const result = $input.all()[0].json;\n\nconsole.log('Database create result:', result);\n\n// Return the newly created component\nreturn {\n json: result[0] || result\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
496,
1984
],
"id": "753d9e87-28ec-4165-a7f2-bc08f72e824c",
"name": "Format Response"
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ JSON.stringify($input.all().map(item => item.json)) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "b9bd1a3a-822b-4175-86bb-170be1da74a0",
"name": "Respond to Webhook7",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
720,
1984
]
}
],
"connections": {
"Parse Params": {
"main": [
[
{
"node": "Execute a SQL query",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Parse Params",
"type": "main",
"index": 0
}
]
]
},
"Execute a SQL query": {
"main": [
[
{
"node": "Remove Image Info",
"type": "main",
"index": 0
},
{
"node": "Image",
"type": "main",
"index": 0
}
]
]
},
"Remove Image Info": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Image": {
"main": [
[
{
"node": "Convert to File",
"type": "main",
"index": 0
}
]
]
},
"Convert to File": {
"main": [
[
{
"node": "Read/Write Files from Disk",
"type": "main",
"index": 0
}
]
]
},
"api/components/stocks": {
"main": [
[
{
"node": "Parse Params",
"type": "main",
"index": 0
}
]
]
},
"Execute a SQL query1": {
"main": [
[
{
"node": "Respond to Webhook1",
"type": "main",
"index": 0
}
]
]
},
"api/components/gist": {
"main": [
[
{
"node": "Execute a SQL query1",
"type": "main",
"index": 0
}
]
]
},
"Stock Detail Webhook": {
"main": [
[
{
"node": "Extract ID From Body",
"type": "main",
"index": 0
}
]
]
},
"Extract ID From Body": {
"main": [
[
{
"node": "Get Stock Detail Query",
"type": "main",
"index": 0
}
]
]
},
"Get Stock Detail Query": {
"main": [
[
{
"node": "Remove Image Info1",
"type": "main",
"index": 0
}
]
]
},
"Format Stock Detail Response": {
"main": [
[
{
"node": "Respond Stock Detail",
"type": "main",
"index": 0
}
]
]
},
"Remove Image Info1": {
"main": [
[
{
"node": "Format Stock Detail Response",
"type": "main",
"index": 0
}
]
]
},
"Execute a SQL query2": {
"main": [
[
{
"node": "Remove Image Info2",
"type": "main",
"index": 0
}
]
]
},
"Remove Image Info2": {
"main": [
[
{
"node": "Respond to Webhook2",
"type": "main",
"index": 0
}
]
]
},
"api/components/stocks1": {
"main": [
[
{
"node": "Execute a SQL query2",
"type": "main",
"index": 0
}
]
]
},
"Purchase History": {
"main": [
[
{
"node": "Postgres",
"type": "main",
"index": 0
}
]
]
},
"Postgres": {
"main": [
[
{
"node": "Format as Array",
"type": "main",
"index": 0
}
]
]
},
"Format as Array": {
"main": [
[
{
"node": "Respond to Webhook3",
"type": "main",
"index": 0
}
]
]
},
"Project Usage Webhook": {
"main": [
[
{
"node": "Postgres1",
"type": "main",
"index": 0
}
]
]
},
"Postgres1": {
"main": [
[
{
"node": "Respond to Webhook4",
"type": "main",
"index": 0
}
]
]
},
"General Usage Webhook": {
"main": [
[
{
"node": "Postgres2",
"type": "main",
"index": 0
}
]
]
},
"Postgres2": {
"main": [
[
{
"node": "Respond to Webhook5",
"type": "main",
"index": 0
}
]
]
},
"Postgres3": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
},
"Component Update": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Postgres3",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"node": "Respond to Webhook6",
"type": "main",
"index": 0
}
]
]
},
"Postgres - Create Component": {
"main": [
[
{
"node": "Format Response",
"type": "main",
"index": 0
}
]
]
},
"Component Create Webhook": {
"main": [
[
{
"node": "Build Create SQL",
"type": "main",
"index": 0
}
]
]
},
"Build Create SQL": {
"main": [
[
{
"node": "Postgres - Create Component",
"type": "main",
"index": 0
}
]
]
},
"Format Response": {
"main": [
[
{
"node": "Respond to Webhook7",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "6a6f41ca-8673-4640-bb68-ac8b3bf20e4d",
"triggerCount": 9,
"tags": [],
"shared": [
{
"createdAt": "2025-10-12T12:32:03.665Z",
"updatedAt": "2025-10-12T12:32:03.665Z",
"role": "workflow:owner",
"workflowId": "GPIoYMXINj8bnfIw",
"projectId": "0tBJbgcFWwxEMKPn",
"project": {
"createdAt": "2025-10-05T16:55:31.619Z",
"updatedAt": "2025-10-05T16:55:58.616Z",
"id": "0tBJbgcFWwxEMKPn",
"name": "Bikash Panda <oksbwn@gmail.com>",
"type": "personal",
"icon": null,
"description": null
}
}
]
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
postgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Components. Uses postgres, readWriteFile. Event-driven trigger; 42 nodes.
Source: https://github.com/oksbwn/Inventory-Management-System/blob/7f26462a83a574049df178775808226e970cd4f8/n8n/GPIoYMXINj8bnfIw.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
My-Workflow. Uses readWriteFile, postgres. Event-driven trigger; 5 nodes.
This workflow acts as a junior finance research analyst for a UK boutique M&A or corporate finance team. It listens for Slack messages, classifies the request, gathers company or market data, and prod
Agendamiento_v2. Uses n8n-nodes-evolution-api, redis, httpRequest, executeWorkflowTrigger. Event-driven trigger; 59 nodes.
Cancelacion_v2. Uses executeWorkflowTrigger, redis, httpRequest, n8n-nodes-evolution-api. Event-driven trigger; 46 nodes.
This N8N workflow is designed to enrich seller data stored in a Postgres database by performing automated Google search lookups. It uses Bright Data's Web Unlocker to bypass search result restrictions