File size: 11,371 Bytes
da2e594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
 * Expression Format Validator for n8n expressions
 *
 * Combines universal expression validation with node-specific intelligence
 * to provide comprehensive expression format validation. Uses the
 * UniversalExpressionValidator for 100% reliable base validation and adds
 * node-specific resource locator detection on top.
 */

import { UniversalExpressionValidator, UniversalValidationResult } from './universal-expression-validator';
import { ConfidenceScorer } from './confidence-scorer';

export interface ExpressionFormatIssue {
  fieldPath: string;
  currentValue: any;
  correctedValue: any;
  issueType: 'missing-prefix' | 'needs-resource-locator' | 'invalid-rl-structure' | 'mixed-format';
  explanation: string;
  severity: 'error' | 'warning';
  confidence?: number; // 0.0 to 1.0, only for node-specific recommendations
}

export interface ResourceLocatorField {
  __rl: true;
  value: string;
  mode: string;
}

export interface ValidationContext {
  nodeType: string;
  nodeName: string;
  nodeId?: string;
}

export class ExpressionFormatValidator {
  private static readonly VALID_RL_MODES = ['id', 'url', 'expression', 'name', 'list'] as const;
  private static readonly MAX_RECURSION_DEPTH = 100;
  private static readonly EXPRESSION_PREFIX = '='; // Keep for resource locator generation

  /**
   * Known fields that commonly use resource locator format
   * Map of node type patterns to field names
   */
  private static readonly RESOURCE_LOCATOR_FIELDS: Record<string, string[]> = {
    'github': ['owner', 'repository', 'user', 'organization'],
    'googleSheets': ['sheetId', 'documentId', 'spreadsheetId', 'rangeDefinition'],
    'googleDrive': ['fileId', 'folderId', 'driveId'],
    'slack': ['channel', 'user', 'channelId', 'userId', 'teamId'],
    'notion': ['databaseId', 'pageId', 'blockId'],
    'airtable': ['baseId', 'tableId', 'viewId'],
    'monday': ['boardId', 'itemId', 'groupId'],
    'hubspot': ['contactId', 'companyId', 'dealId'],
    'salesforce': ['recordId', 'objectName'],
    'jira': ['projectKey', 'issueKey', 'boardId'],
    'gitlab': ['projectId', 'mergeRequestId', 'issueId'],
    'mysql': ['table', 'database', 'schema'],
    'postgres': ['table', 'database', 'schema'],
    'mongodb': ['collection', 'database'],
    's3': ['bucketName', 'key', 'fileName'],
    'ftp': ['path', 'fileName'],
    'ssh': ['path', 'fileName'],
    'redis': ['key'],
  };


  /**
   * Determine if a field should use resource locator format based on node type and field name
   */
  private static shouldUseResourceLocator(fieldName: string, nodeType: string): boolean {
    // Extract the base node type (e.g., 'github' from 'n8n-nodes-base.github')
    const nodeBase = nodeType.split('.').pop()?.toLowerCase() || '';

    // Check if this node type has resource locator fields
    for (const [pattern, fields] of Object.entries(this.RESOURCE_LOCATOR_FIELDS)) {
      // Use exact match or prefix matching for precision
      // This prevents false positives like 'postgresqlAdvanced' matching 'postgres'
      if ((nodeBase === pattern || nodeBase.startsWith(`${pattern}-`)) && fields.includes(fieldName)) {
        return true;
      }
    }

    // Don't apply resource locator to generic fields
    return false;
  }

  /**
   * Check if a value is a valid resource locator object
   */
  private static isResourceLocator(value: any): value is ResourceLocatorField {
    if (typeof value !== 'object' || value === null || value.__rl !== true) {
      return false;
    }

    if (!('value' in value) || !('mode' in value)) {
      return false;
    }

    // Validate mode is one of the allowed values
    if (typeof value.mode !== 'string' || !this.VALID_RL_MODES.includes(value.mode as any)) {
      return false;
    }

    return true;
  }

  /**
   * Generate the corrected value for an expression
   */
  private static generateCorrection(
    value: string,
    needsResourceLocator: boolean
  ): any {
    const correctedValue = value.startsWith(this.EXPRESSION_PREFIX)
      ? value
      : `${this.EXPRESSION_PREFIX}${value}`;

    if (needsResourceLocator) {
      return {
        __rl: true,
        value: correctedValue,
        mode: 'expression'
      };
    }

    return correctedValue;
  }

