BE-LightningReport/Import_Wind_Turbine_Farm.json

411 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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": []
}