411 lines
18 KiB
JSON
411 lines
18 KiB
JSON
{
|
||
"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": []
|
||
}
|