{ "name": "Import_Wind_Turbine_Farm", "nodes": [ { "parameters": { "content": "Upload turbine CSV through the n8n form (no server file access needed).\n\n1. Import this workflow and **Activate** it.\n2. Open **Import Wind Turbines Form** → copy the **Production URL**.\n3. Submit the form with customer_id + CSV file.\n\nCSV format (comma or semicolon):\nname,latitude,longitude,farm_name\nWTG-01,39.854321,32.765432,My Wind Farm\n\nAlso supported headers:\n- name / turbin / wtg / turbin adi\n- latitude / lat / enlem\n- longitude / lon / boylam\n\nOptional column: customer_id\n\nIf import fails, the error shows the column names n8n detected.", "height": 340, "width": 560 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -560, -160 ], "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Sticky Note" }, { "parameters": { "formTitle": "Import Wind Turbines", "formDescription": "Upload a CSV with turbine names and coordinates for one wind farm.\nRequired columns: name, latitude, longitude", "formPath": "import-wind-turbines", "responseMode": "lastNode", "authentication": "none", "formFields": { "values": [ { "fieldLabel": "customer_id", "fieldType": "text", "fieldName": "customer_id", "requiredField": true, "placeholder": "Must match customers.id in datatable" }, { "fieldLabel": "csv_file", "fieldType": "file", "fieldName": "csv_file", "requiredField": true, "acceptFileTypes": ".csv", "multipleFiles": false }, { "fieldLabel": "replace_existing", "fieldType": "dropdown", "fieldName": "replace_existing", "requiredField": true, "fieldOptions": { "values": [ { "option": "yes" }, { "option": "no" } ] } } ] }, "options": { "buttonLabel": "Import", "appendAttribution": false } }, "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.4, "position": [ 0, 0 ], "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Import Wind Turbines Form", "webhookId": "f1e2d3c4-b5a6-7890-abcd-ef1234567890" }, { "parameters": { "jsCode": "const item = $input.first();\nconst json = item.json ?? {};\n\nconst normalizeKey = (value) => String(value).toLowerCase().replace(/[\\W_]+/g, '');\n\nconst pickJson = (aliases) => {\n for (const alias of aliases) {\n if (json[alias] != null && String(json[alias]).trim() !== '') {\n return String(json[alias]).trim();\n }\n }\n const wanted = new Set(aliases.map(normalizeKey));\n for (const [key, value] of Object.entries(json)) {\n if (!wanted.has(normalizeKey(key))) continue;\n if (value != null && String(value).trim() !== '') {\n return String(value).trim();\n }\n }\n return '';\n};\n\nconst customerId = pickJson([\n 'customer_id',\n 'Customer ID',\n 'Customer_ID',\n 'customer id',\n]);\nif (!customerId) {\n const available = Object.keys(json).filter((key) => !['submittedAt', 'formMode', 'formQueryParameters'].includes(key));\n throw new Error(`customer_id is required. Form returned keys: ${available.join(', ') || '(none)'}`);\n}\n\nconst replaceRaw = pickJson([\n 'replace_existing',\n 'Replace existing turbines for this customer?',\n]) || 'no';\nconst replaceExisting = ['yes', 'true', '1'].includes(replaceRaw.toLowerCase());\n\nconst binary = item.binary ?? {};\nconst csvKey = Object.keys(binary).find((key) => {\n const norm = normalizeKey(key);\n return norm === 'csvfile' || norm.startsWith('csvfile') || norm.includes('turbinecsv') || norm.includes('csv');\n}) || (Object.keys(binary).length === 1 ? Object.keys(binary)[0] : '');\nif (!csvKey) {\n throw new Error(`Upload a CSV file. Binary keys: ${Object.keys(binary).join(', ') || '(none)'}`);\n}\n\nreturn [{\n json: {\n customer_id: customerId,\n replace_existing: replaceExisting,\n },\n binary: {\n csv_file: binary[csvKey],\n },\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 240, 0 ], "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "name": "Normalize Form Input" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 }, "conditions": [ { "id": "44444444-4444-4444-8444-444444444444", "leftValue": "={{ $json.replace_existing }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [ 480, 0 ], "id": "d4e5f6a7-b8c9-0123-def0-234567890123", "name": "Replace Existing?" }, { "parameters": { "resource": "row", "operation": "deleteRows", "dataTableId": { "__rl": true, "value": "wind_turbine_farm", "mode": "name", "cachedResultName": "wind_turbine_farm" }, "matchType": "allConditions", "filters": { "conditions": [ { "keyName": "customer_id", "condition": "eq", "keyValue": "={{ $('Normalize Form Input').first().json.customer_id }}" } ] } }, "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 720, -96 ], "id": "e5f6a7b8-c9d0-1234-ef01-345678901234", "name": "Delete Existing Turbines", "alwaysOutputData": true }, { "parameters": { "jsCode": "const normalized = $('Normalize Form Input').first();\nreturn [{\n json: normalized.json,\n binary: normalized.binary,\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 960, 0 ], "id": "f6a7b8c9-d0e1-2345-f012-456789012345", "name": "Reattach CSV Binary" }, { "parameters": { "operation": "csv", "binaryPropertyName": "csv_file", "options": { "headerRow": true } }, "type": "n8n-nodes-base.extractFromFile", "typeVersion": 1, "position": [ 1200, 0 ], "id": "a7b8c9d0-e1f2-3456-0123-567890123456", "name": "Extract From CSV" }, { "parameters": { "jsCode": "const config = $('Normalize Form Input').first().json;\nconst defaultCustomerId = String(config.customer_id ?? '').trim();\nif (!defaultCustomerId) {\n throw new Error('customer_id is required (must match customers.id).');\n}\n\nconst cleanKey = (key) => String(key).replace(/^\\uFEFF/, '').trim();\nconst normalizeKey = (key) => cleanKey(key).toLowerCase().replace(/[\\s_\\-]+/g, '');\n\nconst NAME_ALIASES = [\n 'name', 'turbine', 'turbinename', 'turbin', 'turbinadi', 'turbinadı', 'turbine_name',\n 'wtg', 'label', 'id', 'unit', 'generator', 'turbineid', 'turbinid', 'ad', 'isim',\n];\nconst LAT_ALIASES = ['latitude', 'lat', 'enlem', 'y', 'northing'];\nconst LON_ALIASES = ['longitude', 'lon', 'lng', 'boylam', 'long', 'x', 'easting'];\nconst CUSTOMER_ALIASES = ['customerid', 'customer_id', 'customer'];\nconst FARM_ALIASES = ['farmname', 'farm_name', 'farm', 'windfarm', 'wind_farm', 'santral', 'res', 'projeadi', 'projectname'];\n\nconst isFarmKey = (key) => {\n const normalized = normalizeKey(key);\n return normalized.includes('farm') || normalized.includes('santral') || FARM_ALIASES.some((alias) => normalizeKey(alias) === normalized);\n};\n\nconst parseNumber = (value) => {\n const text = String(value ?? '').trim().replace(/\\s+/g, '');\n if (!text) return NaN;\n if (text.includes(',') && !text.includes('.')) {\n return Number(text.replace(',', '.'));\n }\n return Number(text);\n};\n\nconst expandDelimitedRow = (row) => {\n const keys = Object.keys(row);\n if (keys.length !== 1) return row;\n\n const onlyKey = cleanKey(keys[0]);\n const onlyValue = row[keys[0]];\n\n if (onlyKey.includes(';') && (onlyValue == null || String(onlyValue).trim() === '')) {\n const headers = onlyKey.split(';').map(cleanKey);\n return Object.fromEntries(headers.map((header) => [header, '']));\n }\n\n const delimiter = onlyKey.includes(';') ? ';' : (onlyKey.includes('\\t') ? '\\t' : null);\n if (!delimiter) return row;\n\n const headers = onlyKey.split(delimiter).map(cleanKey);\n const values = String(onlyValue ?? '').split(delimiter).map((part) => cleanKey(part));\n if (headers.length < 2 || values.length < 2) return row;\n\n const expanded = {};\n for (let i = 0; i < headers.length; i += 1) {\n expanded[headers[i]] = values[i] ?? '';\n }\n return expanded;\n};\n\nconst buildNormalizedRow = (row) => {\n const expanded = expandDelimitedRow(row);\n const normalized = {};\n for (const [key, value] of Object.entries(expanded)) {\n normalized[normalizeKey(key)] = value;\n }\n return normalized;\n};\n\nconst pickValue = (normRow, aliases, fuzzyTokens = []) => {\n for (const alias of aliases) {\n const value = normRow[normalizeKey(alias)];\n if (value != null && String(value).trim() !== '') {\n return String(value).trim();\n }\n }\n\n for (const token of fuzzyTokens) {\n for (const [key, value] of Object.entries(normRow)) {\n if (!key.includes(token)) continue;\n if (value != null && String(value).trim() !== '') {\n return String(value).trim();\n }\n }\n }\n\n return '';\n};\n\nconst inferRow = (normRow) => {\n const entries = Object.entries(normRow)\n .filter(([key, value]) => key && value != null && String(value).trim() !== '' && !CUSTOMER_ALIASES.includes(key) && !isFarmKey(key));\n\n const numeric = [];\n const text = [];\n\n for (const [key, value] of entries) {\n const parsed = parseNumber(value);\n const raw = String(value).trim();\n if (Number.isFinite(parsed) && /^-?\\d/.test(raw)) {\n numeric.push(parsed);\n } else {\n text.push(String(value).trim());\n }\n }\n\n if (text.length >= 1 && numeric.length >= 2) {\n return {\n name: text[0],\n latitude: numeric[0],\n longitude: numeric[1],\n };\n }\n\n if (entries.length === 3) {\n const values = entries.map(([, value]) => String(value).trim());\n const nums = values.map(parseNumber);\n const allNumeric = nums.every(Number.isFinite);\n if (allNumeric) {\n return { name: `T${values[0]}`, latitude: nums[0], longitude: nums[1] };\n }\n const numericIndexes = nums.map((num, idx) => (Number.isFinite(num) ? idx : -1)).filter((idx) => idx >= 0);\n if (numericIndexes.length >= 2) {\n const nameIndex = values.findIndex((value, idx) => !Number.isFinite(nums[idx]));\n return {\n name: nameIndex >= 0 ? values[nameIndex] : values[0],\n latitude: nums[numericIndexes[0]],\n longitude: nums[numericIndexes[1]],\n };\n }\n }\n\n return null;\n};\n\nconst rows = $input.all().map((item) => item.json);\nif (!rows.length) {\n throw new Error('CSV file is empty or could not be parsed.');\n}\n\nreturn rows.map((rawRow, index) => {\n const normRow = buildNormalizedRow(rawRow);\n\n let name = pickValue(normRow, NAME_ALIASES, ['name', 'turbin', 'wtg', 'turbine', 'label', 'ad', 'isim']);\n let latRaw = pickValue(normRow, LAT_ALIASES, ['lat', 'enlem']);\n let lonRaw = pickValue(normRow, LON_ALIASES, ['lon', 'lng', 'boylam', 'long']);\n const rowCustomerId = pickValue(normRow, CUSTOMER_ALIASES, ['customer']) || defaultCustomerId;\n const farmName = pickValue(normRow, FARM_ALIASES, ['farm', 'santral', 'res']);\n\n let latitude = parseNumber(latRaw);\n let longitude = parseNumber(lonRaw);\n\n if (!name || !Number.isFinite(latitude) || !Number.isFinite(longitude)) {\n const inferred = inferRow(normRow);\n if (inferred) {\n name = name || inferred.name;\n latitude = Number.isFinite(latitude) ? latitude : inferred.latitude;\n longitude = Number.isFinite(longitude) ? longitude : inferred.longitude;\n }\n }\n\n if (!name) {\n const keys = Object.keys(rawRow).map(cleanKey);\n throw new Error(\n `Missing turbine name in CSV row ${index + 1}. Found columns: ${keys.join(', ') || '(none)'}. ` +\n 'Expected headers like name, latitude, longitude (also supports turbin, enlem, boylam).',\n );\n }\n if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {\n throw new Error(\n `Invalid coordinates for turbine \"${name}\" in CSV row ${index + 1}. ` +\n `latitude=\"${latRaw}\" longitude=\"${lonRaw}\"`,\n );\n }\n\n return {\n json: {\n customer_id: rowCustomerId,\n farm_name: farmName,\n name,\n latitude,\n longitude,\n },\n };\n});" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1440, 0 ], "id": "b8c9d0e1-f234-5678-1234-678901234567", "name": "Map Turbine Rows" }, { "parameters": { "resource": "row", "operation": "insert", "dataTableId": { "__rl": true, "value": "wind_turbine_farm", "mode": "name", "cachedResultName": "wind_turbine_farm" }, "columns": { "mappingMode": "defineBelow", "value": { "customer_id": "={{ $json.customer_id }}", "farm_name": "={{ $json.farm_name }}", "name": "={{ $json.name }}", "latitude": "={{ $json.latitude }}", "longitude": "={{ $json.longitude }}" }, "matchingColumns": [], "schema": [ { "id": "customer_id", "displayName": "customer_id", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "farm_name", "displayName": "farm_name", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "name", "displayName": "name", "required": false, "defaultMatch": false, "display": true, "type": "string", "readOnly": false, "removed": false }, { "id": "latitude", "displayName": "latitude", "required": false, "defaultMatch": false, "display": true, "type": "number", "readOnly": false, "removed": false }, { "id": "longitude", "displayName": "longitude", "required": false, "defaultMatch": false, "display": true, "type": "number", "readOnly": false, "removed": false } ], "attemptToConvertTypes": true, "convertFieldsToString": false }, "options": { "optimizeBulk": true } }, "type": "n8n-nodes-base.dataTable", "typeVersion": 1.1, "position": [ 1680, 0 ], "id": "c9d0e1f2-3456-7890-2345-789012345678", "name": "Insert Turbine Rows" }, { "parameters": { "operation": "completion", "completionTitle": "Import complete", "completionMessage": "Turbine rows were written to the wind_turbine_farm datatable.", "options": {} }, "type": "n8n-nodes-base.form", "typeVersion": 1, "position": [ 1920, 0 ], "id": "d0e1f2a3-4567-8901-2345-678901234567", "name": "Import Complete" } ], "pinData": {}, "connections": { "Import Wind Turbines Form": { "main": [ [ { "node": "Normalize Form Input", "type": "main", "index": 0 } ] ] }, "Normalize Form Input": { "main": [ [ { "node": "Replace Existing?", "type": "main", "index": 0 } ] ] }, "Replace Existing?": { "main": [ [ { "node": "Delete Existing Turbines", "type": "main", "index": 0 } ], [ { "node": "Reattach CSV Binary", "type": "main", "index": 0 } ] ] }, "Delete Existing Turbines": { "main": [ [ { "node": "Reattach CSV Binary", "type": "main", "index": 0 } ] ] }, "Reattach CSV Binary": { "main": [ [ { "node": "Extract From CSV", "type": "main", "index": 0 } ] ] }, "Extract From CSV": { "main": [ [ { "node": "Map Turbine Rows", "type": "main", "index": 0 } ] ] }, "Map Turbine Rows": { "main": [ [ { "node": "Insert Turbine Rows", "type": "main", "index": 0 } ] ] }, "Insert Turbine Rows": { "main": [ [ { "node": "Import Complete", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1", "binaryMode": "separate" }, "versionId": "00000000-0000-4000-8000-000000000002", "meta": { "templateCredsSetupCompleted": false }, "tags": [] }