Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/extraction/getTypeFromValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions src/extraction/getTypeVariant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
12 changes: 6 additions & 6 deletions src/extraction/getTypesFromNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(", ");
Expand Down
4 changes: 2 additions & 2 deletions src/extraction/getValueFromType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand Down
119 changes: 59 additions & 60 deletions src/suggestion/getNodeSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Fu${i}${getGenericsCount(f.signature!) > 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<typeof Fu${i}${getGenericsCount(f.signature!) > 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<number>();
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<number>();
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) ?? [];
}
18 changes: 11 additions & 7 deletions src/suggestion/getReferenceSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand Down Expand Up @@ -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;
Expand Down
40 changes: 20 additions & 20 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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") {
Expand All @@ -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 =>
Expand All @@ -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<string>()): boolean => {
const currentId = currentNode.id;
const isNodeReachable = (flow?: Flow, currentNode?: NodeFunction, targetId?: string, visited = new Set<string>()): 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!);
Expand All @@ -226,28 +226,28 @@ 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!);
}

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}.`
};
}

Expand Down
12 changes: 6 additions & 6 deletions src/validation/getFlowValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
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.
Expand Down Expand Up @@ -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');

Expand Down
Loading