-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgetFlowValidation.ts
More file actions
112 lines (95 loc) · 4.27 KB
/
Copy pathgetFlowValidation.ts
File metadata and controls
112 lines (95 loc) · 4.27 KB
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
import {flattenDiagnosticMessageText} from "typescript";
import {DataType, Flow, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types";
import {createCompilerHost, generateFlowSourceCode, ValidationResult} from "../utils";
/**
* Validates a flow by generating virtual TypeScript code and running it through the TS compiler.
*/
export const getFlowValidation = (
flow?: Flow,
functions?: FunctionDefinition[],
dataTypes?: DataType[]
): ValidationResult => {
if (!flow?.startingNodeId) {
return {
isValid: false,
returnType: "void",
diagnostics: [{
nodeId: null,
parameterIndex: null,
code: 0,
message: "You need to provide a starting node to be able to execute this flow.",
severity: "error",
}]
}
}
if (!flow.nodes?.nodes?.find(n => n?.id == flow.startingNodeId)) {
return {
isValid: false,
returnType: "void",
diagnostics: [{
nodeId: null,
parameterIndex: null,
code: 0,
message: "The starting node is not linked within the flow. Please make sure the starting node is connected to the rest of the flow.",
severity: "error",
}]
}
}
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes);
// 3. Virtual TypeScript Compilation
const fileName = "index.ts";
const host = createCompilerHost(fileName, sourceCode);
const sourceFile = host.getSourceFile(fileName)!;
const program = host.languageService.getProgram()!;
const diagnostics = program.getSemanticDiagnostics(sourceFile);
const errors = diagnostics.map(d => {
const message = flattenDiagnosticMessageText(d.messageText, "\n");
// "Argument of type 'undefined' is not assignable to parameter of type 'number'."
// We ignore this in flow validation too because we might generate code for incomplete flows.
let nodeId: NodeFunction['id'] | undefined;
let parameterIndex: number | null = null;
if (d.start !== undefined) {
const fullText = sourceFile.getFullText();
// Search for position marker comment near the error location
// The error position is typically the start of the problematic token (e.g., "undefined")
const searchStart = Math.max(0, d.start - 300);
const searchEnd = Math.min(fullText.length, d.start);
const searchText = fullText.substring(searchStart, searchEnd);
// Find all @pos comments in the search range
const posRegex = /\/\* @pos ([^ ]+) (\d+|null) \*\//g;
let match;
let closestMatch: RegExpExecArray | null = null;
let closestCommentEnd = -1;
// Collect all matches and find the one whose end is closest to d.start
// We want the comment that is immediately before the error
while ((match = posRegex.exec(searchText)) !== null) {
const commentStart = searchStart + match.index;
const commentEnd = commentStart + match[0].length;
// Only consider comments that end before or very close to the error start
// This ensures we get the @pos comment that directly precedes the problematic argument
if (commentEnd <= d.start!) {
if (commentEnd > closestCommentEnd) {
closestCommentEnd = commentEnd;
closestMatch = match;
}
}
}
if (closestMatch) {
nodeId = closestMatch[1] === "null" ? null : closestMatch[1] as NodeFunction['id'];
parameterIndex = parseInt(closestMatch[2], 10);
}
}
return {
message,
code: d.code,
severity: "error" as const,
nodeId,
parameterIndex: typeof parameterIndex == "number" && Number.isSafeInteger(parameterIndex) ? parameterIndex : null,
};
}).filter((e) => e !== null);
return {
isValid: !errors.some(e => e?.severity === "error"),
returnType: "void",
diagnostics: errors,
};
};