diff --git a/src/extraction/getTypeFromValue.ts b/src/extraction/getTypeFromValue.ts index b151625..42af967 100644 --- a/src/extraction/getTypeFromValue.ts +++ b/src/extraction/getTypeFromValue.ts @@ -5,7 +5,9 @@ import {LiteralValue} from "@code0-tech/sagittarius-graphql-types"; /** * Uses the TypeScript compiler to generate a precise type string from any runtime value. */ -export const getTypeFromValue = (value: LiteralValue | null): string => { +export const getTypeFromValue = ( + value?: LiteralValue | null +): string => { // 1. Serialize value to a JSON string for embedding in source code. const literal = JSON.stringify(value?.value); diff --git a/src/extraction/getTypeVariant.ts b/src/extraction/getTypeVariant.ts index ace6e99..9efabe7 100644 --- a/src/extraction/getTypeVariant.ts +++ b/src/extraction/getTypeVariant.ts @@ -13,8 +13,8 @@ export enum DataTypeVariant { * Determines the variant of a given TypeScript type string using the TS compiler. */ export const getTypeVariant = ( - type: string, - dataTypes: DataType[] + type?: string, + dataTypes?: DataType[] ): DataTypeVariant => { const typeDefs = getSharedTypeDeclarations(dataTypes); diff --git a/src/extraction/getTypesFromNode.ts b/src/extraction/getTypesFromNode.ts index a68d30a..3797f7e 100644 --- a/src/extraction/getTypesFromNode.ts +++ b/src/extraction/getTypesFromNode.ts @@ -12,12 +12,12 @@ export interface NodeTypes { * Resolves the types of the parameters and the return type of a NodeFunction. */ export const getTypesFromNode = ( - node: NodeFunction, - functions: FunctionDefinition[], - dataTypes: DataType[] + node?: NodeFunction, + functions?: FunctionDefinition[], + dataTypes?: DataType[] ): NodeTypes => { - const funcMap = new Map(functions.map(f => [f.identifier, f])); - const funcDef = funcMap.get(node.functionDefinition?.identifier); + const funcMap = new Map(functions?.map(f => [f.identifier, f])); + const funcDef = funcMap.get(node?.functionDefinition?.identifier); if (!funcDef) { return { @@ -31,7 +31,7 @@ export const getTypesFromNode = ( nodes: {__typename: "NodeFunctionConnection", nodes: [node]} } as Flow; - const params = (node.parameters?.nodes as NodeParameter[]) || []; + const params = (node?.parameters?.nodes as NodeParameter[]) || []; const paramCodes = params.map(param => getParameterCode(param, mockFlow, (f, n) => getNodeValidation(f, n, functions, dataTypes))); const funcCallArgs = paramCodes.map(code => code === 'undefined' ? '({} as any)' : code).join(", "); diff --git a/src/extraction/getValueFromType.ts b/src/extraction/getValueFromType.ts index d896103..9c31ed1 100644 --- a/src/extraction/getValueFromType.ts +++ b/src/extraction/getValueFromType.ts @@ -6,8 +6,8 @@ import {createCompilerHost, getSharedTypeDeclarations} from "../utils"; * Generates a sample LiteralValue from a TypeScript type string. */ export const getValueFromType = ( - type: string, - dataTypes: DataType[] + type?: string, + dataTypes?: DataType[] ): LiteralValue => { // 1. Prepare declarations. const sourceCode = ` diff --git a/src/suggestion/getNodeSuggestions.ts b/src/suggestion/getNodeSuggestions.ts index 72d1f30..baf3724 100644 --- a/src/suggestion/getNodeSuggestions.ts +++ b/src/suggestion/getNodeSuggestions.ts @@ -5,77 +5,76 @@ import {createCompilerHost, getSharedTypeDeclarations} from "../utils"; * Suggests NodeFunctions based on a given type and a list of available FunctionDefinitions. * Returns functions whose return type is compatible with the target type. */ -export function getNodeSuggestions(type: string, functions: FunctionDefinition[], dataTypes: DataType[]): NodeFunction[] { - if (!type || !functions || functions.length === 0) { - return []; - } +export const getNodeSuggestions = ( + type?: string, + functions?: FunctionDefinition[], + dataTypes?: DataType[] +): NodeFunction[] => { - function getGenericsCount(input: string): number { - const match = input.match(/<([^>]+)>/); - if (!match) return 0; - return match[1].split(',').map(s => s.trim()).filter(Boolean).length; - } + let functionToSuggest = functions - const sharedTypes = getSharedTypeDeclarations(dataTypes); - const sourceCode = ` - ${sharedTypes} - type TargetType = ${type}; - ${functions.map((f, i) => { + if (type && functions) { + function getGenericsCount(input: string): number { + const match = input.match(/<([^>]+)>/); + if (!match) return 0; + return match[1].split(',').map(s => s.trim()).filter(Boolean).length; + } - return ` - declare function Fu${i}${f.signature}; - type F${i} = ReturnType 0 ? `<${Array(getGenericsCount(f.signature!)).fill("any").join(", ")}>` : ""}>; - `; - }).join("\n")} - ${functions.map((_, i) => `const check${i}: TargetType = {} as F${i};`).join("\n")} + const sourceCode = ` + ${getSharedTypeDeclarations(dataTypes)} + type TargetType = ${type}; + ${functions?.map((f, i) => { + return ` + declare function Fu${i}${f.signature}; + type F${i} = ReturnType 0 ? `<${Array(getGenericsCount(f.signature!)).fill("any").join(", ")}>` : ""}>; + `; + }).join("\n")} + ${functions?.map((_, i) => `const check${i}: TargetType = {} as F${i};`).join("\n")} `; - const fileName = "index.ts"; - const host = createCompilerHost(fileName, sourceCode); - const sourceFile = host.getSourceFile(fileName)!; - const program = host.languageService.getProgram()!; + const fileName = "index.ts"; + const host = createCompilerHost(fileName, sourceCode); + const sourceFile = host.getSourceFile(fileName)!; + const program = host.languageService.getProgram()!; - const diagnostics = program.getSemanticDiagnostics(); - const errorLines = new Set(); - diagnostics.forEach(diag => { - if (diag.file === sourceFile && diag.start !== undefined) { - errorLines.add(sourceFile.getLineAndCharacterOfPosition(diag.start).line); - } - }); + const diagnostics = program.getSemanticDiagnostics(); + const errorLines = new Set(); + diagnostics.forEach(diag => { + if (diag.file === sourceFile && diag.start !== undefined) { + errorLines.add(sourceFile.getLineAndCharacterOfPosition(diag.start).line); + } + }); - return functions - .map((f, i) => { - // Find the line number of 'const check${i}' + functionToSuggest = functions.filter((_, i) => { const lineToMatch = `const check${i}: TargetType = {} as F${i};`; const lines = sourceCode.split("\n"); const actualLine = lines.findIndex(l => l.includes(lineToMatch)); + return actualLine !== -1 && !errorLines.has(actualLine); + }); + } - if (actualLine !== -1 && errorLines.has(actualLine)) { - return null; + return functionToSuggest?.map(f=> { + return { + __typename: "NodeFunction", + id: `gid://sagittarius/NodeFunction/1`, + functionDefinition: { + __typename: "FunctionDefinition", + id: f.identifier as any, + identifier: f.identifier, + }, + parameters: { + __typename: "NodeParameterConnection", + nodes: (f.parameterDefinitions?.nodes || []).map(p => ({ + __typename: "NodeParameter", + parameterDefinition: { + __typename: "ParameterDefinition", + id: p?.identifier as any, + identifier: p?.identifier + }, + value: null + })) } - - return { - __typename: "NodeFunction", - id: `gid://sagittarius/NodeFunction/1`, - functionDefinition: { - __typename: "FunctionDefinition", - id: f.identifier as any, - identifier: f.identifier, - }, - parameters: { - __typename: "NodeParameterConnection", - nodes: (f.parameterDefinitions?.nodes || []).map(p => ({ - __typename: "NodeParameter", - parameterDefinition: { - __typename: "ParameterDefinition", - id: p?.identifier as any, - identifier: p?.identifier - }, - value: null - })) - } - } as any as NodeFunction; - }) - .filter((f): f is NodeFunction => f !== null); + } as any as NodeFunction; + }).filter((f): f is NodeFunction => f !== null) ?? []; } diff --git a/src/suggestion/getReferenceSuggestions.ts b/src/suggestion/getReferenceSuggestions.ts index 560b260..50132b1 100644 --- a/src/suggestion/getReferenceSuggestions.ts +++ b/src/suggestion/getReferenceSuggestions.ts @@ -15,14 +15,18 @@ import {getNodeValidation} from "../validation/getNodeValidation"; * and filters them by a required type. */ export const getReferenceSuggestions = ( - flow: Flow, - nodeId: NodeFunction['id'], - type: string, - functions: FunctionDefinition[], - dataTypes: DataType[] + flow?: Flow, + nodeId?: NodeFunction['id'], + type: string = "any", + functions?: FunctionDefinition[], + dataTypes?: DataType[] ): ReferenceValue[] => { + + if (!flow) return [] + if (!nodeId) return [] + const suggestions: ReferenceValue[] = []; - const nodes = flow.nodes?.nodes || []; + const nodes = flow?.nodes?.nodes || []; const targetNode = nodes.find(n => n?.id === nodeId); if (!targetNode) return []; @@ -152,7 +156,7 @@ export const getReferenceSuggestions = ( // 3. Inputs of parent nodes (Scopes) let currentParent = getParentScopeNode(flow, nodeId!); while (currentParent) { - const funcDef = functions.find(f => f.identifier === currentParent!.functionDefinition?.identifier); + const funcDef = functions?.find(f => f.identifier === currentParent!.functionDefinition?.identifier); if (funcDef) { const paramIndex = currentParent.parameters?.nodes?.findIndex(p => { const val = p?.value; diff --git a/src/utils.ts b/src/utils.ts index 9e5486c..0b64a5a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -107,8 +107,8 @@ export function getTypeFromReferencePath(value: any, referencePath: ReferencePat /** * Helper to find a node by ID within the flow structure. */ -function findNodeById(flow: Flow, nodeId: string): NodeFunction | undefined { - const nodes = flow.nodes; +function findNodeById(flow?: Flow, nodeId?: string): NodeFunction | undefined { + const nodes = flow?.nodes; if (!nodes) return undefined; if (Array.isArray(nodes)) { @@ -122,9 +122,9 @@ function findNodeById(flow: Flow, nodeId: string): NodeFunction | undefined { * Extracts and returns the TypeScript code representation for a NodeParameter. */ export function getParameterCode( - param: NodeParameter, - flow: Flow, - getNodeValidation: (flow: Flow, node: NodeFunction) => ValidationResult + param?: NodeParameter, + flow?: Flow, + getNodeValidation?: (flow?: Flow, node?: NodeFunction) => ValidationResult ): string { const value = param?.value; if (!value) return 'undefined'; @@ -135,7 +135,7 @@ export function getParameterCode( if (!refNode) return 'undefined'; - let refType = getNodeValidation(flow, refNode).returnType; + let refType = getNodeValidation?.(flow, refNode).returnType; if (refValue.referencePath && refValue.referencePath.length > 0) { let refVal: any = undefined; @@ -169,8 +169,8 @@ export function getParameterCode( const returnNode = findReturnNode(refNode); if (!returnNode) return '(() => undefined)'; - const validation = getNodeValidation(flow, returnNode); - return `(() => ({} as ${validation.returnType}))`; + const validation = getNodeValidation?.(flow, returnNode); + return `(() => ({} as ${validation?.returnType}))`; } if (value.__typename === "LiteralValue") { @@ -183,8 +183,8 @@ export function getParameterCode( /** * Finds the parent node that initiated this sub-tree via a NodeFunctionIdWrapper. */ -const getParentScopeNode = (flow: Flow, currentNodeId: string): NodeFunction | undefined => { - const nodes = flow.nodes?.nodes; +const getParentScopeNode = (flow?: Flow, currentNodeId?: string): NodeFunction | undefined => { + const nodes = flow?.nodes?.nodes; if (!nodes) return undefined; return nodes.find(n => @@ -197,14 +197,14 @@ const getParentScopeNode = (flow: Flow, currentNodeId: string): NodeFunction | u /** * Checks if a target node is reachable (executed before) the current node. */ -const isNodeReachable = (flow: Flow, currentNode: NodeFunction, targetId: string, visited = new Set()): boolean => { - const currentId = currentNode.id; +const isNodeReachable = (flow?: Flow, currentNode?: NodeFunction, targetId?: string, visited = new Set()): boolean => { + const currentId = currentNode?.id; if (!currentId || visited.has(currentId)) return false; visited.add(currentId); // Scenario 1: Is the node a predecessor in the same execution chain? const isPredecessor = (startId: string): boolean => { - const pred = flow.nodes?.nodes?.find(n => n?.nextNodeId === startId); + const pred = flow?.nodes?.nodes?.find(n => n?.nextNodeId === startId); if (!pred) return false; if (pred.id === targetId) return true; return isPredecessor(pred.id!); @@ -226,20 +226,20 @@ const isNodeReachable = (flow: Flow, currentNode: NodeFunction, targetId: string * Validates if a reference is accessible from the current node's scope. */ export const validateReference = ( - flow: Flow, - currentNode: NodeFunction, - ref: ReferenceValue + flow?: Flow, + currentNode?: NodeFunction, + ref?: ReferenceValue ): { isValid: boolean, error?: string } => { // Scenario 3: Global flow input - if (!ref.nodeFunctionId) { + if (!ref?.nodeFunctionId) { return {isValid: true}; } // Scenario 2: Parameter input reference (e.g., "item" in CONSUMER) if (ref.parameterIndex !== undefined && ref.inputIndex !== undefined) { - if (currentNode.id === ref.nodeFunctionId) return {isValid: true}; + if (currentNode?.id === ref.nodeFunctionId) return {isValid: true}; - let tempParent = getParentScopeNode(flow, currentNode.id!); + let tempParent = getParentScopeNode(flow, currentNode?.id!); while (tempParent) { if (tempParent.id === ref.nodeFunctionId) return {isValid: true}; tempParent = getParentScopeNode(flow, tempParent.id!); @@ -247,7 +247,7 @@ export const validateReference = ( return { isValid: false, - error: `Invalid input reference: Node ${currentNode.id} is not in the scope of Node ${ref.nodeFunctionId}.` + error: `Invalid input reference: Node ${currentNode?.id} is not in the scope of Node ${ref.nodeFunctionId}.` }; } diff --git a/src/validation/getFlowValidation.ts b/src/validation/getFlowValidation.ts index 34bb89e..2af2cd3 100644 --- a/src/validation/getFlowValidation.ts +++ b/src/validation/getFlowValidation.ts @@ -16,14 +16,14 @@ const sanitizeId = (id: string) => id.replace(/[^a-zA-Z0-9]/g, '_'); * Validates a flow by generating virtual TypeScript code and running it through the TS compiler. */ export const getFlowValidation = ( - flow: Flow, - functions: FunctionDefinition[], - dataTypes: DataType[] + flow?: Flow, + functions?: FunctionDefinition[], + dataTypes?: DataType[] ): ValidationResult => { const visited = new Set(); - const nodes = flow.nodes?.nodes || []; + const nodes = flow?.nodes?.nodes || []; - const funcMap = new Map(functions.map(f => [f.identifier, f])); + const funcMap = new Map(functions?.map(f => [f.identifier, f])); /** * Recursive function to generate TypeScript code for a node and its execution path. @@ -94,7 +94,7 @@ export const getFlowValidation = ( // 1. Generate Declarations const typeDefs = getSharedTypeDeclarations(dataTypes); - const funcDeclarations = functions.map(funcDef => { + const funcDeclarations = functions?.map(funcDef => { return `declare function fn_${funcDef.identifier?.replace(/::/g, '_')}${funcDef.signature}`; }).join('\n'); diff --git a/src/validation/getNodeValidation.ts b/src/validation/getNodeValidation.ts index 961092c..acb0f9e 100644 --- a/src/validation/getNodeValidation.ts +++ b/src/validation/getNodeValidation.ts @@ -19,22 +19,22 @@ import { * Validates a single node's parameters and scope, then infers its return type. */ export const getNodeValidation = ( - flow: Flow, - node: NodeFunction, - functions: FunctionDefinition[], - dataTypes: DataType[] + flow?: Flow, + node?: NodeFunction, + functions?: FunctionDefinition[], + dataTypes?: DataType[] ): ValidationResult => { - const funcMap = new Map(functions.map(f => [f.identifier, f])); - const funcDef = funcMap.get(node.functionDefinition?.identifier); + const funcMap = new Map(functions?.map(f => [f.identifier, f])); + const funcDef = funcMap.get(node?.functionDefinition?.identifier); if (!funcDef) { return { isValid: false, returnType: "any", - diagnostics: [{message: `Function ${node.id} not found`, nodeId: node.id, code: 404, severity: "error"}], + diagnostics: [{message: `Function ${node?.id} not found`, nodeId: node?.id, code: 404, severity: "error"}], }; } - const params = (node.parameters?.nodes as NodeParameter[]) || []; + const params = (node?.parameters?.nodes as NodeParameter[]) || []; const scopeErrors: ValidationResult["diagnostics"] = []; // 1. Parameter scope validation @@ -46,7 +46,7 @@ export const getNodeValidation = ( scopeErrors.push({ message: validation.error || "Scope error", code: 403, - nodeId: node.id, + nodeId: node?.id, parameterIndex: params.indexOf(param), severity: "error" }); @@ -116,7 +116,7 @@ export const getNodeValidation = ( return { message, code: d.code, - nodeId: node.id, + nodeId: node?.id, parameterIndex: (() => { if (d.start !== undefined) { const argIndex = params.findIndex((_, i) => { diff --git a/src/validation/getValueValidation.ts b/src/validation/getValueValidation.ts index 4a6ad61..58953f5 100644 --- a/src/validation/getValueValidation.ts +++ b/src/validation/getValueValidation.ts @@ -3,11 +3,11 @@ import {DataType, LiteralValue} from "@code0-tech/sagittarius-graphql-types"; import {createCompilerHost, getSharedTypeDeclarations, ValidationResult} from "../utils"; export const getValueValidation = ( - type: string, - value: LiteralValue, - dataTypes: DataType[] + type?: string, + value?: LiteralValue, + dataTypes?: DataType[] ): ValidationResult => { - const valueAsCode = JSON.stringify(value.value); + const valueAsCode = JSON.stringify(value?.value); // 1. Construct the source code for validation. const sourceCode = `