  /**
   * Validate and fix expression format for a single value
   */
  static validateAndFix(
    value: any,
    fieldPath: string,
    context: ValidationContext
  ): ExpressionFormatIssue | null {
    // Skip non-string values unless they're resource locators
    if (typeof value !== 'string' && !this.isResourceLocator(value)) {
      return null;
    }

    // Handle resource locator objects
    if (this.isResourceLocator(value)) {
      // Use universal validator for the value inside RL
      const universalResults = UniversalExpressionValidator.validate(value.value);
      const invalidResult = universalResults.find(r => !r.isValid && r.needsPrefix);

      if (invalidResult) {
        return {
          fieldPath,
          currentValue: value,
          correctedValue: {
            ...value,
            value: UniversalExpressionValidator.getCorrectedValue(value.value)
          },
          issueType: 'missing-prefix',
          explanation: `Resource locator value: ${invalidResult.explanation}`,
          severity: 'error'
        };
      }
      return null;
    }

    // First, use universal validator for 100% reliable validation
    const universalResults = UniversalExpressionValidator.validate(value);
    const invalidResults = universalResults.filter(r => !r.isValid);

    // If universal validator found issues, report them
    if (invalidResults.length > 0) {
      // Prioritize prefix issues
      const prefixIssue = invalidResults.find(r => r.needsPrefix);
      if (prefixIssue) {
        // Check if this field should use resource locator format with confidence scoring
        const fieldName = fieldPath.split('.').pop() || '';
        const confidenceScore = ConfidenceScorer.scoreResourceLocatorRecommendation(
          fieldName,
          context.nodeType,
          value
        );

        // Only suggest resource locator for high confidence matches when there's a prefix issue
        if (confidenceScore.value >= 0.8) {
          return {
            fieldPath,
            currentValue: value,
            correctedValue: this.generateCorrection(value, true),
            issueType: 'needs-resource-locator',
            explanation: `Field '${fieldName}' contains expression but needs resource locator format with '${this.EXPRESSION_PREFIX}' prefix for evaluation.`,
            severity: 'error',
            confidence: confidenceScore.value
          };
        } else {
          return {
            fieldPath,
            currentValue: value,
            correctedValue: UniversalExpressionValidator.getCorrectedValue(value),
            issueType: 'missing-prefix',
            explanation: prefixIssue.explanation,
            severity: 'error'
          };
        }
      }

      // Report other validation issues
      const firstIssue = invalidResults[0];
      return {
        fieldPath,
        currentValue: value,
        correctedValue: value,
        issueType: 'mixed-format',
        explanation: firstIssue.explanation,
        severity: 'error'
      };
    }

    // Universal validation passed, now check for node-specific improvements
    // Only if the value has expressions
    const hasExpression = universalResults.some(r => r.hasExpression);
    if (hasExpression && typeof value === 'string') {
      const fieldName = fieldPath.split('.').pop() || '';
      const confidenceScore = ConfidenceScorer.scoreResourceLocatorRecommendation(
        fieldName,
        context.nodeType,
        value
      );

      // Only suggest resource locator for medium-high confidence as a warning
      if (confidenceScore.value >= 0.5) {
        // Has prefix but should use resource locator format
        return {
          fieldPath,
          currentValue: value,
          correctedValue: this.generateCorrection(value, true),
          issueType: 'needs-resource-locator',
          explanation: `Field '${fieldName}' should use resource locator format for better compatibility. (Confidence: ${Math.round(confidenceScore.value * 100)}%)`,
          severity: 'warning',
          confidence: confidenceScore.value
        };
      }
    }

    return null;
  }

  /**
   * Validate all expressions in a node's parameters recursively
   */
  static validateNodeParameters(
    parameters: any,
    context: ValidationContext
  ): ExpressionFormatIssue[] {
    const issues: ExpressionFormatIssue[] = [];
    const visited = new WeakSet();

    this.validateRecursive(parameters, '', context, issues, visited);

    return issues;
  }

  /**
   * Recursively validate parameters for expression format issues
   */
  private static validateRecursive(
    obj: any,
    path: string,
    context: ValidationContext,
    issues: ExpressionFormatIssue[],
    visited: WeakSet<object>,
    depth = 0
  ): void {
    // Prevent excessive recursion
    if (depth > this.MAX_RECURSION_DEPTH) {
      issues.push({
        fieldPath: path,
        currentValue: obj,
        correctedValue: obj,
        issueType: 'mixed-format',
        explanation: `Maximum recursion depth (${this.MAX_RECURSION_DEPTH}) exceeded. Object may have circular references or be too deeply nested.`,
        severity: 'warning'
      });
      return;
    }

    // Handle circular references
    if (obj && typeof obj === 'object') {
      if (visited.has(obj)) return;
      visited.add(obj);
    }

    // Check current value
    const issue = this.validateAndFix(obj, path, context);
    if (issue) {
      issues.push(issue);
    }

    // Recurse into objects and arrays
    if (Array.isArray(obj)) {
      obj.forEach((item, index) => {
        const newPath = path ? `${path}[${index}]` : `[${index}]`;
        this.validateRecursive(item, newPath, context, issues, visited, depth + 1);
      });
    } else if (obj && typeof obj === 'object') {
      // Skip resource locator internals if already validated
      if (this.isResourceLocator(obj)) {
        return;
      }

      Object.entries(obj).forEach(([key, value]) => {
        // Skip special keys
        if (key.startsWith('__')) return;

        const newPath = path ? `${path}.${key}` : key;
        this.validateRecursive(value, newPath, context, issues, visited, depth + 1);
      });
    }
  }

  /**
   * Generate a detailed error message with examples
   */
  static formatErrorMessage(issue: ExpressionFormatIssue, context: ValidationContext): string {
    let message = `Expression format ${issue.severity} in node '${context.nodeName}':\n`;
    message += `Field '${issue.fieldPath}' ${issue.explanation}\n\n`;

    message += `Current (incorrect):\n`;
    if (typeof issue.currentValue === 'string') {
      message += `"${issue.fieldPath}": "${issue.currentValue}"\n\n`;
    } else {
      message += `"${issue.fieldPath}": ${JSON.stringify(issue.currentValue, null, 2)}\n\n`;
    }

    message += `Fixed (correct):\n`;
    if (typeof issue.correctedValue === 'string') {
      message += `"${issue.fieldPath}": "${issue.correctedValue}"`;
    } else {
      message += `"${issue.fieldPath}": ${JSON.stringify(issue.correctedValue, null, 2)}`;
    }

    return message;
  }
}