Skip to content

Commit ea4fbab

Browse files
author
nicosammito
committed
feat: make parameters optional in flow validation and suggestion functions for improved flexibility
1 parent 8e54747 commit ea4fbab

File tree

10 files changed

+123
-118
lines changed

10 files changed

+123
-118
lines changed

src/extraction/getTypeFromValue.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {LiteralValue} from "@code0-tech/sagittarius-graphql-types";
55
/**
66
* Uses the TypeScript compiler to generate a precise type string from any runtime value.
77
*/
8-
export const getTypeFromValue = (value: LiteralValue | null): string => {
8+
export const getTypeFromValue = (
9+
value?: LiteralValue | null
10+
): string => {
911
// 1. Serialize value to a JSON string for embedding in source code.
1012
const literal = JSON.stringify(value?.value);
1113

src/extraction/getTypeVariant.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export enum DataTypeVariant {
1313
* Determines the variant of a given TypeScript type string using the TS compiler.
1414
*/
1515
export const getTypeVariant = (
16-
type: string,
17-
dataTypes: DataType[]
16+
type?: string,
17+
dataTypes?: DataType[]
1818
): DataTypeVariant => {
1919
const typeDefs = getSharedTypeDeclarations(dataTypes);
2020

src/extraction/getTypesFromNode.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ export interface NodeTypes {
1212
* Resolves the types of the parameters and the return type of a NodeFunction.
1313
*/
1414
export const getTypesFromNode = (
15-
node: NodeFunction,
16-
functions: FunctionDefinition[],
17-
dataTypes: DataType[]
15+
node?: NodeFunction,
16+
functions?: FunctionDefinition[],
17+
dataTypes?: DataType[]
1818
): NodeTypes => {
19-
const funcMap = new Map(functions.map(f => [f.identifier, f]));
20-
const funcDef = funcMap.get(node.functionDefinition?.identifier);
19+
const funcMap = new Map(functions?.map(f => [f.identifier, f]));
20+
const funcDef = funcMap.get(node?.functionDefinition?.identifier);
2121

2222
if (!funcDef) {
2323
return {
@@ -31,7 +31,7 @@ export const getTypesFromNode = (
3131
nodes: {__typename: "NodeFunctionConnection", nodes: [node]}
3232
} as Flow;
3333

34-
const params = (node.parameters?.nodes as NodeParameter[]) || [];
34+
const params = (node?.parameters?.nodes as NodeParameter[]) || [];
3535
const paramCodes = params.map(param => getParameterCode(param, mockFlow, (f, n) => getNodeValidation(f, n, functions, dataTypes)));
3636

3737
const funcCallArgs = paramCodes.map(code => code === 'undefined' ? '({} as any)' : code).join(", ");

src/extraction/getValueFromType.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {createCompilerHost, getSharedTypeDeclarations} from "../utils";
66
* Generates a sample LiteralValue from a TypeScript type string.
77
*/
88
export const getValueFromType = (
9-
type: string,
10-
dataTypes: DataType[]
9+
type?: string,
10+
dataTypes?: DataType[]
1111
): LiteralValue => {
1212
// 1. Prepare declarations.
1313
const sourceCode = `

src/suggestion/getNodeSuggestions.ts

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,77 +5,76 @@ import {createCompilerHost, getSharedTypeDeclarations} from "../utils";
55
* Suggests NodeFunctions based on a given type and a list of available FunctionDefinitions.
66
* Returns functions whose return type is compatible with the target type.
77
*/
8-
export function getNodeSuggestions(type: string, functions: FunctionDefinition[], dataTypes: DataType[]): NodeFunction[] {
9-
if (!type || !functions || functions.length === 0) {
10-
return [];
11-
}
8+
export const getNodeSuggestions = (
9+
type?: string,
10+
functions?: FunctionDefinition[],
11+
dataTypes?: DataType[]
12+
): NodeFunction[] => {
1213

13-
function getGenericsCount(input: string): number {
14-
const match = input.match(/<([^>]+)>/);
15-
if (!match) return 0;
16-
return match[1].split(',').map(s => s.trim()).filter(Boolean).length;
17-
}
14+
let functionToSuggest = functions
1815

19-
const sharedTypes = getSharedTypeDeclarations(dataTypes);
20-
const sourceCode = `
21-
${sharedTypes}
22-
type TargetType = ${type};
23-
${functions.map((f, i) => {
16+
if (type && functions) {
17+
function getGenericsCount(input: string): number {
18+
const match = input.match(/<([^>]+)>/);
19+
if (!match) return 0;
20+
return match[1].split(',').map(s => s.trim()).filter(Boolean).length;
21+
}
2422

25-
return `
26-
declare function Fu${i}${f.signature};
27-
type F${i} = ReturnType<typeof Fu${i}${getGenericsCount(f.signature!) > 0 ? `<${Array(getGenericsCount(f.signature!)).fill("any").join(", ")}>` : ""}>;
28-
`;
29-
}).join("\n")}
30-
${functions.map((_, i) => `const check${i}: TargetType = {} as F${i};`).join("\n")}
23+
const sourceCode = `
24+
${getSharedTypeDeclarations(dataTypes)}
25+
type TargetType = ${type};
26+
${functions?.map((f, i) => {
27+
return `
28+
declare function Fu${i}${f.signature};
29+
type F${i} = ReturnType<typeof Fu${i}${getGenericsCount(f.signature!) > 0 ? `<${Array(getGenericsCount(f.signature!)).fill("any").join(", ")}>` : ""}>;
30+
`;
31+
}).join("\n")}
32+
${functions?.map((_, i) => `const check${i}: TargetType = {} as F${i};`).join("\n")}
3133
`;
3234

33-
const fileName = "index.ts";
34-
const host = createCompilerHost(fileName, sourceCode);
35-
const sourceFile = host.getSourceFile(fileName)!;
36-
const program = host.languageService.getProgram()!;
35+
const fileName = "index.ts";
36+
const host = createCompilerHost(fileName, sourceCode);
37+
const sourceFile = host.getSourceFile(fileName)!;
38+
const program = host.languageService.getProgram()!;
3739

38-
const diagnostics = program.getSemanticDiagnostics();
39-
const errorLines = new Set<number>();
40-
diagnostics.forEach(diag => {
41-
if (diag.file === sourceFile && diag.start !== undefined) {
42-
errorLines.add(sourceFile.getLineAndCharacterOfPosition(diag.start).line);
43-
}
44-
});
40+
const diagnostics = program.getSemanticDiagnostics();
41+
const errorLines = new Set<number>();
42+
diagnostics.forEach(diag => {
43+
if (diag.file === sourceFile && diag.start !== undefined) {
44+
errorLines.add(sourceFile.getLineAndCharacterOfPosition(diag.start).line);
45+
}
46+
});
4547

46-
return functions
47-
.map((f, i) => {
48-
// Find the line number of 'const check${i}'
48+
functionToSuggest = functions.filter((_, i) => {
4949
const lineToMatch = `const check${i}: TargetType = {} as F${i};`;
5050
const lines = sourceCode.split("\n");
5151
const actualLine = lines.findIndex(l => l.includes(lineToMatch));
52+
return actualLine !== -1 && !errorLines.has(actualLine);
53+
});
54+
}
5255

5356

54-
if (actualLine !== -1 && errorLines.has(actualLine)) {
55-
return null;
57+
return functionToSuggest?.map(f=> {
58+
return {
59+
__typename: "NodeFunction",
60+
id: `gid://sagittarius/NodeFunction/1`,
61+
functionDefinition: {
62+
__typename: "FunctionDefinition",
63+
id: f.identifier as any,
64+
identifier: f.identifier,
65+
},
66+
parameters: {
67+
__typename: "NodeParameterConnection",
68+
nodes: (f.parameterDefinitions?.nodes || []).map(p => ({
69+
__typename: "NodeParameter",
70+
parameterDefinition: {
71+
__typename: "ParameterDefinition",
72+
id: p?.identifier as any,
73+
identifier: p?.identifier
74+
},
75+
value: null
76+
}))
5677
}
57-
58-
return {
59-
__typename: "NodeFunction",
60-
id: `gid://sagittarius/NodeFunction/1`,
61-
functionDefinition: {
62-
__typename: "FunctionDefinition",
63-
id: f.identifier as any,
64-
identifier: f.identifier,
65-
},
66-
parameters: {
67-
__typename: "NodeParameterConnection",
68-
nodes: (f.parameterDefinitions?.nodes || []).map(p => ({
69-
__typename: "NodeParameter",
70-
parameterDefinition: {
71-
__typename: "ParameterDefinition",
72-
id: p?.identifier as any,
73-
identifier: p?.identifier
74-
},
75-
value: null
76-
}))
77-
}
78-
} as any as NodeFunction;
79-
})
80-
.filter((f): f is NodeFunction => f !== null);
78+
} as any as NodeFunction;
79+
}).filter((f): f is NodeFunction => f !== null) ?? [];
8180
}

src/suggestion/getReferenceSuggestions.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ import {getNodeValidation} from "../validation/getNodeValidation";
1515
* and filters them by a required type.
1616
*/
1717
export const getReferenceSuggestions = (
18-
flow: Flow,
19-
nodeId: NodeFunction['id'],
20-
type: string,
21-
functions: FunctionDefinition[],
22-
dataTypes: DataType[]
18+
flow?: Flow,
19+
nodeId?: NodeFunction['id'],
20+
type: string = "any",
21+
functions?: FunctionDefinition[],
22+
dataTypes?: DataType[]
2323
): ReferenceValue[] => {
24+
25+
if (!flow) return []
26+
if (!nodeId) return []
27+
2428
const suggestions: ReferenceValue[] = [];
25-
const nodes = flow.nodes?.nodes || [];
29+
const nodes = flow?.nodes?.nodes || [];
2630
const targetNode = nodes.find(n => n?.id === nodeId);
2731

2832
if (!targetNode) return [];
@@ -152,7 +156,7 @@ export const getReferenceSuggestions = (
152156
// 3. Inputs of parent nodes (Scopes)
153157
let currentParent = getParentScopeNode(flow, nodeId!);
154158
while (currentParent) {
155-
const funcDef = functions.find(f => f.identifier === currentParent!.functionDefinition?.identifier);
159+
const funcDef = functions?.find(f => f.identifier === currentParent!.functionDefinition?.identifier);
156160
if (funcDef) {
157161
const paramIndex = currentParent.parameters?.nodes?.findIndex(p => {
158162
const val = p?.value;

src/utils.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ export function getTypeFromReferencePath(value: any, referencePath: ReferencePat
107107
/**
108108
* Helper to find a node by ID within the flow structure.
109109
*/
110-
function findNodeById(flow: Flow, nodeId: string): NodeFunction | undefined {
111-
const nodes = flow.nodes;
110+
function findNodeById(flow?: Flow, nodeId?: string): NodeFunction | undefined {
111+
const nodes = flow?.nodes;
112112
if (!nodes) return undefined;
113113

114114
if (Array.isArray(nodes)) {
@@ -122,9 +122,9 @@ function findNodeById(flow: Flow, nodeId: string): NodeFunction | undefined {
122122
* Extracts and returns the TypeScript code representation for a NodeParameter.
123123
*/
124124
export function getParameterCode(
125-
param: NodeParameter,
126-
flow: Flow,
127-
getNodeValidation: (flow: Flow, node: NodeFunction) => ValidationResult
125+
param?: NodeParameter,
126+
flow?: Flow,
127+
getNodeValidation?: (flow?: Flow, node?: NodeFunction) => ValidationResult
128128
): string {
129129
const value = param?.value;
130130
if (!value) return 'undefined';
@@ -135,7 +135,7 @@ export function getParameterCode(
135135

136136
if (!refNode) return 'undefined';
137137

138-
let refType = getNodeValidation(flow, refNode).returnType;
138+
let refType = getNodeValidation?.(flow, refNode).returnType;
139139

140140
if (refValue.referencePath && refValue.referencePath.length > 0) {
141141
let refVal: any = undefined;
@@ -169,8 +169,8 @@ export function getParameterCode(
169169
const returnNode = findReturnNode(refNode);
170170
if (!returnNode) return '(() => undefined)';
171171

172-
const validation = getNodeValidation(flow, returnNode);
173-
return `(() => ({} as ${validation.returnType}))`;
172+
const validation = getNodeValidation?.(flow, returnNode);
173+
return `(() => ({} as ${validation?.returnType}))`;
174174
}
175175

176176
if (value.__typename === "LiteralValue") {
@@ -183,8 +183,8 @@ export function getParameterCode(
183183
/**
184184
* Finds the parent node that initiated this sub-tree via a NodeFunctionIdWrapper.
185185
*/
186-
const getParentScopeNode = (flow: Flow, currentNodeId: string): NodeFunction | undefined => {
187-
const nodes = flow.nodes?.nodes;
186+
const getParentScopeNode = (flow?: Flow, currentNodeId?: string): NodeFunction | undefined => {
187+
const nodes = flow?.nodes?.nodes;
188188
if (!nodes) return undefined;
189189

190190
return nodes.find(n =>
@@ -197,14 +197,14 @@ const getParentScopeNode = (flow: Flow, currentNodeId: string): NodeFunction | u
197197
/**
198198
* Checks if a target node is reachable (executed before) the current node.
199199
*/
200-
const isNodeReachable = (flow: Flow, currentNode: NodeFunction, targetId: string, visited = new Set<string>()): boolean => {
201-
const currentId = currentNode.id;
200+
const isNodeReachable = (flow?: Flow, currentNode?: NodeFunction, targetId?: string, visited = new Set<string>()): boolean => {
201+
const currentId = currentNode?.id;
202202
if (!currentId || visited.has(currentId)) return false;
203203
visited.add(currentId);
204204

205205
// Scenario 1: Is the node a predecessor in the same execution chain?
206206
const isPredecessor = (startId: string): boolean => {
207-
const pred = flow.nodes?.nodes?.find(n => n?.nextNodeId === startId);
207+
const pred = flow?.nodes?.nodes?.find(n => n?.nextNodeId === startId);
208208
if (!pred) return false;
209209
if (pred.id === targetId) return true;
210210
return isPredecessor(pred.id!);
@@ -226,28 +226,28 @@ const isNodeReachable = (flow: Flow, currentNode: NodeFunction, targetId: string
226226
* Validates if a reference is accessible from the current node's scope.
227227
*/
228228
export const validateReference = (
229-
flow: Flow,
230-
currentNode: NodeFunction,
231-
ref: ReferenceValue
229+
flow?: Flow,
230+
currentNode?: NodeFunction,
231+
ref?: ReferenceValue
232232
): { isValid: boolean, error?: string } => {
233233
// Scenario 3: Global flow input
234-
if (!ref.nodeFunctionId) {
234+
if (!ref?.nodeFunctionId) {
235235
return {isValid: true};
236236
}
237237

238238
// Scenario 2: Parameter input reference (e.g., "item" in CONSUMER)
239239
if (ref.parameterIndex !== undefined && ref.inputIndex !== undefined) {
240-
if (currentNode.id === ref.nodeFunctionId) return {isValid: true};
240+
if (currentNode?.id === ref.nodeFunctionId) return {isValid: true};
241241

242-
let tempParent = getParentScopeNode(flow, currentNode.id!);
242+
let tempParent = getParentScopeNode(flow, currentNode?.id!);
243243
while (tempParent) {
244244
if (tempParent.id === ref.nodeFunctionId) return {isValid: true};
245245
tempParent = getParentScopeNode(flow, tempParent.id!);
246246
}
247247

248248
return {
249249
isValid: false,
250-
error: `Invalid input reference: Node ${currentNode.id} is not in the scope of Node ${ref.nodeFunctionId}.`
250+
error: `Invalid input reference: Node ${currentNode?.id} is not in the scope of Node ${ref.nodeFunctionId}.`
251251
};
252252
}
253253

src/validation/getFlowValidation.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ const sanitizeId = (id: string) => id.replace(/[^a-zA-Z0-9]/g, '_');
1616
* Validates a flow by generating virtual TypeScript code and running it through the TS compiler.
1717
*/
1818
export const getFlowValidation = (
19-
flow: Flow,
20-
functions: FunctionDefinition[],
21-
dataTypes: DataType[]
19+
flow?: Flow,
20+
functions?: FunctionDefinition[],
21+
dataTypes?: DataType[]
2222
): ValidationResult => {
2323
const visited = new Set<string>();
24-
const nodes = flow.nodes?.nodes || [];
24+
const nodes = flow?.nodes?.nodes || [];
2525

26-
const funcMap = new Map(functions.map(f => [f.identifier, f]));
26+
const funcMap = new Map(functions?.map(f => [f.identifier, f]));
2727

2828
/**
2929
* Recursive function to generate TypeScript code for a node and its execution path.
@@ -94,7 +94,7 @@ export const getFlowValidation = (
9494
// 1. Generate Declarations
9595
const typeDefs = getSharedTypeDeclarations(dataTypes);
9696

97-
const funcDeclarations = functions.map(funcDef => {
97+
const funcDeclarations = functions?.map(funcDef => {
9898
return `declare function fn_${funcDef.identifier?.replace(/::/g, '_')}${funcDef.signature}`;
9999
}).join('\n');
100100

0 commit comments

Comments
 (0)