import WorkflowValidator from '../resources/WorkflowValidator.js';
/**
* Validateur pour les intégrations avec Google Calendar dans les workflows n8n
* Vérifie la configuration des nœuds Google Calendar, la gestion des quotas,
* les mécanismes de rafraîchissement OAuth et la validation des formats de date/heure
*/
class CalendarIntegrationValidator implements WorkflowValidator {
/**
* Valide les aspects d'intégration avec Google Calendar dans un workflow n8n
* @param workflow Les données du workflow à valider
* @param strictness Le niveau de rigueur de la validation
* @returns Résultat de la validation avec les problèmes détectés
*/
validate(workflow: any, strictness: 'low' | 'medium' | 'high') {
const issues: Array<{
message: string;
recommendation: string;
severity: 'low' | 'medium' | 'high';
location?: string;
}> = [];
// Vérifier si le workflow contient des nœuds Google Calendar
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
return { valid: true, issues: [] };
}
const calendarNodes = workflow.nodes.filter((node: any) =>
(node.type || '').toLowerCase().includes('googlecalendar') ||
(node.name || '').toLowerCase().includes('calendar')
);
if (calendarNodes.length === 0) {
return { valid: true, issues: [] };
}
// Vérifier chaque nœud Google Calendar
calendarNodes.forEach((node: any) => {
// 1. Vérifier la configuration des nœuds Google Calendar
this.checkCalendarNodeConfiguration(node, issues);
// 2. Vérifier la gestion des quotas d'API
this.checkApiQuotaManagement(node, workflow, issues, strictness);
// 3. Vérifier les mécanismes de rafraîchissement des tokens OAuth
this.checkOAuthRefreshMechanism(node, workflow, issues);
// 4. Vérifier la validation des formats de date/heure
this.checkDateTimeFormats(node, issues);
});
// 5. Vérifier la présence de nœuds de gestion d'erreurs pour les appels Calendar
this.checkErrorHandlingForCalendarNodes(workflow, calendarNodes, issues, strictness);
return {
valid: issues.length === 0,
issues,
};
}
/**
* Vérifie la configuration correcte des nœuds Google Calendar
*/
private checkCalendarNodeConfiguration(node: any, issues: any[]): void {
// Vérifier si les paramètres requis sont présents
if (!node.parameters) {
issues.push({
message: `Le nœud "${node.name}" n'a pas de paramètres configurés`,
recommendation: 'Configurez les paramètres requis pour le nœud Google Calendar',
severity: 'high',
location: node.id,
});
return;
}
// Vérifier si le calendrier est spécifié
if (node.parameters.calendarId === undefined || node.parameters.calendarId === '') {
issues.push({
message: `Le nœud "${node.name}" n'a pas d'ID de calendrier spécifié`,
recommendation: 'Spécifiez un ID de calendrier valide ou utilisez "primary" pour le calendrier principal',
severity: 'high',
location: node.id,
});
}
// Vérifier si les opérations sont correctement configurées
if (node.parameters.operation === undefined) {
issues.push({
message: `Le nœud "${node.name}" n'a pas d'opération spécifiée`,
recommendation: 'Spécifiez une opération valide (ex: create, update, get, list, delete)',
severity: 'high',
location: node.id,
});
}
// Vérifier si les credentials sont configurées
if (!node.credentials || !node.credentials.googleCalendarOAuth2Api) {
issues.push({
message: `Le nœud "${node.name}" n'a pas de credentials OAuth configurées`,
recommendation: 'Configurez les credentials OAuth pour Google Calendar',
severity: 'high',
location: node.id,
});
}
}
/**
* Vérifie la gestion des quotas d'API Google Calendar
*/
private checkApiQuotaManagement(node: any, workflow: any, issues: any[], strictness: string): void {
// Vérifier si le workflow a des mécanismes de limitation de débit
const hasRateLimiting = workflow.nodes.some((n: any) =>
(n.type || '').toLowerCase().includes('limit') ||
(n.name || '').toLowerCase().includes('throttle') ||
(n.name || '').toLowerCase().includes('limit') ||
(n.name || '').toLowerCase().includes('delay')
);
if (!hasRateLimiting && strictness !== 'low') {
issues.push({
message: `Le workflow utilisant Google Calendar n'a pas de mécanisme de limitation de débit`,
recommendation: 'Ajoutez des nœuds de limitation de débit pour éviter de dépasser les quotas de l\'API Google Calendar (ex: Limit, Delay)',
severity: strictness === 'high' ? 'high' : 'medium',
location: node.id,
});
}
// Vérifier si le workflow a des mécanismes de mise en cache
const hasCaching = workflow.nodes.some((n: any) =>
(n.name || '').toLowerCase().includes('cache') ||
(n.name || '').toLowerCase().includes('store')
);
if (!hasCaching && strictness === 'high') {
issues.push({
message: `Le workflow n'utilise pas de mise en cache pour les données Google Calendar`,
recommendation: 'Implémentez un mécanisme de mise en cache pour réduire le nombre d\'appels à l\'API Google Calendar',
severity: 'medium',
location: node.id,
});
}
// Vérifier si le workflow a des nœuds de traitement par lots pour les opérations en masse
if (this.isListOperation(node) && !this.hasBatchProcessing(workflow)) {
issues.push({
message: `Le nœud "${node.name}" effectue une opération de liste sans traitement par lots`,
recommendation: 'Utilisez des nœuds de traitement par lots pour gérer efficacement les grandes quantités de données',
severity: strictness === 'low' ? 'low' : 'medium',
location: node.id,
});
}
}
/**
* Vérifie si un nœud effectue une opération de liste
*/
private isListOperation(node: any): boolean {
return node.parameters?.operation === 'list' ||
(node.parameters?.resource === 'event' && node.parameters?.operation === 'getAll');
}
/**
* Vérifie si le workflow a des nœuds de traitement par lots
*/
private hasBatchProcessing(workflow: any): boolean {
return workflow.nodes.some((n: any) =>
(n.type || '').toLowerCase().includes('split') ||
(n.name || '').toLowerCase().includes('batch') ||
(n.name || '').toLowerCase().includes('split') ||
(n.name || '').toLowerCase().includes('chunk')
);
}
/**
* Vérifie les mécanismes de rafraîchissement des tokens OAuth
*/
private checkOAuthRefreshMechanism(node: any, workflow: any, issues: any[]): void {
// Vérifier si le workflow a un mécanisme de gestion des erreurs d'authentification
const hasAuthErrorHandling = workflow.nodes.some((n: any) => {
if ((n.type || '').toLowerCase().includes('if') || (n.type || '').toLowerCase().includes('switch')) {
const nodeString = JSON.stringify(n);
return nodeString.includes('401') ||
nodeString.includes('403') ||
nodeString.toLowerCase().includes('unauthorized') ||
nodeString.toLowerCase().includes('authentication') ||
nodeString.toLowerCase().includes('token expired');
}
return false;
});
if (!hasAuthErrorHandling) {
issues.push({
message: `Le workflow n'a pas de mécanisme pour gérer les erreurs d'authentification ou les tokens expirés`,
recommendation: 'Ajoutez une gestion des erreurs 401/403 et un mécanisme de rafraîchissement des tokens OAuth',
severity: 'high',
location: node.id,
});
}
// Vérifier si le workflow a un nœud pour rafraîchir les tokens
const hasRefreshNode = workflow.nodes.some((n: any) =>
(n.name || '').toLowerCase().includes('refresh') &&
(n.name || '').toLowerCase().includes('token')
);
if (!hasRefreshNode) {
issues.push({
message: `Le workflow n'a pas de nœud explicite pour rafraîchir les tokens OAuth`,
recommendation: 'Ajoutez un nœud dédié au rafraîchissement des tokens OAuth pour Google Calendar',
severity: 'medium',
location: node.id,
});
}
}
/**
* Vérifie la validation des formats de date/heure
*/
private checkDateTimeFormats(node: any, issues: any[]): void {
// Vérifier si le nœud manipule des dates/heures
if (this.isEventCreationOrUpdate(node)) {
// Vérifier si les dates sont correctement formatées pour Google Calendar
const nodeString = JSON.stringify(node);
// Vérifier si le workflow utilise des fonctions de formatage de date
const hasDateFormatting =
nodeString.includes('new Date') ||
nodeString.includes('moment') ||
nodeString.includes('format') ||
nodeString.includes('toISOString') ||
nodeString.includes('$formatDate');
if (!hasDateFormatting) {
issues.push({
message: `Le nœud "${node.name}" manipule des événements sans formater explicitement les dates`,
recommendation: 'Utilisez des fonctions de formatage de date (ex: $formatDate, toISOString()) pour assurer la compatibilité avec Google Calendar',
severity: 'medium',
location: node.id,
});
}
// Vérifier si le fuseau horaire est géré
const hasTimezoneHandling =
nodeString.includes('timeZone') ||
nodeString.includes('timezone') ||
nodeString.includes('utc');
if (!hasTimezoneHandling) {
issues.push({
message: `Le nœud "${node.name}" ne gère pas explicitement les fuseaux horaires`,
recommendation: 'Spécifiez explicitement les fuseaux horaires pour éviter les problèmes de conversion de date/heure',
severity: 'medium',
location: node.id,
});
}
}
}
/**
* Vérifie si un nœud crée ou met à jour des événements
*/
private isEventCreationOrUpdate(node: any): boolean {
if (!node.parameters) return false;
return (node.parameters.resource === 'event' &&
(node.parameters.operation === 'create' ||
node.parameters.operation === 'update')) ||
node.parameters.operation === 'insert' ||
node.parameters.operation === 'patch' ||
node.parameters.operation === 'update';
}
/**
* Vérifie la présence de nœuds de gestion d'erreurs pour les appels Calendar
*/
private checkErrorHandlingForCalendarNodes(workflow: any, calendarNodes: any[], issues: any[], strictness: string): void {
// Vérifier si chaque nœud Calendar a une gestion d'erreur associée
calendarNodes.forEach((calendarNode: any) => {
const hasErrorHandling = this.hasErrorHandlingForNode(workflow, calendarNode);
if (!hasErrorHandling) {
issues.push({
message: `Le nœud "${calendarNode.name}" n'a pas de gestion d'erreur associée`,
recommendation: 'Ajoutez des nœuds de gestion d\'erreur (Error Trigger) pour gérer les échecs d\'appels à l\'API Google Calendar',
severity: strictness === 'low' ? 'medium' : 'high',
location: calendarNode.id,
});
}
});
// Vérifier si le workflow a un mécanisme de retry pour les erreurs temporaires
const hasRetryMechanism = workflow.nodes.some((n: any) =>
(n.name || '').toLowerCase().includes('retry') ||
(n.parameters?.continueOnFail === true)
);
if (!hasRetryMechanism && strictness !== 'low') {
issues.push({
message: 'Le workflow n\'a pas de mécanisme de retry pour les erreurs temporaires de l\'API Google Calendar',
recommendation: 'Implémentez un mécanisme de retry avec backoff exponentiel pour gérer les erreurs temporaires de l\'API',
severity: 'medium',
});
}
}
/**
* Vérifie si un nœud a une gestion d'erreur associée
*/
private hasErrorHandlingForNode(workflow: any, node: any): boolean {
// Vérifier s'il y a un nœud Error Trigger connecté à ce nœud
const hasErrorTrigger = workflow.nodes.some((n: any) =>
(n.type || '').toLowerCase().includes('error') &&
this.isConnectedToNode(workflow, n, node)
);
// Vérifier si le nœud a l'option continueOnFail activée
const hasContinueOnFail = node.parameters?.continueOnFail === true;
// Vérifier s'il y a un nœud IF qui vérifie les erreurs après ce nœud
const hasErrorCheckingIf = workflow.nodes.some((n: any) => {
if ((n.type || '').toLowerCase().includes('if')) {
const nodeString = JSON.stringify(n);
return (nodeString.includes('error') || nodeString.includes('fail')) &&
this.isNodeAfter(workflow, n, node);
}
return false;
});
return hasErrorTrigger || hasContinueOnFail || hasErrorCheckingIf;
}
/**
* Vérifie si un nœud est connecté à un autre nœud
*/
private isConnectedToNode(workflow: any, node1: any, node2: any): boolean {
if (!workflow.connections) return false;
// Parcourir toutes les connexions pour voir si node1 est connecté à node2
for (const sourceNode in workflow.connections) {
if (sourceNode === node2.id) {
for (const sourceOutput in workflow.connections[sourceNode]) {
for (const connection of workflow.connections[sourceNode][sourceOutput]) {
if (connection.node === node1.id) {
return true;
}
}
}
}
}
return false;
}
/**
* Vérifie si un nœud est exécuté après un autre nœud
*/
private isNodeAfter(workflow: any, node1: any, node2: any): boolean {
if (!workflow.connections) return false;
// Fonction récursive pour vérifier si node1 est atteignable depuis node2
const isReachable = (currentNodeId: string, visited = new Set<string>()): boolean => {
if (currentNodeId === node1.id) return true;
if (visited.has(currentNodeId)) return false;
visited.add(currentNodeId);
if (!workflow.connections[currentNodeId]) return false;
for (const output in workflow.connections[currentNodeId]) {
for (const connection of workflow.connections[currentNodeId][output]) {
if (isReachable(connection.node, visited)) {
return true;
}
}
}
return false;
};
return isReachable(node2.id);
}
}
export default CalendarIntegrationValidator;