Skip to main content
Glama
availability-check.json20.6 kB
{ "name": "Template - Vérification de disponibilité Google Calendar", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "check-availability", "options": { "responseMode": "responseNode" }, "authentication": "basicAuth", "responseCode": "200", "responseContentType": "application/json", "responsePropertyName": "data" }, "id": "webhook-trigger", "name": "Webhook - Réception requête", "type": "n8n-nodes-base.webhook", "typeVersion": 1, "position": [ 250, 300 ], "webhookId": "check-availability" }, { "parameters": { "jsCode": "// Récupération des données d'entrée\nconst input = $input.item.json.body || $input.item.json;\n\n// Validation des champs requis\nconst requiredFields = ['timeMin', 'timeMax'];\nconst missingFields = [];\n\nfor (const field of requiredFields) {\n if (!input[field]) {\n missingFields.push(field);\n }\n}\n\nif (missingFields.length > 0) {\n return {\n error: true,\n message: `Champs requis manquants: ${missingFields.join(', ')}`,\n statusCode: 400\n };\n}\n\n// Validation des formats de date\nfunction isValidDate(dateStr) {\n return !isNaN(new Date(dateStr).getTime());\n}\n\nif (!isValidDate(input.timeMin) || !isValidDate(input.timeMax)) {\n return {\n error: true,\n message: 'Format de date invalide. Utilisez le format ISO 8601',\n statusCode: 400\n };\n}\n\n// Vérification que timeMin est avant timeMax\nconst timeMin = new Date(input.timeMin);\nconst timeMax = new Date(input.timeMax);\n\nif (timeMin >= timeMax) {\n return {\n error: true,\n message: 'La date de début doit être antérieure à la date de fin',\n statusCode: 400\n };\n}\n\n// Vérification que la plage n'est pas trop grande (limite à 60 jours pour éviter les abus)\nconst maxRangeMs = 60 * 24 * 60 * 60 * 1000; // 60 jours en millisecondes\nif (timeMax.getTime() - timeMin.getTime() > maxRangeMs) {\n return {\n error: true,\n message: 'La plage de dates ne peut pas dépasser 60 jours',\n statusCode: 400\n };\n}\n\n// Préparation des données pour la requête\nreturn {\n timeMin: timeMin.toISOString(),\n timeMax: timeMax.toISOString(),\n calendarId: input.calendarId || 'primary',\n timeZone: input.timeZone || 'Europe/Paris',\n // Paramètres optionnels\n minTime: input.minTime, // Heure minimale de la journée (format: 'HH:MM')\n maxTime: input.maxTime, // Heure maximale de la journée (format: 'HH:MM')\n durationMinutes: input.durationMinutes || 60, // Durée des créneaux en minutes\n bufferMinutes: input.bufferMinutes || 0, // Tampon entre les créneaux en minutes\n excludeWeekends: input.excludeWeekends === true, // Exclure les weekends\n requestId: $execution.id\n};" }, "id": "validate-format-data", "name": "Validation et formatage des données", "type": "n8n-nodes-base.code", "typeVersion": 1, "position": [ 450, 300 ] }, { "parameters": { "conditions": { "string": [ { "value1": "={{ $json.error }}", "operation": "exists" } ] } }, "id": "check-validation-errors", "name": "Vérification des erreurs de validation", "type": "n8n-nodes-base.if", "typeVersion": 1, "position": [ 650, 300 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ $json }}", "options": { "responseCode": "={{ $json.statusCode || 400 }}" } }, "id": "respond-validation-error", "name": "Répondre - Erreur de validation", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [ 850, 200 ] }, { "parameters": { "resource": "calendar", "operation": "get", "calendarId": "={{ $json.calendarId }}" }, "id": "check-calendar-exists", "name": "Vérifier l'existence du calendrier", "type": "n8n-nodes-base.googleCalendar", "typeVersion": 2, "position": [ 850, 400 ], "credentials": { "googleCalendarOAuth2Api": { "id": "1", "name": "Google Calendar OAuth2 API" } }, "continueOnFail": true }, { "parameters": { "conditions": { "string": [ { "value1": "={{ $json.error }}", "operation": "exists" } ] } }, "id": "check-calendar-error", "name": "Vérification erreur calendrier", "type": "n8n-nodes-base.if", "typeVersion": 1, "position": [ 1050, 400 ] }, { "parameters": { "respondWith": "json", "responseBody": "={\n \"error\": true,\n \"message\": \"Calendrier non trouvé ou erreur d'accès: \" + $json.error.message,\n \"statusCode\": 404\n}", "options": { "responseCode": 404 } }, "id": "respond-calendar-error", "name": "Répondre - Erreur calendrier", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [ 1250, 300 ] }, { "parameters": { "functionCode": "// Génération d'une clé de cache unique basée sur les paramètres\nconst cacheKey = `availability_${$json.calendarId}_${$json.timeMin}_${$json.timeMax}_${$json.timeZone}`;\n\n// Simuler la vérification du cache (dans un environnement réel, utilisez Redis ou une autre solution de cache)\nconst mockCache = {}; // Ceci serait remplacé par une vraie solution de cache\n\n// Vérifier si les données sont en cache\nif (mockCache[cacheKey]) {\n console.log(`Données trouvées en cache pour la clé: ${cacheKey}`);\n return {\n fromCache: true,\n cacheKey,\n ...mockCache[cacheKey]\n };\n}\n\n// Si pas en cache, continuer le flux\nreturn {\n fromCache: false,\n cacheKey,\n calendarId: $json.calendarId,\n timeMin: $json.timeMin,\n timeMax: $json.timeMax,\n timeZone: $json.timeZone\n};" }, "id": "check-cache", "name": "Vérification du cache", "type": "n8n-nodes-base.function", "typeVersion": 1, "position": [ 1250, 500 ] }, { "parameters": { "conditions": { "boolean": [ { "value1": "={{ $json.fromCache }}", "value2": true } ] } }, "id": "check-from-cache", "name": "Données en cache?", "type": "n8n-nodes-base.if", "typeVersion": 1, "position": [ 1450, 500 ] }, { "parameters": { "resource": "event", "operation": "getAll", "calendarId": "={{ $json.calendarId }}", "returnAll": true, "options": { "timeMin": "={{ $json.timeMin }}", "timeMax": "={{ $json.timeMax }}", "singleEvents": true, "orderBy": "startTime" } }, "id": "get-events", "name": "Récupération des événements", "type": "n8n-nodes-base.googleCalendar", "typeVersion": 2, "position": [ 1650, 600 ], "credentials": { "googleCalendarOAuth2Api": { "id": "1", "name": "Google Calendar OAuth2 API" } }, "continueOnFail": true }, { "parameters": { "conditions": { "string": [ { "value1": "={{ $json.error }}", "operation": "exists" } ] } }, "id": "check-events-error", "name": "Vérification erreur événements", "type": "n8n-nodes-base.if", "typeVersion": 1, "position": [ 1850, 600 ] }, { "parameters": { "jsCode": "// Gestion des erreurs d'API Google Calendar\nconst error = $input.item.json.error;\n\n// Vérifier si c'est une erreur d'authentification\nif (error.message && (error.message.includes('401') || error.message.includes('auth'))) {\n return {\n error: true,\n authError: true,\n message: \"Erreur d'authentification. Le token OAuth doit être rafraîchi.\",\n originalError: error.message,\n statusCode: 401\n };\n}\n\n// Vérifier si c'est une erreur de quota\nif (error.message && error.message.includes('quota')) {\n return {\n error: true,\n quotaError: true,\n message: \"Quota d'API Google Calendar dépassé. Réessayez plus tard.\",\n originalError: error.message,\n statusCode: 429\n };\n}\n\n// Erreur générique\nreturn {\n error: true,\n message: \"Erreur lors de la récupération des événements: \" + error.message,\n originalError: error.message,\n statusCode: 500\n};" }, "id": "analyze-error", "name": "Analyse de l'erreur", "type": "n8n-nodes-base.code", "typeVersion": 1, "position": [ 2050, 500 ] }, { "parameters": { "respondWith": "json", "responseBody": "={{ $json }}", "options": { "responseCode": "={{ $json.statusCode || 500 }}" } }, "id": "respond-events-error", "name": "Répondre - Erreur événements", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [ 2250, 500 ] }, { "parameters": { "jsCode": "// Récupération des paramètres\nconst timeMin = new Date($('validate-format-data').item.json.timeMin);\nconst timeMax = new Date($('validate-format-data').item.json.timeMax);\nconst timeZone = $('validate-format-data').item.json.timeZone;\nconst durationMinutes = $('validate-format-data').item.json.durationMinutes;\nconst bufferMinutes = $('validate-format-data').item.json.bufferMinutes;\nconst excludeWeekends = $('validate-format-data').item.json.excludeWeekends;\nconst minTime = $('validate-format-data').item.json.minTime;\nconst maxTime = $('validate-format-data').item.json.maxTime;\n\n// Récupération des événements existants\nconst events = $input.item.json;\n\n// Fonction pour convertir une date en minutes depuis minuit\nfunction dateToMinutes(date) {\n return date.getHours() * 60 + date.getMinutes();\n}\n\n// Fonction pour convertir des minutes en chaîne HH:MM\nfunction minutesToTimeString(minutes) {\n const hours = Math.floor(minutes / 60);\n const mins = minutes % 60;\n return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;\n}\n\n// Fonction pour vérifier si une date est un weekend\nfunction isWeekend(date) {\n const day = date.getDay();\n return day === 0 || day === 6; // 0 = dimanche, 6 = samedi\n}\n\n// Convertir minTime et maxTime en minutes si spécifiés\nlet dayStartMinutes = 8 * 60; // Par défaut 8h00\nlet dayEndMinutes = 18 * 60; // Par défaut 18h00\n\nif (minTime) {\n const [hours, minutes] = minTime.split(':').map(Number);\n dayStartMinutes = hours * 60 + minutes;\n}\n\nif (maxTime) {\n const [hours, minutes] = maxTime.split(':').map(Number);\n dayEndMinutes = hours * 60 + minutes;\n}\n\n// Créer un tableau de créneaux occupés\nconst busySlots = [];\nif (Array.isArray(events)) {\n events.forEach(event => {\n // Ignorer les événements transparents (n'affectent pas la disponibilité)\n if (event.transparency === 'transparent') return;\n \n const start = new Date(event.start.dateTime || event.start.date);\n const end = new Date(event.end.dateTime || event.end.date);\n \n busySlots.push({\n start,\n end,\n title: event.summary,\n id: event.id\n });\n });\n}\n\n// Trier les créneaux occupés par date de début\nbusySlots.sort((a, b) => a.start.getTime() - b.start.getTime());\n\n// Générer les créneaux disponibles\nconst availableSlots = [];\nlet currentDate = new Date(timeMin);\n\n// Réinitialiser l'heure au début de la journée\ncurrentDate.setHours(0, 0, 0, 0);\n\nwhile (currentDate < timeMax) {\n // Vérifier si c'est un weekend\n if (excludeWeekends && isWeekend(currentDate)) {\n // Passer au jour suivant\n currentDate.setDate(currentDate.getDate() + 1);\n currentDate.setHours(0, 0, 0, 0);\n continue;\n }\n \n // Définir le début et la fin de la journée de travail\n const dayStart = new Date(currentDate);\n dayStart.setHours(Math.floor(dayStartMinutes / 60), dayStartMinutes % 60, 0, 0);\n \n const dayEnd = new Date(currentDate);\n dayEnd.setHours(Math.floor(dayEndMinutes / 60), dayEndMinutes % 60, 0, 0);\n \n // Ignorer si le jour est déjà passé\n if (dayEnd <= timeMin) {\n currentDate.setDate(currentDate.getDate() + 1);\n currentDate.setHours(0, 0, 0, 0);\n continue;\n }\n \n // Ajuster le début si nécessaire\n let slotStart = dayStart;\n if (slotStart < timeMin) {\n slotStart = new Date(timeMin);\n }\n \n // Trouver les créneaux occupés pour cette journée\n const todayBusySlots = busySlots.filter(slot => \n slot.start.getDate() === currentDate.getDate() &&\n slot.start.getMonth() === currentDate.getMonth() &&\n slot.start.getFullYear() === currentDate.getFullYear()\n );\n \n // Générer les créneaux disponibles pour cette journée\n while (slotStart < dayEnd && slotStart < timeMax) {\n const slotEnd = new Date(slotStart.getTime() + durationMinutes * 60 * 1000);\n \n // Vérifier si le créneau dépasse la fin de la journée ou timeMax\n if (slotEnd > dayEnd || slotEnd > timeMax) {\n break;\n }\n \n // Vérifier si le créneau est disponible\n const isAvailable = !todayBusySlots.some(busySlot => \n (slotStart < busySlot.end && slotEnd > busySlot.start)\n );\n \n if (isAvailable) {\n availableSlots.push({\n start: slotStart.toISOString(),\n end: slotEnd.toISOString(),\n startFormatted: slotStart.toLocaleString('fr-FR', { timeZone }),\n endFormatted: slotEnd.toLocaleString('fr-FR', { timeZone }),\n duration: durationMinutes\n });\n }\n \n // Passer au créneau suivant (ajouter la durée + le tampon)\n slotStart = new Date(slotStart.getTime() + (durationMinutes + bufferMinutes) * 60 * 1000);\n }\n \n // Passer au jour suivant\n currentDate.setDate(currentDate.getDate() + 1);\n currentDate.setHours(0, 0, 0, 0);\n}\n\n// Préparer les données de résultat\nconst result = {\n timeMin: timeMin.toISOString(),\n timeMax: timeMax.toISOString(),\n timeZone,\n durationMinutes,\n bufferMinutes,\n busySlots: busySlots.map(slot => ({\n start: slot.start.toISOString(),\n end: slot.end.toISOString(),\n title: slot.title\n })),\n availableSlots,\n totalAvailable: availableSlots.length,\n requestId: $('validate-format-data').item.json.requestId\n};\n\n// Simuler la mise en cache (dans un environnement réel, utilisez Redis ou une autre solution de cache)\n// mockCache[$('check-cache').item.json.cacheKey] = result;\n\nreturn result;" }, "id": "calculate-availability", "name": "Calcul des disponibilités", "type": "n8n-nodes-base.code", "typeVersion": 1, "position": [ 2050, 700 ] }, { "parameters": { "functionCode": "// Mise en cache des résultats (simulation)\n// Dans un environnement réel, utilisez Redis ou une autre solution de cache\n\nconst cacheKey = $('check-cache').item.json.cacheKey;\nconst data = $input.item.json;\n\n// Simuler la mise en cache\nconsole.log(`Mise en cache des données pour la clé: ${cacheKey}`);\n\n// Définir une durée d'expiration (30 minutes)\nconst expirationMs = 30 * 60 * 1000;\n\n// Dans un environnement réel, ce serait quelque chose comme:\n// await redisClient.set(cacheKey, JSON.stringify(data), 'PX', expirationMs);\n\n// Retourner les données inchangées\nreturn data;" }, "id": "cache-results", "name": "Mise en cache des résultats", "type": "n8n-nodes-base.function", "typeVersion": 1, "position": [ 2250, 700 ] }, { "parameters": { "respondWith": "json", "responseBody": "={\n \"success\": true,\n \"fromCache\": $('check-from-cache').item.json.fromCache === true,\n \"data\": $json\n}", "options": {} }, "id": "respond-success", "name": "Répondre - Succès", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [ 2450, 600 ] } ], "connections": { "webhook-trigger": { "main": [ [ { "node": "validate-format-data", "type": "main", "index": 0 } ] ] }, "validate-format-data": { "main": [ [ { "node": "check-validation-errors", "type": "main", "index": 0 } ] ] }, "check-validation-errors": { "main": [ [ { "node": "respond-validation-error", "type": "main", "index": 0 } ], [ { "node": "check-calendar-exists", "type": "main", "index": 0 } ] ] }, "check-calendar-exists": { "main": [ [ { "node": "check-calendar-error", "type": "main", "index": 0 } ] ] }, "check-calendar-error": { "main": [ [ { "node": "respond-calendar-error", "type": "main", "index": 0 } ], [ { "node": "check-cache", "type": "main", "index": 0 } ] ] }, "check-cache": { "main": [ [ { "node": "check-from-cache", "type": "main", "index": 0 } ] ] }, "check-from-cache": { "main": [ [ { "node": "respond-success", "type": "main", "index": 0 } ], [ { "node": "get-events", "type": "main", "index": 0 } ] ] }, "get-events": { "main": [ [ { "node": "check-events-error", "type": "main", "index": 0 } ] ] }, "check-events-error": { "main": [ [ { "node": "analyze-error", "type": "main", "index": 0 } ], [ { "node": "calculate-availability", "type": "main", "index": 0 } ] ] }, "analyze-error": { "main": [ [ { "node": "respond-events-error", "type": "main", "index": 0 } ] ] }, "calculate-availability": { "main": [ [ { "node": "cache-results", "type": "main", "index": 0 } ] ] }, "cache-results": { "main": [ [ { "node": "respond-success", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1", "saveManualExecutions": true, "callerPolicy": "workflowsFromSameOwner", "errorWorkflow": "", "saveDataErrorExecution": "all", "saveDataSuccessExecution": "all", "saveExecutionProgress": true }, "tags": ["template", "calendar", "availability"], "pinData": {}, "versionId": "1", "meta": { "templateCreatedBy": "MCP n8n Server", "templateDescription": "Workflow pour vérifier la disponibilité d'une plage horaire dans Google Calendar.", "templateName": "Vérification de disponibilité Google Calendar", "templateCategory": "calendar", "templateVersion": "1.0.0", "templateDocumentation": "Ce template permet de vérifier la disponibilité dans Google Calendar et de calculer les créneaux disponibles sur une plage de dates. Il implémente un système de mise en cache pour optimiser les performances et respecter les quotas d'API Google Calendar." } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lowprofix/n8n-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server