From 543a9642351b68c948e9910d9abba1c3d5ca43cb Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 23 May 2025 16:08:06 +0200 Subject: [PATCH] W n8n --- .DS_Store | Bin 10244 -> 10244 bytes My_workflow_43.json => My_workflow_49.json | 203 +++++++++++++-------- app/api/missions/route.ts | 85 +++++++-- 3 files changed, 199 insertions(+), 89 deletions(-) rename My_workflow_43.json => My_workflow_49.json (72%) diff --git a/.DS_Store b/.DS_Store index 4d6a0a18363cfa05c47ae01f9366efbe190988ac..ee7697b818aae33b24e7c5480d864d8ba8511c99 100644 GIT binary patch delta 66 zcmV-I0KNZ&P=rvBPXQ;fP`eKSCzA{iQa;M1& delta 315 zcmZn(XbG6$F8U^hRb&SV~eU_nuaWQH7uRE9)`JRq6E5I^~gkOmvWdIkojbCW{^ zYNdr3oEeJ13QB;aFGD6nF%V`lV#sDl1Dcr+ zG8i%FF(d)a&ju=&WR(RQ>>;em43wE{AR)s9v@&d OGWovnGOC!z$OHfwmPI50 diff --git a/My_workflow_43.json b/My_workflow_49.json similarity index 72% rename from My_workflow_43.json rename to My_workflow_49.json index 68edce4b..3e848aaf 100644 --- a/My_workflow_43.json +++ b/My_workflow_49.json @@ -1,5 +1,5 @@ { - "name": "My workflow 43", + "name": "My workflow 49", "nodes": [ { "parameters": { @@ -12,7 +12,7 @@ -1040, 600 ], - "id": "ba97cc36-b427-42e2-b9a6-9e83779631d9" + "id": "31981488-6ec3-458d-bd38-a83aab699bb5" }, { "parameters": { @@ -25,11 +25,11 @@ -840, 480 ], - "id": "6d79fb81-50e1-4b32-a91b-87e7d465ee1d" + "id": "b844080e-a6e9-423b-9556-22af43234952" }, { "parameters": { - "functionCode": "// Process attachments for S3 upload with improved error handling\nconst input = $input.item.json;\nconst attachments = input?.missionProcessed?.attachments || [];\n\n// Create a default output with a flag indicating if there are attachments\nconst defaultOutput = {\n json: {\n ...input,\n hasAttachments: false,\n attachmentUrls: [] // Initialize empty array for consistency\n }\n};\n\n// If no attachments or invalid input, return default output and skip S3 node\nif (!input || !Array.isArray(attachments) || attachments.length === 0) {\n console.log('No attachments found or invalid input structure');\n return defaultOutput;\n}\n\n// Process the first attachment to check if it's valid\nlet hasValidAttachments = false;\nlet validAttachments = [];\n\n// Check all attachments for validity\ntry {\n for (const attachment of attachments) {\n if (attachment && attachment.data) {\n // Extract pure base64 (remove data:image/... prefix if present)\n let base64Data = attachment.data;\n if (typeof base64Data === 'string' && base64Data.includes(',')) {\n base64Data = base64Data.split(',')[1];\n }\n \n // If we have valid data, mark as valid and include in valid attachments\n if (base64Data && base64Data.trim() !== '') {\n try {\n // Test if it's valid base64 by creating a buffer\n const testBuffer = Buffer.from(base64Data, 'base64');\n if (testBuffer.length > 0) {\n hasValidAttachments = true;\n validAttachments.push({\n data: base64Data,\n name: attachment.name || `attachment-${validAttachments.length}.png`,\n type: attachment.type || 'application/octet-stream'\n });\n }\n } catch (e) {\n console.log(`Skipping invalid attachment: ${e.message}`);\n // Skip this attachment but continue processing others\n }\n }\n }\n }\n} catch (error) {\n // If any error in the loop, log it but don't fail the workflow\n console.error('Error checking attachment validity:', error);\n}\n\n// If no valid attachments after checking, return the default output\nif (!hasValidAttachments || validAttachments.length === 0) {\n console.log('No valid attachments found after validation');\n return defaultOutput;\n}\n\n// At this point, we know we have at least one valid attachment\n// Prepare the output with attachment info\nreturn {\n json: {\n ...input,\n hasAttachments: true,\n attachmentCount: validAttachments.length,\n attachmentData: validAttachments,\n originalAttachmentsCount: attachments.length\n }\n};" + "functionCode": "// Process attachments for S3 upload with improved error handling\nconst input = $input.item.json;\nconst attachments = input?.missionProcessed?.attachments || [];\n\n// Create a default output with a flag indicating if there are attachments\nconst defaultOutput = {\n json: {\n ...input,\n hasAttachments: false,\n attachmentUrls: [], // Initialize empty array for consistency\n skipIntegrations: false // Add flag to control integration flow\n }\n};\n\n// If no attachments or invalid input, return default output and skip S3 node\nif (!input || !Array.isArray(attachments) || attachments.length === 0) {\n console.log('No attachments found or invalid input structure');\n return defaultOutput;\n}\n\n// Process the first attachment to check if it's valid\nlet hasValidAttachments = false;\nlet validAttachments = [];\n\n// Check all attachments for validity\ntry {\n for (const attachment of attachments) {\n if (attachment && attachment.data) {\n // Extract pure base64 (remove data:image/... prefix if present)\n let base64Data = attachment.data;\n if (typeof base64Data === 'string' && base64Data.includes(',')) {\n base64Data = base64Data.split(',')[1];\n }\n \n // If we have valid data, mark as valid and include in valid attachments\n if (base64Data && base64Data.trim() !== '') {\n try {\n // Test if it's valid base64 by creating a buffer\n const testBuffer = Buffer.from(base64Data, 'base64');\n if (testBuffer.length > 0) {\n hasValidAttachments = true;\n validAttachments.push({\n data: base64Data,\n name: attachment.name || `attachment-${validAttachments.length}.png`,\n type: attachment.type || 'application/octet-stream'\n });\n }\n } catch (e) {\n console.log(`Skipping invalid attachment: ${e.message}`);\n // Skip this attachment but continue processing others\n }\n }\n }\n }\n} catch (error) {\n // If any error in the loop, log it but don't fail the workflow\n console.error('Error checking attachment validity:', error);\n}\n\n// If no valid attachments after checking, return the default output\nif (!hasValidAttachments || validAttachments.length === 0) {\n console.log('No valid attachments found after validation');\n return defaultOutput;\n}\n\n// At this point, we know we have at least one valid attachment\n// Prepare the output with attachment info\nreturn {\n json: {\n ...input,\n hasAttachments: true,\n attachmentCount: validAttachments.length,\n attachmentData: validAttachments,\n originalAttachmentsCount: attachments.length,\n skipIntegrations: false // Ensure integrations run\n }\n};" }, "name": "Check Attachments", "type": "n8n-nodes-base.function", @@ -38,7 +38,7 @@ -840, 740 ], - "id": "3ca930b1-7832-447f-8d62-2198dfca43b9" + "id": "44c8a7fd-01f6-460b-8d95-7cdb658b90f5" }, { "parameters": { @@ -58,7 +58,7 @@ -620, 740 ], - "id": "a44f291e-d706-4808-92b9-56305c780195" + "id": "4320ba6c-7901-4a66-b7f8-1e58be057508" }, { "parameters": { @@ -71,7 +71,7 @@ -440, 680 ], - "id": "432fa25f-99f9-4902-8d72-08f99e587c24" + "id": "e8eb3c2e-b506-48e0-9d76-f77bf599cb65" }, { "parameters": { @@ -82,7 +82,7 @@ "acl": "public-read" } }, - "id": "3f611b84-ab2c-4263-963c-afd919f09ab4", + "id": "e06d90e2-09fc-4512-9467-03c617b7fef2", "name": "S3 Upload Logo", "type": "n8n-nodes-base.s3", "typeVersion": 1, @@ -109,11 +109,11 @@ -720, 480 ], - "id": "442e09d1-ec4b-412e-b986-d6f1752f424c" + "id": "76f45b82-130a-45f1-98b8-eb75013e4f67" }, { "parameters": { - "functionCode": "// Generate empty attachment result when there are no attachments\nconst input = $input.item.json;\n\n// Return input with empty attachment urls\nreturn {\n json: {\n ...input,\n attachmentUrls: [],\n noAttachments: true\n }\n};" + "functionCode": "// Generate empty attachment result when there are no attachments\nconst input = $input.item.json;\n\n// Return input with empty attachment urls and skip integrations flag\nreturn {\n json: {\n ...input,\n attachmentUrls: [],\n noAttachments: true,\n skipIntegrations: true // Skip integrations for this path\n }\n};" }, "name": "Empty Attachment Result", "type": "n8n-nodes-base.function", @@ -122,11 +122,11 @@ -320, 880 ], - "id": "ad5b814d-1e9c-45d3-9efb-ebdc0de6f668" + "id": "ea6c4d9d-e936-4f81-82c8-61a6cc50eb7d" }, { "parameters": { - "functionCode": "// Combine file upload results with improved error handling\nconst input = $input.item.json;\n\n// Debug log\nconsole.log('Process Upload Results received input');\n\n// Defensive access to logo result\nlet logoResult;\ntry {\n logoResult = $node['S3 Upload Logo']?.json || {};\n console.log('Logo upload result accessed successfully');\n} catch (e) { \n console.error('Error accessing logo upload result:', e);\n logoResult = {};\n}\n\n// Check if we have a valid logo URL\nlet logoUrl = '';\nif (logoResult && logoResult.Location) {\n logoUrl = logoResult.Location;\n console.log('Found logo URL:', logoUrl);\n} else if (logoResult && logoResult.error) {\n console.error('Logo upload error:', logoResult.error);\n} else {\n console.log('No logo URL found in upload result');\n}\n\n// Initialize empty attachment URLs array\nlet attachmentUrls = [];\n\n// Handle attachments based on whether they exist\nif (input.hasAttachments === true) {\n try {\n // Try to get results from S3 Upload Attachments node\n const attachmentsResults = $node['S3 Upload Attachments']?.json;\n console.log('Attachment results accessed successfully');\n \n if (attachmentsResults) {\n if (Array.isArray(attachmentsResults)) {\n // Filter out any undefined or null values\n attachmentUrls = attachmentsResults\n .filter(result => result && result.Location)\n .map(result => result.Location);\n console.log(`Processed ${attachmentUrls.length} attachment URLs from array`);\n } else if (attachmentsResults.Location) {\n // Single attachment case\n attachmentUrls = [attachmentsResults.Location];\n console.log('Processed single attachment URL');\n }\n } else {\n console.log('No attachment results found');\n }\n } catch (error) {\n console.error('Error processing attachment results:', error.message);\n }\n} else {\n // If no attachments, use empty array\n console.log('No attachments to process');\n attachmentUrls = input.attachmentUrls || [];\n}\n\n// Create result object with necessary data for downstream nodes\nconst result = {\n ...input,\n logoUrl: logoUrl,\n attachmentUrls: attachmentUrls,\n publicUrl: logoUrl, // For backward compatibility\n logoProcessed: !!logoUrl,\n attachmentsProcessed: attachmentUrls.length > 0,\n processingTimestamp: new Date().toISOString()\n};\n\n// Ensure critical properties exist\nif (!result.missionProcessed) {\n console.log('Restoring missionProcessed from input');\n result.missionProcessed = input.missionProcessed || {};\n}\n\nif (!result.missionOriginal) {\n console.log('Restoring missionOriginal from input');\n result.missionOriginal = input.missionOriginal || {};\n}\n\nif (!result.config) {\n console.log('Restoring config from input');\n result.config = input.config || {};\n}\n\nconsole.log('Process Upload Results completed');\nreturn { json: result };" + "functionCode": "// Process upload results and prepare for integrations\nconst input = $input.item.json;\n\n// Log the input for debugging\nconsole.log('Process Upload Results - Input:', {\n hasInput: !!input,\n hasAttachments: input?.hasAttachments,\n logoUrl: input?.logoUrl,\n attachmentUrls: input?.attachmentUrls,\n skipIntegrations: input?.skipIntegrations\n});\n\n// Determine if we should run integrations\nconst shouldRunIntegrations = !input?.skipIntegrations;\n\n// Create the output with all necessary data\nconst output = {\n ...input,\n logoUrl: input?.logoUrl || '',\n attachmentUrls: Array.isArray(input?.attachmentUrls) ? input.attachmentUrls : [],\n hasAttachments: !!input?.hasAttachments,\n skipIntegrations: !shouldRunIntegrations\n};\n\n// Log the output for debugging\nconsole.log('Process Upload Results - Output:', {\n hasLogoUrl: !!output.logoUrl,\n attachmentUrlsCount: output.attachmentUrls.length,\n hasAttachments: output.hasAttachments,\n skipIntegrations: output.skipIntegrations\n});\n\nreturn { json: output };" }, "name": "Process Upload Results", "type": "n8n-nodes-base.function", @@ -135,7 +135,7 @@ -60, 780 ], - "id": "d745e817-7622-4611-a1a3-fbae64631c63" + "id": "8696d145-82db-43b2-8119-66ee438066d4" }, { "parameters": { @@ -184,7 +184,7 @@ -80, 520 ], - "id": "d2466678-96b3-43f3-aa65-d62038e55610", + "id": "eb6f6a28-865e-437f-8543-9af850af5ddf", "continueOnFail": true }, { @@ -198,7 +198,7 @@ 160, 680 ], - "id": "231e02a3-6996-4f47-8cf7-775129063e12", + "id": "08e00de9-c887-428d-b15a-b04d7c27bb32", "continueOnFail": true }, { @@ -212,14 +212,14 @@ 160, 880 ], - "id": "fc9d51f8-5aba-4ce0-b101-b1857ae4d06b" + "id": "0669c514-4f7e-44ad-a1a3-a6b9518b3b71" }, { "parameters": { "conditions": { "string": [ { - "value1": "={{ Array.isArray($node['Process Mission Data'].json.missionProcessed.services) ? $node['Process Mission Data'].json.missionProcessed.services.includes('Gite') || $node['Process Mission Data'].json.missionProcessed.services.includes('Calcul') : false }}", + "value1": "={{ Array.isArray($node['Process Mission Data'].json.missionProcessed.services) ? $node['Process Mission Data'].json.missionProcessed.services.includes('Gite') : false }}", "value2": "true" } ] @@ -232,7 +232,7 @@ 140, 500 ], - "id": "0108856a-5588-46b3-b481-8bab59779093" + "id": "320f5719-682b-4926-ba20-f7ebc0b15967" }, { "parameters": { @@ -288,7 +288,7 @@ 460, 460 ], - "id": "4e280998-ccf8-41b2-9521-06797731cee4", + "id": "6b7f99e2-8dcc-48b0-8ef5-dcaaa3acf1ad", "continueOnFail": true }, { @@ -346,7 +346,7 @@ 460, 620 ], - "id": "b45a420d-303b-4ed9-b553-0dd0b1e90c37", + "id": "c2530d9b-4733-49de-bead-c4d3af09916d", "continueOnFail": true }, { @@ -412,7 +412,7 @@ 480, 940 ], - "id": "e8dc9496-19e9-4e1a-8c0a-d01ef174f748", + "id": "77a70da5-dc7f-4c48-9836-6d1d2064be1b", "continueOnFail": true }, { @@ -474,12 +474,12 @@ 460, 760 ], - "id": "35887b57-e5df-4780-82e7-79cf8a80f1a2", + "id": "0bbfdeaa-fc6e-4f00-b85e-fe994bef1bc9", "continueOnFail": true }, { "parameters": { - "functionCode": "// Combine results from all integrations with better error handling\ntry {\n // Defensively get results from each service node\n let gitRepoResult = {};\n let leantimeResult = {};\n let docCollectionResult = {};\n let rocketChatResult = {};\n let uploadResults = {};\n let keycloakToken = {};\n \n try { \n gitRepoResult = $node['Create Git Repository']?.json || {};\n console.log('Git repo node executed successfully'); \n } catch (e) { \n console.log('Git repo node not executed yet, continuing anyway'); \n }\n \n try { \n leantimeResult = $node['Create Leantime Project']?.json || {};\n console.log('Leantime node executed successfully');\n } catch (e) { \n console.log('Leantime node not executed yet, continuing anyway'); \n }\n \n try { \n docCollectionResult = $node['Create Documentation Collection']?.json || {};\n console.log('Documentation node executed successfully');\n } catch (e) { \n console.log('Documentation node not executed yet, continuing anyway'); \n }\n \n try { \n rocketChatResult = $node['Create RocketChat Channel']?.json || {};\n console.log('RocketChat node executed successfully');\n } catch (e) { \n console.log('RocketChat node not executed yet, continuing anyway'); \n }\n \n try { \n uploadResults = $node['Process Upload Results']?.json || {};\n console.log('Upload Results available');\n } catch (e) { \n console.log('Upload Results not available, continuing anyway'); \n }\n \n try { \n keycloakToken = $node['Process Token']?.json || {};\n console.log('Keycloak token available');\n } catch (e) { \n console.log('Keycloak token not available, continuing anyway'); \n }\n \n // Gather information about what executed\n const executedNodes = [];\n if (Object.keys(gitRepoResult).length > 0) executedNodes.push('Git');\n if (Object.keys(leantimeResult).length > 0) executedNodes.push('Leantime');\n if (Object.keys(docCollectionResult).length > 0) executedNodes.push('Documentation');\n if (Object.keys(rocketChatResult).length > 0) executedNodes.push('RocketChat');\n \n console.log(`Executed nodes (${executedNodes.length}): ${executedNodes.join(', ')}`);\n \n // Handle empty results with empty objects to prevent errors\n const results = {\n gitRepo: gitRepoResult.error ? { error: gitRepoResult.error.message || 'Git repository creation failed' } : (gitRepoResult.body || gitRepoResult || {}),\n leantimeProject: leantimeResult.error ? { error: leantimeResult.error.message || 'Leantime project creation failed' } : (leantimeResult.body || leantimeResult || {}),\n docCollection: docCollectionResult.error ? { error: docCollectionResult.error.message || 'Documentation collection creation failed' } : (docCollectionResult.body || docCollectionResult || {}),\n rocketChatChannel: rocketChatResult.error ? { error: rocketChatResult.error.message || 'RocketChat channel creation failed' } : (rocketChatResult.body || rocketChatResult || {}),\n uploadResults: uploadResults || {},\n keycloakToken: keycloakToken || {},\n executedNodes: executedNodes\n };\n \n // Log key details for debugging\n console.log('Git repo HTML URL:', results.gitRepo?.html_url || 'not available');\n console.log('Leantime project ID:', results.leantimeProject?.result?.id || 'not available');\n console.log('Documentation ID:', results.docCollection?.id || 'not available');\n console.log('RocketChat channel ID:', results.rocketChatChannel?.channel?._id || 'not available');\n \n return results;\n} catch (error) {\n console.error('Error in Combine Results:', error);\n // Return minimal object to allow workflow to continue\n return {\n error: `Error combining results: ${error.message}`,\n gitRepo: {},\n leantimeProject: {},\n docCollection: {},\n rocketChatChannel: {}\n };\n}" + "functionCode": "// Combine results from all integrations with better error handling\ntry {\n // Defensively get results from each service node\n let gitRepoResult = {};\n let leantimeResult = {};\n let docCollectionResult = {};\n let rocketChatResult = {};\n let uploadResults = {};\n let keycloakToken = {};\n \n try { \n gitRepoResult = $node['Create Git Repository']?.json || {};\n console.log('Git repo node executed successfully'); \n } catch (e) { \n console.log('Git repo node not executed yet, continuing anyway'); \n }\n \n try { \n leantimeResult = $node['Create Leantime Project']?.json || {};\n console.log('Leantime node executed successfully');\n } catch (e) { \n console.log('Leantime node not executed yet, continuing anyway'); \n }\n \n try { \n docCollectionResult = $node['Create Documentation Collection']?.json || {};\n console.log('Documentation node executed successfully');\n } catch (e) { \n console.log('Documentation node not executed yet, continuing anyway'); \n }\n \n try { \n rocketChatResult = $node['Create RocketChat Channel']?.json || {};\n console.log('RocketChat node executed successfully');\n } catch (e) { \n console.log('RocketChat node not executed yet, continuing anyway'); \n }\n \n try { \n uploadResults = $node['Process Upload Results']?.json || {};\n console.log('Upload Results available');\n } catch (e) { \n console.log('Upload Results not available, continuing anyway'); \n }\n \n try { \n keycloakToken = $node['Process Token']?.json || {};\n console.log('Keycloak token available');\n } catch (e) { \n console.log('Keycloak token not available, continuing anyway'); \n }\n \n // Track which resources were actually created vs already existed\n const resourceStatus = {\n gitRepo: false,\n leantimeProject: false,\n docCollection: false,\n rocketChatChannel: false\n };\n \n // Process Git repository result\n if (gitRepoResult.error?.includes('already exists')) {\n console.log('Git repository already exists');\n gitRepoResult = { exists: true };\n } else if (gitRepoResult.body?.html_url) {\n resourceStatus.gitRepo = true;\n }\n \n // Process Leantime project result\n if (leantimeResult.error?.includes('already exists')) {\n console.log('Leantime project already exists');\n leantimeResult = { exists: true };\n } else if (leantimeResult.body?.result?.id) {\n resourceStatus.leantimeProject = true;\n }\n \n // Process Documentation collection result\n if (docCollectionResult.error?.includes('already exists')) {\n console.log('Documentation collection already exists');\n docCollectionResult = { exists: true };\n } else if (docCollectionResult.body?.id) {\n resourceStatus.docCollection = true;\n }\n \n // Process RocketChat channel result\n if (rocketChatResult.error?.includes('error-duplicate-channel-name')) {\n console.log('RocketChat channel already exists');\n rocketChatResult = { exists: true };\n } else if (rocketChatResult.body?.channel?._id) {\n resourceStatus.rocketChatChannel = true;\n }\n \n // Gather information about what executed\n const executedNodes = [];\n if (Object.keys(gitRepoResult).length > 0) executedNodes.push('Git');\n if (Object.keys(leantimeResult).length > 0) executedNodes.push('Leantime');\n if (Object.keys(docCollectionResult).length > 0) executedNodes.push('Documentation');\n if (Object.keys(rocketChatResult).length > 0) executedNodes.push('RocketChat');\n \n console.log(`Executed nodes (${executedNodes.length}): ${executedNodes.join(', ')}`);\n \n // Handle empty results with empty objects to prevent errors\n const results = {\n gitRepo: gitRepoResult.error ? { error: gitRepoResult.error.message || 'Git repository creation failed' } : (gitRepoResult.body || gitRepoResult || {}),\n leantimeProject: leantimeResult.error ? { error: leantimeResult.error.message || 'Leantime project creation failed' } : (leantimeResult.body || leantimeResult || {}),\n docCollection: docCollectionResult.error ? { error: docCollectionResult.error.message || 'Documentation collection creation failed' } : (docCollectionResult.body || docCollectionResult || {}),\n rocketChatChannel: rocketChatResult.error ? { error: rocketChatResult.error.message || 'RocketChat channel creation failed' } : (rocketChatResult.body || rocketChatResult || {}),\n uploadResults: uploadResults || {},\n keycloakToken: keycloakToken || {},\n executedNodes: executedNodes,\n resourceStatus: resourceStatus\n };\n \n // Log key details for debugging\n console.log('Git repo HTML URL:', results.gitRepo?.html_url || 'not available');\n console.log('Leantime project ID:', results.leantimeProject?.result?.id || 'not available');\n console.log('Documentation ID:', results.docCollection?.id || 'not available');\n console.log('RocketChat channel ID:', results.rocketChatChannel?.channel?._id || 'not available');\n \n return results;\n} catch (error) {\n console.error('Error in Combine Results:', error);\n // Return minimal object to allow workflow to continue\n return {\n error: `Error combining results: ${error.message}`,\n gitRepo: {},\n leantimeProject: {},\n docCollection: {},\n rocketChatChannel: {}\n };\n}" }, "name": "Combine Results", "type": "n8n-nodes-base.function", @@ -488,13 +488,13 @@ 660, 600 ], - "id": "5ced9249-1a7c-4070-9838-2f61d8686fa7", + "id": "dbccf675-ef1a-4b2f-babc-7e59b9e6a216", "continueOnFail": true }, { "parameters": { "method": "POST", - "url": "={{ $node['Process Mission Data'].json.config.MISSION_API_URL + '/api/missions' }}", + "url": "={{ $node['Process Mission Data'].json.config.MISSION_API_URL + '/mission-created' }}", "sendHeaders": true, "headerParameters": { "parameters": [ @@ -585,11 +585,11 @@ 820, 600 ], - "id": "59d18f68-07f8-4d44-a1da-df756d1b10ae" + "id": "5b4557b7-3559-4ce4-80a2-179bc3180ac8" }, { "parameters": { - "jsCode": "// Defensive access to all nodes\nconst missionData = $node['Process Mission Data']?.json?.missionProcessed || {};\nconst integrationResults = $node['Combine Results']?.json || {};\nconst saveMissionResult = $node['Save Mission To API']?.json || {};\nconst errors = [];\nif (saveMissionResult.error) errors.push(`Failed to save mission: ${saveMissionResult.error.message || 'Unknown error'}`);\nif (!integrationResults.gitRepo?.html_url) errors.push('Git repository creation failed');\nif (!integrationResults.leantimeProject?.result?.id) errors.push('Leantime project creation failed');\nif (!integrationResults.rocketChatChannel?.channel?._id) errors.push('RocketChat channel creation failed');\nif (!integrationResults.docCollection?.id) errors.push('Documentation collection creation failed');\nconst output = {\n success: errors.length === 0,\n error: errors.length > 0 ? errors.join('; ') : null,\n errors: errors,\n missionData,\n integrationResults,\n saveMissionResult,\n message: errors.length === 0 ? 'Mission integration complete: All systems updated successfully' : `Mission integration failed: ${errors.join('; ')}`\n};\nreturn output;" + "jsCode": "// Defensive access to all nodes\nconst missionData = $node['Process Mission Data']?.json?.missionProcessed || {};\nconst integrationResults = $node['Combine Results']?.json || {};\nconst saveMissionResult = $node['Save Mission To API']?.json || {};\nconst errors = [];\nconst warnings = [];\n\n// Check for actual errors vs expected failures\nif (saveMissionResult.error) {\n errors.push(`Failed to save mission: ${saveMissionResult.error.message || 'Unknown error'}`);\n}\n\n// Track which resources were actually created vs already existed\nconst resourceStatus = {\n gitRepo: false,\n leantimeProject: false,\n docCollection: false,\n rocketChatChannel: false\n};\n\n// Check Git repository\nif (integrationResults.gitRepo?.html_url) {\n resourceStatus.gitRepo = true;\n console.log('Git repository created successfully');\n} else if (integrationResults.gitRepo?.error?.includes('already exists')) {\n console.log('Git repository already exists, this is expected');\n warnings.push('Git repository already exists');\n} else if (integrationResults.gitRepo?.error) {\n errors.push(`Git repository creation failed: ${integrationResults.gitRepo.error}`);\n} else {\n errors.push('Git repository creation failed: Unknown error');\n}\n\n// Check Leantime project\nif (integrationResults.leantimeProject?.result?.id) {\n resourceStatus.leantimeProject = true;\n console.log('Leantime project created successfully');\n} else if (integrationResults.leantimeProject?.error?.includes('already exists')) {\n console.log('Leantime project already exists, this is expected');\n warnings.push('Leantime project already exists');\n} else if (integrationResults.leantimeProject?.error) {\n errors.push(`Leantime project creation failed: ${integrationResults.leantimeProject.error}`);\n} else {\n errors.push('Leantime project creation failed: Unknown error');\n}\n\n// Check RocketChat channel\nif (integrationResults.rocketChatChannel?.channel?._id) {\n resourceStatus.rocketChatChannel = true;\n console.log('RocketChat channel created successfully');\n} else if (integrationResults.rocketChatChannel?.error?.includes('error-duplicate-channel-name')) {\n console.log('RocketChat channel already exists, this is expected');\n warnings.push('RocketChat channel already exists');\n} else if (integrationResults.rocketChatChannel?.error) {\n errors.push(`RocketChat channel creation failed: ${integrationResults.rocketChatChannel.error}`);\n} else {\n errors.push('RocketChat channel creation failed: Unknown error');\n}\n\n// Check Documentation collection\nif (integrationResults.docCollection?.id) {\n resourceStatus.docCollection = true;\n console.log('Documentation collection created successfully');\n} else if (integrationResults.docCollection?.error?.includes('already exists')) {\n console.log('Documentation collection already exists, this is expected');\n warnings.push('Documentation collection already exists');\n} else if (integrationResults.docCollection?.error) {\n errors.push(`Documentation collection creation failed: ${integrationResults.docCollection.error}`);\n} else {\n errors.push('Documentation collection creation failed: Unknown error');\n}\n\n// Check if any critical resources failed to create\nconst criticalFailures = errors.filter(error => \n !error.includes('already exists') && \n !error.includes('expected')\n);\n\n// If the mission was successfully saved, consider it a success even if some resources already exist\nconst success = saveMissionResult.body?.message === 'Mission updated successfully' || \n saveMissionResult.body?.message === 'Mission created successfully';\n\n// Determine the final status\nconst status = criticalFailures.length > 0 ? 'error' : \n warnings.length > 0 ? 'warning' : \n 'success';\n\nconst output = {\n success: success && criticalFailures.length === 0,\n status,\n error: errors.length > 0 ? errors.join('; ') : null,\n errors,\n warnings,\n missionData,\n integrationResults,\n saveMissionResult,\n resourceStatus,\n message: status === 'success' ? \n 'Mission integration complete: All systems updated successfully' : \n status === 'warning' ? \n `Mission integration complete with warnings: ${warnings.join('; ')}` : \n `Mission integration failed: ${errors.join('; ')}`\n};\n\n// Log the final status\nconsole.log('Process Results - Final Status:', {\n success: output.success,\n status: output.status,\n errors: output.errors.length,\n warnings: output.warnings.length,\n resourceStatus\n});\n\nreturn output;" }, "name": "Process Results", "type": "n8n-nodes-base.code", @@ -598,7 +598,7 @@ 1000, 600 ], - "id": "0d656acc-1f5f-4180-9d35-64dc3c346276" + "id": "88c3f621-9d42-45d0-8231-8f7ac0186f0f" }, { "parameters": { @@ -613,7 +613,7 @@ 1220, 600 ], - "id": "61c53717-1999-4ef3-ba6b-687b71e9a582" + "id": "446ed359-ebc3-4954-b115-9c5dca73747c" }, { "parameters": { @@ -631,7 +631,7 @@ -280, 680 ], - "id": "f5ee87d0-2dc6-49d8-8748-0cbaba48242f", + "id": "8c233d82-9495-45de-9ffc-aed5faa703b8", "credentials": { "s3": { "id": "xvSkHfsTBJxzopIj", @@ -656,7 +656,40 @@ 600 ], "webhookId": "mission-created", - "id": "69b88a00-f5fb-4ef5-afbf-dcd04576509e" + "id": "ded0bff4-be16-4e88-991b-47e85120de29" + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.skipIntegrations }}", + "value2": false + } + ] + } + }, + "name": "IF Run Integrations", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 140, + 500 + ], + "id": "320f5719-682b-4926-ba20-f7ebc0b15967" + }, + { + "parameters": { + "functionCode": "// Merge paths after Process Upload Results\nconst input = $input.item.json;\n\n// Log the input for debugging\nconsole.log('Merge Paths - Input:', {\n hasInput: !!input,\n hasAttachments: input?.hasAttachments,\n skipIntegrations: input?.skipIntegrations,\n logoUrl: input?.logoUrl,\n attachmentUrls: input?.attachmentUrls,\n hasMissionProcessed: !!input?.missionProcessed,\n hasConfig: !!input?.config\n});\n\n// Get mission data from Process Mission Data node\nconst missionData = $node['Process Mission Data']?.json || {};\n\n// Ensure we have all necessary data\nconst output = {\n ...input,\n // Ensure these fields exist even if they weren't in the input\n logoUrl: input?.logoUrl || '',\n attachmentUrls: Array.isArray(input?.attachmentUrls) ? input.attachmentUrls : [],\n hasAttachments: !!input?.hasAttachments,\n skipIntegrations: !!input?.skipIntegrations,\n // Add mission data from Process Mission Data node\n missionProcessed: {\n ...missionData.missionProcessed,\n name: missionData.missionProcessed?.name || input?.missionProcessed?.name || 'Unnamed Mission',\n sanitizedName: missionData.missionProcessed?.sanitizedName || input?.missionProcessed?.sanitizedName || 'unnamed-mission',\n intention: missionData.missionProcessed?.intention || input?.missionProcessed?.intention || '',\n description: missionData.missionProcessed?.description || input?.missionProcessed?.description || 'Mission documentation',\n startDate: missionData.missionProcessed?.startDate || input?.missionProcessed?.startDate || new Date().toISOString().split('T')[0],\n endDate: missionData.missionProcessed?.endDate || input?.missionProcessed?.endDate || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n missionType: missionData.missionProcessed?.missionType || input?.missionProcessed?.missionType || 'default',\n guardians: missionData.missionProcessed?.guardians || input?.missionProcessed?.guardians || {},\n volunteers: Array.isArray(missionData.missionProcessed?.volunteers) ? missionData.missionProcessed.volunteers : (Array.isArray(input?.missionProcessed?.volunteers) ? input.missionProcessed.volunteers : []),\n profils: Array.isArray(missionData.missionProcessed?.profils) ? missionData.missionProcessed.profils : (Array.isArray(input?.missionProcessed?.profils) ? input.missionProcessed.profils : []),\n services: Array.isArray(missionData.missionProcessed?.services) ? missionData.missionProcessed.services : (Array.isArray(input?.missionProcessed?.services) ? input.missionProcessed.services : []),\n clientId: missionData.missionProcessed?.clientId || input?.missionProcessed?.clientId || 2,\n rocketChatUsernames: Array.isArray(missionData.missionProcessed?.rocketChatUsernames) ? missionData.missionProcessed.rocketChatUsernames : (Array.isArray(input?.missionProcessed?.rocketChatUsernames) ? input.missionProcessed.rocketChatUsernames : [])\n },\n config: {\n ...missionData.config,\n GITEA_API_URL: missionData.config?.GITEA_API_URL || input?.config?.GITEA_API_URL || 'https://gite.slm-lab.net/api/v1',\n GITEA_API_TOKEN: missionData.config?.GITEA_API_TOKEN || input?.config?.GITEA_API_TOKEN || '310645d564cbf752be1fe3b42582a3d5f5d0bddd',\n GITEA_OWNER: missionData.config?.GITEA_OWNER || input?.config?.GITEA_OWNER || 'alma',\n LEANTIME_API_URL: missionData.config?.LEANTIME_API_URL || input?.config?.LEANTIME_API_URL || 'https://agilite.slm-lab.net',\n LEANTIME_API_TOKEN: missionData.config?.LEANTIME_API_TOKEN || input?.config?.LEANTIME_API_TOKEN || 'lt_lsdShQdoYHaPUWuL07XZR1Rf3GeySsIs_UDlll3VJPk5EwAuILpMC4BwzJ9MZFRrb',\n ROCKETCHAT_API_URL: missionData.config?.ROCKETCHAT_API_URL || input?.config?.ROCKETCHAT_API_URL || 'https://parole.slm-lab.net/',\n ROCKETCHAT_AUTH_TOKEN: missionData.config?.ROCKETCHAT_AUTH_TOKEN || input?.config?.ROCKETCHAT_AUTH_TOKEN || 'w91TYgkH-Z67Oz72usYdkW5TZLLRwnre7qyAhp7aHJB',\n ROCKETCHAT_USER_ID: missionData.config?.ROCKETCHAT_USER_ID || input?.config?.ROCKETCHAT_USER_ID || 'Tpuww59PJKsrGNQJB',\n OUTLINE_API_URL: missionData.config?.OUTLINE_API_URL || input?.config?.OUTLINE_API_URL || 'https://chapitre.slm-lab.net/api',\n OUTLINE_API_TOKEN: missionData.config?.OUTLINE_API_TOKEN || input?.config?.OUTLINE_API_TOKEN || 'ol_api_tlLlANBfcoJ4l7zA8GOcpduAeL6QyBTcYvEnlN',\n MISSION_API_URL: missionData.config?.MISSION_API_URL || input?.config?.MISSION_API_URL || 'https://hub.slm-lab.net',\n N8N_API_KEY: missionData.config?.N8N_API_KEY || input?.config?.N8N_API_KEY || 'LwgeE1ntADD20OuWC88S3pR0EaO7FtO4'\n },\n creatorId: missionData.creatorId || input?.creatorId\n};\n\n// Log the output for debugging\nconsole.log('Merge Paths - Output:', {\n hasLogoUrl: !!output.logoUrl,\n attachmentUrlsCount: output.attachmentUrls.length,\n hasAttachments: output.hasAttachments,\n skipIntegrations: output.skipIntegrations,\n hasMissionProcessed: !!output.missionProcessed,\n hasConfig: !!output.config,\n missionName: output.missionProcessed?.name,\n missionType: output.missionProcessed?.missionType,\n services: output.missionProcessed?.services\n});\n\nreturn { json: output };" + }, + "name": "Merge Paths", + "type": "n8n-nodes-base.function", + "typeVersion": 1, + "position": [ + 0, + 600 + ], + "id": "merge-paths-node" } ], "pinData": {}, @@ -668,11 +701,6 @@ "node": "Decode Logo Data", "type": "main", "index": 0 - }, - { - "node": "Check Attachments", - "type": "main", - "index": 0 } ] ] @@ -688,6 +716,39 @@ ] ] }, + "Debug Binary Data": { + "main": [ + [ + { + "node": "S3 Upload Logo", + "type": "main", + "index": 0 + } + ] + ] + }, + "S3 Upload Logo": { + "main": [ + [ + { + "node": "Process Upload Results", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Upload Results": { + "main": [ + [ + { + "node": "Check Attachments", + "type": "main", + "index": 0 + } + ] + ] + }, "Check Attachments": { "main": [ [ @@ -732,7 +793,7 @@ "main": [ [ { - "node": "Process Upload Results", + "node": "Get Keycloak Token", "type": "main", "index": 0 } @@ -740,28 +801,6 @@ ] }, "Empty Attachment Result": { - "main": [ - [ - { - "node": "Process Upload Results", - "type": "main", - "index": 0 - } - ] - ] - }, - "S3 Upload Logo": { - "main": [ - [ - { - "node": "Process Upload Results", - "type": "main", - "index": 0 - } - ] - ] - }, - "Process Upload Results": { "main": [ [ { @@ -795,6 +834,28 @@ ] }, "Debug Service Data": { + "main": [ + [ + { + "node": "Merge Paths", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge Paths": { + "main": [ + [ + { + "node": "IF Run Integrations", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF Run Integrations": { "main": [ [ { @@ -802,6 +863,13 @@ "type": "main", "index": 0 } + ], + [ + { + "node": "Combine Results", + "type": "main", + "index": 0 + } ] ] }, @@ -900,17 +968,6 @@ ] ] }, - "Debug Binary Data": { - "main": [ - [ - { - "node": "S3 Upload Logo", - "type": "main", - "index": 0 - } - ] - ] - }, "Mission Created Webhook": { "main": [ [ @@ -923,15 +980,15 @@ ] } }, - "active": true, + "active": false, "settings": { "executionOrder": "v1" }, - "versionId": "dc60ac68-266c-4508-ac77-f785e6d80df2", + "versionId": "850a95bd-fd96-4626-af02-56d489a194e5", "meta": { "templateCredsSetupCompleted": true, "instanceId": "575d8de48bd511243817deebddae0cc97d73be64c6c4737e5d4e9caddec881d8" }, - "id": "K4CEC0EP0BGzgeab", + "id": "CAO29zOiTqMcscxO", "tags": [] } \ No newline at end of file diff --git a/app/api/missions/route.ts b/app/api/missions/route.ts index 8e36ffde..f3fe7ab3 100644 --- a/app/api/missions/route.ts +++ b/app/api/missions/route.ts @@ -20,11 +20,11 @@ interface MissionCreateInput { guardians?: Record; volunteers?: string[]; logo?: string | null; - leantimeProjectId?: string; - documentationCollectionId?: string; - rocketchatChannelId?: string; - gitRepoUrl?: string; - penpotProjectId?: string; + leantimeProjectId?: string | null; + outlineCollectionId?: string | null; + rocketChatChannelId?: string | null; + giteaRepositoryUrl?: string | null; + penpotProjectId?: string | null; creatorId?: string; } @@ -34,6 +34,26 @@ interface MissionUserInput { missionId: string; } +interface MissionResponse { + name: string; + oddScope: string[]; + niveau: string; + intention: string; + missionType: string; + donneurDOrdre: string; + projection: string; + services: string[]; + profils: string[]; + participation: string; + creatorId: string; + logo: string | null; + leantimeProjectId: string | null; + outlineCollectionId: string | null; + rocketChatChannelId: string | null; + giteaRepositoryUrl: string | null; + penpotProjectId: string | null; +} + // Helper function to check authentication async function checkAuth(request: Request) { const apiKey = request.headers.get('x-api-key'); @@ -171,9 +191,9 @@ export async function POST(request: Request) { where: { id: existingMission.id }, data: { leantimeProjectId: body.leantimeProjectId ? String(body.leantimeProjectId) : null, - outlineCollectionId: body.documentationCollectionId || null, - rocketChatChannelId: body.rocketchatChannelId || null, - giteaRepositoryUrl: body.gitRepoUrl || null, + outlineCollectionId: body.outlineCollectionId || null, + rocketChatChannelId: body.rocketChatChannelId || null, + giteaRepositoryUrl: body.giteaRepositoryUrl || null, penpotProjectId: body.penpotProjectId || null } as Prisma.MissionUpdateInput }); @@ -318,9 +338,9 @@ export async function POST(request: Request) { creatorId: creatorId, logo: body.logo || null, leantimeProjectId: body.leantimeProjectId ? String(body.leantimeProjectId) : null, - outlineCollectionId: body.documentationCollectionId || null, - rocketChatChannelId: body.rocketchatChannelId || null, - giteaRepositoryUrl: body.gitRepoUrl || null, + outlineCollectionId: body.outlineCollectionId || null, + rocketChatChannelId: body.rocketChatChannelId || null, + giteaRepositoryUrl: body.giteaRepositoryUrl || null, penpotProjectId: body.penpotProjectId || null } as Prisma.MissionUncheckedCreateInput }); @@ -362,25 +382,58 @@ export async function POST(request: Request) { } } + // Format response to match workflow output + const missionResponse: MissionResponse = { + name: mission.name, + oddScope: mission.oddScope, + niveau: mission.niveau, + intention: mission.intention, + missionType: mission.missionType, + donneurDOrdre: mission.donneurDOrdre, + projection: mission.projection, + services: mission.services, + profils: mission.profils, + participation: mission.participation || 'default', + creatorId: mission.creatorId, + logo: mission.logo, + leantimeProjectId: (mission as any).leantimeProjectId, + outlineCollectionId: (mission as any).outlineCollectionId, + rocketChatChannelId: (mission as any).rocketChatChannelId, + giteaRepositoryUrl: (mission as any).giteaRepositoryUrl, + penpotProjectId: (mission as any).penpotProjectId + }; + return NextResponse.json({ + success: true, + status: 'success', message: 'Mission created successfully', - mission + mission: missionResponse, + integrationStatus: { + leantimeProject: !!missionResponse.leantimeProjectId, + outlineCollection: !!missionResponse.outlineCollectionId, + rocketChatChannel: !!missionResponse.rocketChatChannelId, + giteaRepository: !!missionResponse.giteaRepositoryUrl + } }); } catch (error) { console.error('Error creating mission:', error); if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === 'P2003') { return NextResponse.json({ - error: 'Invalid reference', - details: 'One or more referenced users do not exist', + success: false, + status: 'error', + error: 'Invalid reference', + message: 'One or more referenced users do not exist', code: 'INVALID_REFERENCE' }, { status: 400 }); } } return NextResponse.json( { - error: 'Failed to create mission', - details: error instanceof Error ? error.message : String(error) + success: false, + status: 'error', + error: 'Failed to create mission', + message: error instanceof Error ? error.message : String(error) }, { status: 500 } );