{
  "createdAt": "2025-10-08T15:15:18.621Z",
  "updatedAt": "2025-10-25T08:30:15.000Z",
  "id": "iawH7WdT0BTBaHsu",
  "name": "Categories",
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "api/components/categories",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -976,
        656
      ],
      "id": "cb4f9ba0-de9e-4c8c-84f0-fad61716307f",
      "name": "api/components/categories [POST]"
    },
    {
      "parameters": {
        "jsCode": "// Get incoming data\nconst items = $input.all();\nconst body = (items[0] && items[0].json && items[0].json.body) ? items[0].json.body : {};\n\n// Extract category name\nconst categoryName = body.category_name || body.name || \"\";\n\n// Validate\nif (!categoryName.trim()) {\n  throw new Error('Category name is required');\n}\n\nreturn [\n  {\n    json: {\n      params: {\n        categoryName: categoryName.trim()\n      }\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -752,
        656
      ],
      "id": "11ff25d6-43b5-40c5-8251-4528c3032ab5",
      "name": "Parse Create Params"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO categories (category_name)\nVALUES ('{{ $json.params.categoryName }}')\nRETURNING category_id, category_name;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -528,
        656
      ],
      "id": "5b8ac70f-0fa1-4a80-b946-de2092d4a26f",
      "name": "Create Category",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const result = $input.first().json;\n\nreturn [\n  {\n    json: {\n      success: true,\n      message: 'Category created successfully',\n      data: result\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -304,
        656
      ],
      "id": "1dcfbcb7-bc6d-43e9-9528-d72a7375e89d",
      "name": "Format Response"
    },
    {
      "parameters": {
        "respondWith": "allIncomingItems",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        -80,
        656
      ],
      "id": "23144832-6425-4701-aeaa-ffff2fce3d6d",
      "name": "Respond to Webhook3"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "api/components/categories/delete",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -976,
        1104
      ],
      "id": "1862f4bc-5e71-4f4a-8a32-bb795f60ea28",
      "name": "api/components/categories/delete [POST]"
    },
    {
      "parameters": {
        "jsCode": "// Get incoming data from POST body\nconst items = $input.all();\nconst body = (items[0] && items[0].json && items[0].json.body) ? items[0].json.body : {};\n\n// Extract category ID from body\nconst categoryId = parseInt(body.category_id || body.categoryId || body.id) || 0;\n\n// Validate\nif (!categoryId) {\n  throw new Error('Category ID is required in request body');\n}\n\nreturn [\n  {\n    json: {\n      params: {\n        categoryId\n      }\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -752,
        1104
      ],
      "id": "ee084609-5a54-44c8-80b8-da619ac8ce35",
      "name": "Parse Delete Params"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "DELETE FROM categories\nWHERE category_id = {{ $json.params.categoryId }}\n  AND NOT EXISTS (\n    SELECT 1 FROM components WHERE category_id = {{ $json.params.categoryId }}\n  )\nRETURNING category_id;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -528,
        1104
      ],
      "id": "fd36110f-7d68-4a76-a922-789c1946dfbd",
      "name": "Delete Category",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "allIncomingItems",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        -80,
        1104
      ],
      "id": "40789883-5ba9-4d04-874e-c1d164ad7fff",
      "name": "Respond to Webhook"
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst result = items[0] && items[0].json ? items[0].json : null;\n\nif (result && result.category_id) {\n  return [\n    {\n      json: {\n        success: true,\n        message: 'Category deleted successfully'\n      }\n    }\n  ];\n} else {\n  return [\n    {\n      json: {\n        success: false,\n        message: 'Cannot delete category. Either it does not exist or has components assigned to it.'\n      }\n    }\n  ];\n}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -304,
        1104
      ],
      "id": "b67a8b72-2c51-4b8b-bde9-06b0e2b0fc12",
      "name": "Format Response2"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "api/components/categories/update",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -976,
        1328
      ],
      "id": "8b154df7-0959-49cd-9ecd-d7b58a574f81",
      "name": "api/components/categories/update [POST]"
    },
    {
      "parameters": {
        "jsCode": "// Get incoming data from POST body\nconst items = $input.all();\nconst body = (items[0] && items[0].json && items[0].json.body) ? items[0].json.body : {};\n\n// Extract data from body\nconst categoryId = parseInt(body.category_id || body.categoryId || body.id) || 0;\nconst categoryName = body.category_name || body.categoryName || body.name || \"\";\n\n// Validate\nif (!categoryId) {\n  throw new Error('Category ID is required in request body');\n}\nif (!categoryName.trim()) {\n  throw new Error('Category name is required');\n}\n\nreturn [\n  {\n    json: {\n      params: {\n        categoryId,\n        categoryName: categoryName.trim()\n      }\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -752,
        1328
      ],
      "id": "87575854-96a6-4a29-8d57-8a2c6b0f24da",
      "name": "Parse Update Params"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE categories\nSET category_name = '{{ $json.params.categoryName }}'\nWHERE category_id = {{ $json.params.categoryId }}\nRETURNING category_id, category_name;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -528,
        1328
      ],
      "id": "26b58d88-720d-4f4f-b402-4e5ac06879c7",
      "name": "Update Category",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const result = $input.first().json;\n\nreturn [\n  {\n    json: {\n      success: true,\n      message: 'Category updated successfully',\n      data: result\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -304,
        1328
      ],
      "id": "2af4908a-33ad-46ca-83a0-f6ec84b73dcf",
      "name": "Format Response1"
    },
    {
      "parameters": {
        "respondWith": "allIncomingItems",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        -80,
        1328
      ],
      "id": "ae411c04-e1ed-4a19-a853-f0cf2fbdec51",
      "name": "Respond to Webhook1"
    },
    {
      "parameters": {
        "path": "api/components/categories/meta",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -976,
        1552
      ],
      "id": "46e0702c-d607-417f-83ef-32d594cadc0f",
      "name": "api/components/categories/meta"
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst q = (items[0] && items[0].json && items[0].json.query) ? items[0].json.query : {};\n\nconst search = q.search || \"\";\nconst sortBy = q.sortBy || \"category_name\";\nconst limit = Number.isInteger(parseInt(q.pageSize)) ? parseInt(q.pageSize) : null;\nconst pageNo = Number.isInteger(parseInt(q.pageNo)) ? parseInt(q.pageNo) : 1;\nconst offset = limit ? (pageNo - 1) * limit : 0;\n\nreturn [{ json: { params: { search, sortBy, limit, offset } } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -752,
        1552
      ],
      "id": "1e41c783-42ad-42b4-8ee4-7212d519e3b0",
      "name": "Parse Params"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  COUNT(DISTINCT c.category_id) AS total_categories,\n  COUNT(DISTINCT CASE WHEN comp.component_id IS NOT NULL THEN c.category_id END) AS active_categories,\n  COUNT(DISTINCT CASE WHEN comp.component_id IS NULL THEN c.category_id END) AS empty_categories,\n  COALESCE(SUM(comp_count.component_count), 0) AS total_components,\n  COALESCE(SUM(new_items.new_count), 0) AS recent_additions,\n  COALESCE(MAX(latest.latest_added_date), NOW()) AS latest_component_date,\n  COALESCE(MIN(oldest.oldest_added_date), NOW()) AS oldest_component_date,\n  ROUND(AVG(cat_sizes.category_size)::numeric, 2) AS avg_category_size,\n  MAX(cat_sizes.category_size) AS largest_category_size,\n  (SELECT category_name FROM categories c2 LEFT JOIN (SELECT category_id, COUNT(*) AS cat_size FROM components GROUP BY category_id) s ON c2.category_id = s.category_id WHERE s.cat_size = (SELECT MAX(cat_size) FROM (SELECT category_id, COUNT(*) AS cat_size FROM components GROUP BY category_id) x) LIMIT 1) AS largest_category_name\nFROM categories c\nLEFT JOIN components comp ON c.category_id = comp.category_id\nLEFT JOIN (\n  SELECT category_id, COUNT(*) AS component_count\n  FROM components\n  GROUP BY category_id\n) comp_count ON comp_count.category_id = c.category_id\nLEFT JOIN (\n  SELECT category_id, COUNT(*) AS new_count\n  FROM components\n  WHERE date_added >= CURRENT_DATE - INTERVAL '30 days'\n  GROUP BY category_id\n) new_items ON new_items.category_id = c.category_id\nLEFT JOIN (\n  SELECT MAX(date_added) AS latest_added_date\n  FROM components\n) latest ON true\nLEFT JOIN (\n  SELECT MIN(date_added) AS oldest_added_date\n  FROM components\n) oldest ON true\nLEFT JOIN (\n  SELECT category_id, COUNT(*) AS category_size\n  FROM components\n  GROUP BY category_id\n) cat_sizes ON cat_sizes.category_id = c.category_id;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -528,
        1552
      ],
      "id": "c004a56b-ca3c-41a8-a4f0-2f2511c93c16",
      "name": "Get Categories Metadata",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const data = $input.item.json;\n\nreturn [\n  {\n    json: {\n      summary: {\n        totalCategories: parseInt(data.total_categories) || 0,\n        activeCategories: parseInt(data.active_categories) || 0,\n        emptyCategories: parseInt(data.empty_categories) || 0,\n        totalComponents: parseInt(data.total_components) || 0,\n        recentAdditions: parseInt(data.recent_additions) || 0\n      },\n      statistics: {\n        averageCategorySize: parseFloat(data.avg_category_size) || 0,\n        largestCategorySize: parseInt(data.largest_category_size) || 0,\n        largestCategoryName: data.largest_category_name || 'N/A',\n        categoryCompleteness: data.total_categories > 0 ? Math.round((data.active_categories / data.total_categories) * 100) : 0\n      },\n      timeline: {\n        latestComponentDate: data.latest_component_date,\n        oldestComponentDate: data.oldest_component_date\n      },\n      insights: {\n        hasEmptyCategories: parseInt(data.empty_categories) > 0,\n        emptyPercentage: data.total_categories > 0 ? Math.round((data.empty_categories / data.total_categories) * 100) : 0,\n        recentActivityRate: data.total_components > 0 ? Math.round((data.recent_additions / data.total_components) * 100) : 0,\n        status: data.active_categories === data.total_categories ? 'All categories in use' : data.empty_categories > 0 ? 'Some unused categories' : 'Optimal'\n      }\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -304,
        1552
      ],
      "id": "82a3bcd0-083b-44d0-95e7-8ef38062823e",
      "name": "Format Metadata Response"
    },
    {
      "parameters": {
        "respondWith": "allIncomingItems",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        -80,
        1552
      ],
      "id": "ed7df4b9-be86-46ac-b620-3935f28ddddc",
      "name": "Respond to Webhook4"
    },
    {
      "parameters": {
        "respondWith": "allIncomingItems",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        -304,
        880
      ],
      "id": "a8304e5b-9e4a-4faf-bd54-b8ea7cdd4953",
      "name": "Respond to Webhook2"
    },
    {
      "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\nconst search = q.search || \"\";\nconst sortBy = q.sortBy || \"category_name\";\nconst limit = Number.isInteger(parseInt(q.pageSize)) ? parseInt(q.pageSize) : null;\nconst pageNo = Number.isInteger(parseInt(q.page)) ? parseInt(q.page) : 1;\nconst offset = limit ? (pageNo - 1) * limit : 0;\n\n// Return a single item with params for the next node\nreturn [\n  {\n    json: {\n      params: {\n        search,\n        sortBy,\n        limit,\n        offset\n      }\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -752,
        880
      ],
      "id": "20c3983f-a366-4da8-9cd6-0f3de8490d2d",
      "name": "Parse Params1"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH filtered_categories AS (\n    SELECT \n        c.category_id,\n        c.category_name,\n        COUNT(comp.component_id) as component_count,\n        COUNT(CASE WHEN comp.date_added >= CURRENT_DATE - INTERVAL '30 days' THEN 1 END) as recent_additions,\n        MIN(comp.date_added) as first_component_date,\n        MAX(comp.date_added) as last_component_date\n    FROM categories c\n    LEFT JOIN components comp ON c.category_id = comp.category_id\n    WHERE \n        CASE \n            WHEN '{{ $json.params.search }}' != '' \n            THEN c.category_name ILIKE '%{{ $json.params.search }}%'\n            ELSE TRUE\n        END\n    GROUP BY c.category_id, c.category_name\n),\npaginated_data AS (\n    SELECT *\n    FROM filtered_categories\n    ORDER BY \n        CASE WHEN '{{ $json.params.sortBy }}' = 'category_name' THEN category_name END ASC,\n        CASE WHEN '{{ $json.params.sortBy }}' = 'component_count' THEN component_count END DESC,\n        category_name ASC\n    {{ $json.params.limit ? 'LIMIT ' + $json.params.limit : '' }}\n    {{ $json.params.offset ? 'OFFSET ' + $json.params.offset : '' }}\n)\nSELECT \n    pd.*,\n    (SELECT COUNT(*) FROM filtered_categories) as total_records\nFROM paginated_data pd;\n",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -528,
        880
      ],
      "id": "4ac0ca48-aa96-417c-998e-aa1c40eae8cf",
      "name": "Get Categories",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "path": "api/components/categories",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -976,
        880
      ],
      "id": "a0a5b8c6-10c4-4599-9870-02c6708df7fe",
      "name": "api/components/categories"
    }
  ],
  "connections": {
    "api/components/categories [POST]": {
      "main": [
        [
          {
            "node": "Parse Create Params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Create Params": {
      "main": [
        [
          {
            "node": "Create Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Category": {
      "main": [
        [
          {
            "node": "Format Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "api/components/categories/delete [POST]": {
      "main": [
        [
          {
            "node": "Parse Delete Params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Delete Params": {
      "main": [
        [
          {
            "node": "Delete Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete Category": {
      "main": [
        [
          {
            "node": "Format Response2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Response2": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "api/components/categories/update [POST]": {
      "main": [
        [
          {
            "node": "Parse Update Params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Update Params": {
      "main": [
        [
          {
            "node": "Update Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Category": {
      "main": [
        [
          {
            "node": "Format Response1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Response1": {
      "main": [
        [
          {
            "node": "Respond to Webhook1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "api/components/categories/meta": {
      "main": [
        [
          {
            "node": "Parse Params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Params": {
      "main": [
        [
          {
            "node": "Get Categories Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Categories Metadata": {
      "main": [
        [
          {
            "node": "Format Metadata Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Metadata Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Params1": {
      "main": [
        [
          {
            "node": "Get Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Categories": {
      "main": [
        [
          {
            "node": "Respond to Webhook2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "api/components/categories": {
      "main": [
        [
          {
            "node": "Parse Params1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "d63120e5-4949-4640-b94b-73748ec709ad",
  "triggerCount": 5,
  "tags": [],
  "shared": [
    {
      "createdAt": "2025-10-08T15:15:18.632Z",
      "updatedAt": "2025-10-08T15:15:18.632Z",
      "role": "workflow:owner",
      "workflowId": "iawH7WdT0BTBaHsu",
      "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
      }
    }
  ]
}