From 9ffffa2d2479fc44b66b4be34945271cc9e33f86 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 25 Apr 2026 00:49:11 +0200 Subject: [PATCH 01/12] feat: implement getSchema function for enhanced flow schema generation --- src/suggestion/getSchema.ts | 307 +++++++++++++++++++++++++++ test/getReferenceSuggestions.test.ts | 7 +- 2 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 src/suggestion/getSchema.ts diff --git a/src/suggestion/getSchema.ts b/src/suggestion/getSchema.ts new file mode 100644 index 0000000..ab2454a --- /dev/null +++ b/src/suggestion/getSchema.ts @@ -0,0 +1,307 @@ +import { + DataType, + Flow, + FunctionDefinition, + LiteralValue, + NodeFunction, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types" +import {createCompilerHost, generateFlowSourceCode, sanitizeId} from "../utils" +import ts, {NumberLiteralType, StringLiteralType, Type} from "typescript" + +export interface TemporaryLiteralValue extends LiteralValue { + references?: Record +} + +interface Input { + input?: string + suggestions?: (NodeFunction | ReferenceValue | LiteralValue)[] +} + +interface GenericInput { + input?: "generic" +} + +interface SubFlowInput { + input?: "sub-flow" +} + +interface PrimitiveInput extends Input { + input?: "boolean" | "number" | "text" | "select" +} + +interface DataInput extends Input { + input?: "data" + properties?: Record + required?: string[] +} + +interface ListInput extends Input { + input?: "list" + items?: Schema | Schema[] +} + +interface TypeInput extends Input { + input?: "type" + properties?: Record + required?: string[] +} + +type Schema = PrimitiveInput | DataInput | ListInput | TypeInput | SubFlowInput | GenericInput + +interface ParameterSchema { + schema: Schema + blockedBy?: number[] +} + +export const getSchema = ( + flow: Flow, + dataTypes: DataType[], + functions: FunctionDefinition[], + nodeId?: NodeFunction['id'] +): ParameterSchema[] => { + + const sourceCode = generateFlowSourceCode(flow, functions, dataTypes) + + const fileName = "index.ts" + const host = createCompilerHost(fileName, sourceCode) + const sourceFile = host.getSourceFile(fileName)! + const program = host.languageService.getProgram()! + const checker = program.getTypeChecker() + + const node2 = flow.nodes?.nodes?.find(n => n?.id === nodeId) + const functionId = `fn_${node2?.functionDefinition?.identifier?.replace(/::/g, '_')}` + const realNodeId = `node_${sanitizeId(nodeId || "")}` + + const declaredFunctionsMap = new Map( + sourceFile.statements + .filter(ts.isFunctionDeclaration) + .map(node => [node.name!.getText(), node]) + ) + + const constantNames = new Map( + sourceFile.statements + .flatMap(node => { + const results: ts.VariableDeclaration[] = [] + + node.forEachChild(function visitor(child) { + if (ts.isVariableDeclaration(child)) { + if ((child.parent.flags & ts.NodeFlags.Const) !== 0) { + results.push(child) + } + } + child.forEachChild(visitor) + }) + + return results + }) + .map(decl => [decl.name.getText(), decl] as [string, ts.VariableDeclaration]) + ) + + const node = constantNames.get(realNodeId) + const funktion = declaredFunctionsMap.get(functionId) + + const nodeParameterTypes: Type[] | undefined = checker.getResolvedSignature(node?.initializer as ts.CallExpression)?.parameters.map(p => { + return checker.getTypeOfSymbolAtLocation(p, node?.initializer as ts.CallExpression) + }) + + const funktionParameterTypes: Type[] | undefined = funktion?.parameters?.map(p => { + const symbol = checker.getSymbolAtLocation(p.name) + return checker.getTypeOfSymbolAtLocation(symbol!, funktion) + }) + + const combinedParameterTypes: Type[] | undefined = funktionParameterTypes?.map((p, i) => { + const nodeType = nodeParameterTypes?.[i]; + if (!nodeType) return p; + + const pSymbol = p.getSymbol(); + const nodeSymbol = nodeType.getSymbol(); + + if (pSymbol && nodeSymbol && pSymbol === nodeSymbol) { + return nodeType; + } + + if (p.isTypeParameter()) { + const constraint = checker.getBaseConstraintOfType(p); + if (!constraint || checker.isTypeAssignableTo(nodeType, constraint)) { + return nodeType; + } + } + + if (checker.isTypeAssignableTo(nodeType, p)) { + return nodeType; + } + + return p; + }) + + const generateSchema = (type: ts.Type): Schema => { + + const literalValueSuggestions = getLiteralValueSuggestions(type) + const suggestions = { + suggestions: [...literalValueSuggestions] + } + + if (isPrimitiveLiteralUnion(type)) return {input: "select", ...suggestions} + + if (isBoolean(type)) return {input: "boolean", ...suggestions} + if (isNumber(type)) return {input: "number", ...suggestions} + if (isString(type)) return {input: "text", ...suggestions} + + if (isSubFlow(type)) return {input: "sub-flow", ...suggestions} + + if (isArrayType(checker, type)) { + const itemType = checker.getTypeArguments(type as ts.TypeReference)[0] + const itemTypes = itemType.isUnion() ? itemType.types : [itemType] + const itemSchemas = itemTypes.map(itemType => generateSchema(itemType)) + + return { + input: "list", + items: itemSchemas.length === 1 ? itemSchemas[0] : itemSchemas, + ...suggestions + } + } + + if ( + (type.flags & ts.TypeFlags.Object) !== 0 + ) { + const properties: Record = {} + const required: string[] = [] + + for (const property of checker.getPropertiesOfType(type)) { + const declaration = property.valueDeclaration ?? property.declarations?.[0] + + if (!declaration) continue + + const propertyType = checker.getTypeOfSymbolAtLocation(property, declaration) + + const isOptional = + (property.flags & ts.SymbolFlags.Optional) !== 0 || + ( + propertyType.isUnion() && + propertyType.types.some(t => (t.flags & ts.TypeFlags.Undefined) !== 0) + ) + + const propertyTypes = propertyType.isUnion() + ? propertyType.types.filter(t => (t.flags & ts.TypeFlags.Undefined) === 0) + : [propertyType] + + const propertySchemas = propertyTypes.map(generateSchema) + + properties[property.name] = + propertySchemas.length === 1 ? propertySchemas[0] : propertySchemas + + if (!isOptional) { + required.push(property.name) + } + } + + return { + input: "data", + properties, + required, + ...suggestions + } + } + + return { + input: "generic" + } + } + + const funktionDependencies = getParameterDependencies(funktion!) + + return combinedParameterTypes?.map((value, index) => { + return { + schema: generateSchema(value), + blockedBy: funktionDependencies + .filter(dep => dep.parameterIndex === index) + .map(dep => dep.dependsOnIndex) + } + }) || [] +} + +function getLiteralValueSuggestions(type: ts.Type): LiteralValue[] { + + if (type.isUnion()) { + return type.types.flatMap(getLiteralValueSuggestions) + } + + if (type.isStringLiteral()) return [{ + value: (type as StringLiteralType).value, + __typename: "LiteralValue" + }] + + if (type.isNumberLiteral()) return [{ + value: (type as NumberLiteralType).value.toString(), + }] + + if ((type as any).intrinsicName === "true") return [{ + value: "true", + __typename: "LiteralValue" + }] + + if ((type as any).intrinsicName === "false") return [{ + value: "true", + __typename: "LiteralValue" + }] + + return [] +} + +function isBoolean(type: ts.Type): boolean { + return ( + (type.flags & ts.TypeFlags.Boolean) !== 0 || + (type.flags & ts.TypeFlags.BooleanLiteral) !== 0 + ) +} + +function isSubFlow(type: ts.Type): boolean { + return ( + type.getCallSignatures().length > 0 + ) +} + +function isNumber(type: ts.Type): boolean { + return ( + (type.flags & ts.TypeFlags.Number) !== 0 || + (type.flags & ts.TypeFlags.NumberLiteral) !== 0 + ) +} + +function isString(type: ts.Type): boolean { + return ( + (type.flags & ts.TypeFlags.String) !== 0 || + (type.flags & ts.TypeFlags.StringLiteral) !== 0 + ) +} + +function isPrimitive(type: ts.Type): boolean { + return isString(type) || isNumber(type) || isBoolean(type) +} + +function isPrimitiveLiteralUnion(type: ts.Type): boolean { + if (!type.isUnion()) return false + return type.types.every(isPrimitive) +} + +function isArrayType(checker: ts.TypeChecker, type: ts.Type): boolean { + return checker.isArrayType(type) || checker.isTupleType(type) +} + +function getParameterDependencies(node: ts.FunctionDeclaration) { + const typeParamNames = node.typeParameters?.map(tp => tp.name.getText()) || []; + const usage: Record = {}; + + node.parameters.forEach((p, i) => { + const text = p.type?.getText() || ""; + typeParamNames.forEach(t => { + if (text.includes(t)) (usage[t] ??= []).push(i); + }); + }); + + return Object.values(usage) + .filter(indices => indices.length > 1) + .map(([first, ...rest]) => rest.map(idx => ({parameterIndex: idx, dependsOnIndex: first}))) + .flat(); +} \ No newline at end of file diff --git a/test/getReferenceSuggestions.test.ts b/test/getReferenceSuggestions.test.ts index 80d6037..d5ac761 100644 --- a/test/getReferenceSuggestions.test.ts +++ b/test/getReferenceSuggestions.test.ts @@ -2,6 +2,7 @@ import {describe, it} from 'vitest'; import {getReferenceSuggestions} from '../src/suggestion/getReferenceSuggestions'; import {Flow} from "@code0-tech/sagittarius-graphql-types"; import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data"; +import {getSchema} from "../src/suggestion/getSchema"; describe('getReferenceSuggestions', () => { it('sd', () => { @@ -75,7 +76,7 @@ describe('getReferenceSuggestions', () => { "identifier": "value" }, "value": null - } + }, ] } } @@ -123,9 +124,9 @@ describe('getReferenceSuggestions', () => { } }; - const suggestions = getReferenceSuggestions(flow, "gid://sagittarius/NodeFunction/4", 0, FUNCTION_SIGNATURES, DATA_TYPES); + const schema = getSchema(flow, DATA_TYPES, FUNCTION_SIGNATURES, "gid://sagittarius/NodeFunction/3"); - //expect(suggestions.some(s => !s.nodeFunctionId)).toBe(true); + console.dir(schema, { depth: null, colors: true }); }); it('2', () => { From 93a1ac8dbaef9710ac7bc8cb2b254ab5ad94e3c3 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sun, 26 Apr 2026 20:04:34 +0200 Subject: [PATCH 02/12] feat: enhance reference suggestion generation in getReferenceSuggestions and add node suggestion functionality --- src/suggestion/getSchema.ts | 233 ++++++++++++++++++++++++--- test/getReferenceSuggestions.test.ts | 133 +++++---------- 2 files changed, 247 insertions(+), 119 deletions(-) diff --git a/src/suggestion/getSchema.ts b/src/suggestion/getSchema.ts index ab2454a..8b7c9f2 100644 --- a/src/suggestion/getSchema.ts +++ b/src/suggestion/getSchema.ts @@ -3,16 +3,12 @@ import { Flow, FunctionDefinition, LiteralValue, - NodeFunction, + NodeFunction, ReferencePath, ReferenceValue } from "@code0-tech/sagittarius-graphql-types" import {createCompilerHost, generateFlowSourceCode, sanitizeId} from "../utils" import ts, {NumberLiteralType, StringLiteralType, Type} from "typescript" -export interface TemporaryLiteralValue extends LiteralValue { - references?: Record -} - interface Input { input?: string suggestions?: (NodeFunction | ReferenceValue | LiteralValue)[] @@ -107,39 +103,41 @@ export const getSchema = ( const funktionParameterTypes: Type[] | undefined = funktion?.parameters?.map(p => { const symbol = checker.getSymbolAtLocation(p.name) - return checker.getTypeOfSymbolAtLocation(symbol!, funktion) + return checker.getTypeOfSymbolAtLocation(symbol!, node?.initializer as ts.CallExpression) }) const combinedParameterTypes: Type[] | undefined = funktionParameterTypes?.map((p, i) => { - const nodeType = nodeParameterTypes?.[i]; - if (!nodeType) return p; + const nodeType = nodeParameterTypes?.[i] + if (!nodeType) return p - const pSymbol = p.getSymbol(); - const nodeSymbol = nodeType.getSymbol(); + const pSymbol = p.getSymbol() + const nodeSymbol = nodeType.getSymbol() if (pSymbol && nodeSymbol && pSymbol === nodeSymbol) { - return nodeType; + return nodeType } if (p.isTypeParameter()) { - const constraint = checker.getBaseConstraintOfType(p); + const constraint = checker.getBaseConstraintOfType(p) if (!constraint || checker.isTypeAssignableTo(nodeType, constraint)) { - return nodeType; + return nodeType } } if (checker.isTypeAssignableTo(nodeType, p)) { - return nodeType; + return nodeType } - return p; + return p }) const generateSchema = (type: ts.Type): Schema => { const literalValueSuggestions = getLiteralValueSuggestions(type) + const referenceSuggestions = getReferenceSuggestions(checker, node!, type, checker.getSymbolsInScope(node!, ts.SymbolFlags.Variable)) + const nodeSuggestions = getNodeSuggestions(checker, Array.from(declaredFunctionsMap.values()), functions, type) const suggestions = { - suggestions: [...literalValueSuggestions] + suggestions: [...literalValueSuggestions, ...referenceSuggestions, ...nodeSuggestions], } if (isPrimitiveLiteralUnion(type)) return {input: "select", ...suggestions} @@ -249,6 +247,156 @@ function getLiteralValueSuggestions(type: ts.Type): LiteralValue[] { return [] } +function getNodeSuggestions(checker: ts.TypeChecker, functionDeclarations: ts.FunctionDeclaration[], functions: FunctionDefinition[], paramType: ts.Type): NodeFunction[] { + + //TODO: if paramType is callable than we should parse in all functions that match the parameters + //TODO: otherwise we should only parse in functions that match the return type + //TODO: we should differentiate between inline usable suggestions and not + + return functionDeclarations.flatMap(func => { + + const signature = checker.getSignatureFromDeclaration(func) + const returnType = checker.getReturnTypeOfSignature(signature!) + + const simplifiedReturnType = returnType.isTypeParameter() + ? (checker.getBaseConstraintOfType(returnType) || checker.getAnyType()) + : returnType + + if (checker.isTypeAssignableTo(simplifiedReturnType, paramType)) { + const functionName = func.name?.getText().replace("fn_", "").replace("_", "::").replace("_", "::") + const funktion = functions.find(f => f.identifier === functionName) + + const node: NodeFunction = { + __typename: "NodeFunction", + id: `gid://sagittarius/NodeFunction/1`, + functionDefinition: { + __typename: "FunctionDefinition", + id: funktion?.id, + identifier: funktion?.identifier, + }, + parameters: { + __typename: "NodeParameterConnection", + nodes: (funktion?.parameterDefinitions?.nodes || []).map(p => ({ + __typename: "NodeParameter", + parameterDefinition: { + __typename: "ParameterDefinition", + id: p?.id, + identifier: p?.identifier + }, + value: p?.defaultValue ? { + __typename: "LiteralValue", + value: p.defaultValue.value + } : null + })) + } + } + + return node + + } + + return [] + + + }) + +} + +function getReferenceSuggestions(checker: ts.TypeChecker, node: ts.VariableDeclaration, paramType: ts.Type, symbols: ts.Symbol[]): ReferenceValue[] { + + return symbols.flatMap(symbol => { + const name = symbol.getName() + + if (!name.startsWith("node_") && !name.startsWith("p_") && !name.startsWith("flow_")) return [] + + const symbolDeclaration = symbol.getDeclarations()?.[0] + if (!symbolDeclaration) return [] + if (symbolDeclaration.getEnd() >= node.getEnd()!) return [] + + const symbolType = checker.getTypeOfSymbolAtLocation(symbol, node) + + if (name.startsWith("node_")) { + if (!((symbolType.flags & ts.TypeFlags.Void) !== 0)) { + + const nodeFunctionId = name + .replace("node_", "") + .replace(/___/g, "://") + .replace(/__/g, "/") + .replace(/_/g, "/") + + const propertyPaths = extractObjectProperties(symbolType, checker, paramType) + + return propertyPaths.flatMap(({path}) => { + const referenceValue: ReferenceValue = { + __typename: 'ReferenceValue', + nodeFunctionId: nodeFunctionId as any + } + + if (path.length > 0) referenceValue.referencePath = path + + return referenceValue + }) + + } + } else if (name.startsWith("p_")) { + + const idPart = name.replace("p_", "") + const lastUnderscoreIndex = idPart.lastIndexOf("_") + const rawId = idPart.substring(0, lastUnderscoreIndex) + const paramIndexFromName = parseInt(idPart.substring(lastUnderscoreIndex + 1), 10) + + const nodeFunctionId = rawId + .replace("p_", "") + .replace(/___/g, "://") + .replace(/__/g, "/") + .replace(/_/g, "/") + + if (checker.isTupleType(symbolType)) { + const typeReference = symbolType as ts.TypeReference + const typeArguments = checker.getTypeArguments(typeReference) + + return typeArguments.flatMap((tupleElementType, tupleIndex) => { + const propertyPaths = extractObjectProperties(tupleElementType, checker, paramType) + + return propertyPaths.flatMap(({ path }) => { + const referenceValue: ReferenceValue = { + __typename: 'ReferenceValue', + nodeFunctionId: nodeFunctionId as any, + parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName, + inputIndex: tupleIndex, + inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[tupleIndex].name.getText() + } + + if (path.length > 0) { + referenceValue.referencePath = path + } + + return referenceValue + }) + + }) + } + + } else if (name.startsWith("flow_")) { + const propertyPaths = extractObjectProperties(symbolType, checker, paramType) + + return propertyPaths.flatMap(({ path }) => { + const referenceValue: ReferenceValue = { + __typename: 'ReferenceValue', + nodeFunctionId: null + } + + if (path.length > 0) referenceValue.referencePath = path + + return referenceValue + }) + } + + return [] + }) + +} + function isBoolean(type: ts.Type): boolean { return ( (type.flags & ts.TypeFlags.Boolean) !== 0 || @@ -290,18 +438,57 @@ function isArrayType(checker: ts.TypeChecker, type: ts.Type): boolean { } function getParameterDependencies(node: ts.FunctionDeclaration) { - const typeParamNames = node.typeParameters?.map(tp => tp.name.getText()) || []; - const usage: Record = {}; + const typeParamNames = node.typeParameters?.map(tp => tp.name.getText()) || [] + const usage: Record = {} node.parameters.forEach((p, i) => { - const text = p.type?.getText() || ""; + const text = p.type?.getText() || "" typeParamNames.forEach(t => { - if (text.includes(t)) (usage[t] ??= []).push(i); - }); - }); + if (text.includes(t)) (usage[t] ??= []).push(i) + }) + }) return Object.values(usage) .filter(indices => indices.length > 1) .map(([first, ...rest]) => rest.map(idx => ({parameterIndex: idx, dependsOnIndex: first}))) - .flat(); + .flat() +} + +const extractObjectProperties = ( + type: ts.Type, + checker: ts.TypeChecker, + expectedType: ts.Type, + currentPath: ReferencePath[] = [] +): Array<{ path: ReferencePath[], type: ts.Type }> => { + const results: Array<{ path: ReferencePath[], type: ts.Type }> = [] + + if (checker.isTypeAssignableTo(type, expectedType)) results.push({ path: currentPath, type }) + + if (isRealObjectType(type)) { + const properties = type.getProperties() + if (properties && properties.length > 0) { + properties.forEach(property => { + const propType = checker.getTypeOfSymbolAtLocation(property, property.valueDeclaration!) + const propName = property.getName() + const newPath = [...currentPath, {path: propName}] + + results.push(...extractObjectProperties(propType, checker, expectedType, newPath)) + }) + } + } + + return results +} + +const isRealObjectType = (type: ts.Type): boolean => { + const primitiveFlags = + ts.TypeFlags.String | + ts.TypeFlags.Number | + ts.TypeFlags.Boolean | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.BigInt | + ts.TypeFlags.ESSymbol + + return (type.flags & primitiveFlags) === 0 } \ No newline at end of file diff --git a/test/getReferenceSuggestions.test.ts b/test/getReferenceSuggestions.test.ts index d5ac761..c3980d1 100644 --- a/test/getReferenceSuggestions.test.ts +++ b/test/getReferenceSuggestions.test.ts @@ -7,120 +7,61 @@ import {getSchema} from "../src/suggestion/getSchema"; describe('getReferenceSuggestions', () => { it('sd', () => { const flow: Flow = { - "__typename": "Flow", - "id": "gid://sagittarius/Flow/1", - "createdAt": "2026-03-17T14:02:31Z", - "name": "Test", - "signature": "(httpURL: HTTP_URL, httpMethod: HTTP_METHOD): REST_ADAPTER_INPUT<{}>", - "nodes": { - "__typename": "NodeFunctionConnection", - "nodes": [ + startingNodeId: "gid://sagittarius/NodeFunction/1", + nodes: { + nodes: [ { - "__typename": "NodeFunction", - "id": "gid://sagittarius/NodeFunction/3", - "functionDefinition": { - "__typename": "FunctionDefinition", - "id": "gid://sagittarius/FunctionDefinition/7", - "identifier": "std::list::for_each" + id: "gid://sagittarius/NodeFunction/1", + functionDefinition: {identifier: "std::number::add"}, + parameters: { + nodes: [ + {value: {__typename: "LiteralValue", value: 0}}, + {value: {__typename: "LiteralValue", value: 0}} + ] }, - "parameters": { - "__typename": "NodeParameterConnection", - "nodes": [ + nextNodeId: "gid://sagittarius/NodeFunction/2" + }, + { + id: "gid://sagittarius/NodeFunction/2", + functionDefinition: {identifier: "std::number::add"}, + parameters: { + nodes: [ { - "__typename": "NodeParameter", - "parameterDefinition": { - "__typename": "ParameterDefinition", - "id": "gid://sagittarius/ParameterDefinition/10", - "identifier": "list" - }, - "value": { - "__typename": "LiteralValue", - "value": [ - { - "test": "test" - } - ] + value: { + __typename: "ReferenceValue", + nodeFunctionId: "gid://sagittarius/NodeFunction/1" } }, { - "__typename": "NodeParameter", - "parameterDefinition": { - "__typename": "ParameterDefinition", - "id": "gid://sagittarius/ParameterDefinition/11", - "identifier": "consumer" - }, - "value": { - "id": "gid://sagittarius/NodeFunction/4", - "__typename": "NodeFunctionIdWrapper" + value: { + __typename: "LiteralValue", + value: 10, } } ] } }, { - "__typename": "NodeFunction", - "id": "gid://sagittarius/NodeFunction/4", - "functionDefinition": { - "__typename": "FunctionDefinition", - "id": "gid://sagittarius/FunctionDefinition/112", - "identifier": "std::control::value" - }, - "parameters": { - "__typename": "NodeParameterConnection", - "nodes": [ + id: "gid://sagittarius/NodeFunction/3", + functionDefinition: {identifier: "std::number::add"}, + parameters: { + nodes: [ { - "__typename": "NodeParameter", - "parameterDefinition": { - "__typename": "ParameterDefinition", - "id": "gid://sagittarius/ParameterDefinition/174", - "identifier": "value" - }, - "value": null + value: { + __typename: "ReferenceValue", + nodeFunctionId: "gid://sagittarius/NodeFunction/1" + } }, + { + value: { + __typename: "LiteralValue", + value: 10, + } + } ] } } ] - }, - "project": { - "__typename": "NamespaceProject", - "id": "gid://sagittarius/NamespaceProject/1" - }, - "settings": { - "__typename": "FlowSettingConnection", - "count": 2, - "nodes": [ - { - "__typename": "FlowSetting", - "id": "gid://sagittarius/FlowSetting/1", - "createdAt": "2026-03-17T14:17:48Z", - "updatedAt": "2026-03-17T14:17:48Z", - "flowSettingIdentifier": "httpURL", - "value": "" - }, - { - "__typename": "FlowSetting", - "id": "gid://sagittarius/FlowSetting/2", - "createdAt": "2026-03-17T14:17:48Z", - "updatedAt": "2026-03-17T14:17:48Z", - "flowSettingIdentifier": "httpMethod", - "value": "" - } - ], - "pageInfo": { - "__typename": "PageInfo", - "endCursor": "Mg", - "hasNextPage": false - } - }, - "startingNodeId": "gid://sagittarius/NodeFunction/3", - "type": { - "__typename": "FlowType", - "id": "gid://sagittarius/FlowType/1" - }, - "userAbilities": { - "__typename": "FlowUserAbilities", - "deleteFlow": true } }; From d69cf6c2740912cabd53277fb82464ce6c47aa72 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 00:11:02 +0200 Subject: [PATCH 03/12] feat: add custom schema support in getSchema function and improve node suggestion handling --- src/suggestion/getSchema.ts | 65 +++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/src/suggestion/getSchema.ts b/src/suggestion/getSchema.ts index 8b7c9f2..7a7d67b 100644 --- a/src/suggestion/getSchema.ts +++ b/src/suggestion/getSchema.ts @@ -3,7 +3,8 @@ import { Flow, FunctionDefinition, LiteralValue, - NodeFunction, ReferencePath, + NodeFunction, + ReferencePath, ReferenceValue } from "@code0-tech/sagittarius-graphql-types" import {createCompilerHost, generateFlowSourceCode, sanitizeId} from "../utils" @@ -54,7 +55,8 @@ export const getSchema = ( flow: Flow, dataTypes: DataType[], functions: FunctionDefinition[], - nodeId?: NodeFunction['id'] + nodeId?: NodeFunction['id'], + schema?: (type: ts.Type) => Schema | null ): ParameterSchema[] => { const sourceCode = generateFlowSourceCode(flow, functions, dataTypes) @@ -140,6 +142,9 @@ export const getSchema = ( suggestions: [...literalValueSuggestions, ...referenceSuggestions, ...nodeSuggestions], } + const customSchema = schema?.(type) + if (customSchema) return customSchema + if (isPrimitiveLiteralUnion(type)) return {input: "select", ...suggestions} if (isBoolean(type)) return {input: "boolean", ...suggestions} @@ -249,18 +254,16 @@ function getLiteralValueSuggestions(type: ts.Type): LiteralValue[] { function getNodeSuggestions(checker: ts.TypeChecker, functionDeclarations: ts.FunctionDeclaration[], functions: FunctionDefinition[], paramType: ts.Type): NodeFunction[] { - //TODO: if paramType is callable than we should parse in all functions that match the parameters - //TODO: otherwise we should only parse in functions that match the return type - //TODO: we should differentiate between inline usable suggestions and not + if (isSubFlow(paramType)) return [] - return functionDeclarations.flatMap(func => { + return functionDeclarations.flatMap(func => { const signature = checker.getSignatureFromDeclaration(func) const returnType = checker.getReturnTypeOfSignature(signature!) - const simplifiedReturnType = returnType.isTypeParameter() - ? (checker.getBaseConstraintOfType(returnType) || checker.getAnyType()) - : returnType + const simplifiedReturnType = returnType.isTypeParameter() + ? (checker.getBaseConstraintOfType(returnType) || checker.getAnyType()) + : returnType if (checker.isTypeAssignableTo(simplifiedReturnType, paramType)) { const functionName = func.name?.getText().replace("fn_", "").replace("_", "::").replace("_", "::") @@ -274,30 +277,28 @@ function getNodeSuggestions(checker: ts.TypeChecker, functionDeclarations: ts.Fu id: funktion?.id, identifier: funktion?.identifier, }, - parameters: { - __typename: "NodeParameterConnection", - nodes: (funktion?.parameterDefinitions?.nodes || []).map(p => ({ - __typename: "NodeParameter", - parameterDefinition: { - __typename: "ParameterDefinition", - id: p?.id, - identifier: p?.identifier - }, - value: p?.defaultValue ? { - __typename: "LiteralValue", - value: p.defaultValue.value - } : null - })) - } + ...((funktion?.parameterDefinitions?.nodes?.length ?? 0) > 0 ? { + parameters: { + __typename: "NodeParameterConnection", + nodes: + (funktion?.parameterDefinitions?.nodes || []).map(p => ({ + __typename: "NodeParameter", + parameterDefinition: { + __typename: "ParameterDefinition", + id: p?.id, + identifier: p?.identifier + }, + value: p?.defaultValue ? { + __typename: "LiteralValue", + value: p.defaultValue.value + } : null + })) + } + } : {}), } - return node - } - return [] - - }) } @@ -358,7 +359,7 @@ function getReferenceSuggestions(checker: ts.TypeChecker, node: ts.VariableDecla return typeArguments.flatMap((tupleElementType, tupleIndex) => { const propertyPaths = extractObjectProperties(tupleElementType, checker, paramType) - return propertyPaths.flatMap(({ path }) => { + return propertyPaths.flatMap(({path}) => { const referenceValue: ReferenceValue = { __typename: 'ReferenceValue', nodeFunctionId: nodeFunctionId as any, @@ -380,7 +381,7 @@ function getReferenceSuggestions(checker: ts.TypeChecker, node: ts.VariableDecla } else if (name.startsWith("flow_")) { const propertyPaths = extractObjectProperties(symbolType, checker, paramType) - return propertyPaths.flatMap(({ path }) => { + return propertyPaths.flatMap(({path}) => { const referenceValue: ReferenceValue = { __typename: 'ReferenceValue', nodeFunctionId: null @@ -462,7 +463,7 @@ const extractObjectProperties = ( ): Array<{ path: ReferencePath[], type: ts.Type }> => { const results: Array<{ path: ReferencePath[], type: ts.Type }> = [] - if (checker.isTypeAssignableTo(type, expectedType)) results.push({ path: currentPath, type }) + if (checker.isTypeAssignableTo(type, expectedType)) results.push({path: currentPath, type}) if (isRealObjectType(type)) { const properties = type.getProperties() From cad50a658fac0f312dcfa0bde0c2da95e4a3f4cf Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 00:56:28 +0200 Subject: [PATCH 04/12] feat: implement getNodeSchema function for generating node schemas with parameter dependencies --- src/schema/getNodeSchema.ts | 323 ++++++++++++++ src/suggestion/getNodeSuggestions.ts | 87 ---- src/suggestion/getReferenceSuggestions.ts | 256 ----------- src/suggestion/getSchema.ts | 495 ---------------------- src/suggestion/getValueSuggestions.ts | 57 --- test/getReferenceSuggestions.test.ts | 207 --------- test/nodeSuggestion.test.ts | 89 ---- 7 files changed, 323 insertions(+), 1191 deletions(-) create mode 100644 src/schema/getNodeSchema.ts delete mode 100644 src/suggestion/getNodeSuggestions.ts delete mode 100644 src/suggestion/getReferenceSuggestions.ts delete mode 100644 src/suggestion/getSchema.ts delete mode 100644 src/suggestion/getValueSuggestions.ts delete mode 100644 test/getReferenceSuggestions.test.ts delete mode 100644 test/nodeSuggestion.test.ts diff --git a/src/schema/getNodeSchema.ts b/src/schema/getNodeSchema.ts new file mode 100644 index 0000000..cd626fc --- /dev/null +++ b/src/schema/getNodeSchema.ts @@ -0,0 +1,323 @@ +import { DataType, Flow, FunctionDefinition, NodeFunction } from "@code0-tech/sagittarius-graphql-types" +import { createCompilerHost, generateFlowSourceCode, sanitizeId } from "../utils" +import ts, { Type } from "typescript" +import { getSchema, Schema } from "../util/schema.util" + +/** + * Represents the schema information for a node parameter. + * Includes the parameter's schema definition and any parameter dependencies that block it. + */ +interface NodeSchema { + /** The schema definition for this node parameter */ + schema: Schema + /** Array of parameter indices that must be resolved before this parameter */ + blockedBy?: number[] +} + +/** + * Represents a parameter dependency relationship. + * Indicates which parameters depend on type parameters defined in other parameters. + */ +interface ParameterDependency { + /** The index of the parameter that has the dependency */ + parameterIndex: number + /** The index of the parameter it depends on */ + dependsOnIndex: number +} + +/** + * Generates node schemas for all parameters of a specified function node. + * + * This function analyzes a TypeScript flow's AST to extract type information for node parameters. + * It resolves parameter types by combining information from both the node's call expression and + * the function definition, accounting for type parameters and generic constraints. + * + * @param flow - The data flow object containing nodes and their relationships + * @param dataTypes - Array of available data type definitions + * @param functions - Array of available function definitions + * @param nodeId - Optional specific node ID to analyze; if provided, only that node's schema is processed + * + * @returns Array of NodeSchema objects, each containing a schema and optional blocked dependencies + * + * @example + * const schemas = getNodeSchema(flow, dataTypes, functions, nodeId); + * schemas.forEach(({ schema, blockedBy }) => { + * console.log(`Parameter schema: ${schema}, blocked by: ${blockedBy?.join(',')}`); + * }); + */ +export const getNodeSchema = ( + flow: Flow, + dataTypes: DataType[], + functions: FunctionDefinition[], + nodeId?: NodeFunction["id"], +): NodeSchema[] => { + // Generate TypeScript source code from the flow definition + const sourceCode = generateFlowSourceCode(flow, functions, dataTypes) + + // Set up the TypeScript compiler environment + const fileName = "index.ts" + const host = createCompilerHost(fileName, sourceCode) + const sourceFile = host.getSourceFile(fileName)! + const program = host.languageService.getProgram()! + const checker = program.getTypeChecker() + + // Retrieve and identify the target node + const targetNode = flow.nodes?.nodes?.find((n) => n?.id === nodeId) + const functionId = `fn_${targetNode?.functionDefinition?.identifier?.replace(/::/g, "_")}` + const realNodeId = `node_${sanitizeId(nodeId || "")}` + + // Build map of declared functions for easy lookup + const declaredFunctionsMap = createFunctionMap(sourceFile) + + // Build map of constant variable declarations for easy lookup + const constantNames = createConstantMap(sourceFile) + + // Retrieve the node's variable declaration and its corresponding function + const node = constantNames.get(realNodeId) + const funktion = declaredFunctionsMap.get(functionId) + + // Extract parameter types from the node's call expression + const nodeParameterTypes = extractNodeParameterTypes(checker, node) + + // Extract parameter types from the function definition + const funktionParameterTypes = extractFunctionParameterTypes(checker, funktion, node) + + // Combine node and function parameter types, preferring node types when assignable + const combinedParameterTypes = mergeParameterTypes( + checker, + funktionParameterTypes, + nodeParameterTypes, + ) + + // Identify parameter dependencies based on type parameters + const funktionDependencies = getParameterDependencies(funktion!) + + // Generate schema for each parameter + return generateNodeSchemas( + checker, + node!, + combinedParameterTypes, + funktionDependencies, + declaredFunctionsMap, + functions, + ) +} + +/** + * Creates a map of all function declarations in the source file. + * + * @param sourceFile - The TypeScript source file to analyze + * @returns Map with function names as keys and FunctionDeclaration nodes as values + */ +const createFunctionMap = ( + sourceFile: ts.SourceFile, +): Map => { + return new Map( + sourceFile.statements + .filter(ts.isFunctionDeclaration) + .map((node) => [node.name!.getText(), node]), + ) +} + +/** + * Creates a map of all constant variable declarations in the source file. + * Recursively traverses the AST to find all const declarations. + * + * @param sourceFile - The TypeScript source file to analyze + * @returns Map with variable names as keys and VariableDeclaration nodes as values + */ +const createConstantMap = ( + sourceFile: ts.SourceFile, +): Map => { + const results: [string, ts.VariableDeclaration][] = [] + + sourceFile.statements.forEach((node) => { + node.forEachChild(function visitor(child) { + if (ts.isVariableDeclaration(child)) { + // Check if this is a const declaration + if ((child.parent.flags & ts.NodeFlags.Const) !== 0) { + results.push([child.name.getText(), child]) + } + } + child.forEachChild(visitor) + }) + }) + + return new Map(results) +} + +/** + * Extracts parameter types from a node's call expression. + * These types represent the actual types passed to the function at the node. + * + * @param checker - The TypeScript type checker + * @param node - The variable declaration containing the call expression + * @returns Array of resolved parameter types, or undefined if not available + */ +const extractNodeParameterTypes = ( + checker: ts.TypeChecker, + node: ts.VariableDeclaration | undefined, +): Type[] | undefined => { + if (!node?.initializer || !ts.isCallExpression(node.initializer)) { + return undefined + } + + const signature = checker.getResolvedSignature(node.initializer) + return signature?.parameters.map((p) => + checker.getTypeOfSymbolAtLocation(p, node.initializer as ts.CallExpression), + ) +} + +/** + * Extracts parameter types from the function definition. + * These are the declared parameter types from the function signature. + * + * @param checker - The TypeScript type checker + * @param funktion - The function declaration to analyze + * @param node - The node's variable declaration (used as location context) + * @returns Array of parameter types, or undefined if function not found + */ +const extractFunctionParameterTypes = ( + checker: ts.TypeChecker, + funktion: ts.FunctionDeclaration | undefined, + node: ts.VariableDeclaration | undefined, +): Type[] | undefined => { + if (!funktion || !node?.initializer) { + return undefined + } + + return funktion.parameters.map((p) => { + const symbol = checker.getSymbolAtLocation(p.name) + return checker.getTypeOfSymbolAtLocation( + symbol!, + node.initializer as ts.CallExpression, + ) + }) +} + +/** + * Merges function and node parameter types by applying type assignability rules. + * Prefers node types when they are assignable to the function's parameter type. + * Handles generic type parameters with constraints. + * + * @param checker - The TypeScript type checker + * @param funktionParameterTypes - Parameter types from function definition + * @param nodeParameterTypes - Parameter types from node's call expression + * @returns Array of resolved parameter types + */ +const mergeParameterTypes = ( + checker: ts.TypeChecker, + funktionParameterTypes: Type[] | undefined, + nodeParameterTypes: Type[] | undefined, +): Type[] | undefined => { + if (!funktionParameterTypes) { + return undefined + } + + return funktionParameterTypes.map((paramType, index) => { + const nodeType = nodeParameterTypes?.[index] + if (!nodeType) { + return paramType + } + + // If both types refer to the same symbol, use the node type + const paramSymbol = paramType.getSymbol() + const nodeSymbol = nodeType.getSymbol() + if (paramSymbol && nodeSymbol && paramSymbol === nodeSymbol) { + return nodeType + } + + // Handle generic type parameters + if (paramType.isTypeParameter()) { + const constraint = checker.getBaseConstraintOfType(paramType) + // Use node type if it satisfies the constraint + if (!constraint || checker.isTypeAssignableTo(nodeType, constraint)) { + return nodeType + } + } + + // Use node type if assignable to parameter type + if (checker.isTypeAssignableTo(nodeType, paramType)) { + return nodeType + } + + // Default to function parameter type + return paramType + }) +} + +/** + * Identifies parameter dependencies based on shared type parameters. + * Determines which parameters depend on type parameters declared in other parameters. + * + * @param node - The function declaration to analyze + * @returns Array of ParameterDependency objects + */ +const getParameterDependencies = (node: ts.FunctionDeclaration): ParameterDependency[] => { + // Extract all type parameter names from the function + const typeParamNames = node.typeParameters?.map((tp) => tp.name.getText()) || [] + const usage: Record = {} + + // Track which parameters use each type parameter + node.parameters.forEach((p, i) => { + const typeText = p.type?.getText() || "" + typeParamNames.forEach((typeParam) => { + if (typeText.includes(typeParam)) { + if (!usage[typeParam]) { + usage[typeParam] = [] + } + usage[typeParam].push(i) + } + }) + }) + + // Extract dependencies: type params used by multiple parameters + return Object.values(usage) + .filter((indices) => indices.length > 1) + .map(([firstIndex, ...otherIndices]) => + otherIndices.map((depIndex) => ({ + parameterIndex: depIndex, + dependsOnIndex: firstIndex, + })), + ) + .flat() +} + +/** + * Generates node schemas for all parameters. + * Creates schema objects for each parameter with their dependencies. + * + * @param checker - The TypeScript type checker + * @param node - The node's variable declaration + * @param combinedParameterTypes - Merged parameter types to use for schema generation + * @param funktionDependencies - Parameter dependencies to link with each parameter + * @param declaredFunctionsMap - Map of available functions for schema context + * @param functions - Array of function definitions + * @returns Array of NodeSchema objects + */ +const generateNodeSchemas = ( + checker: ts.TypeChecker, + node: ts.VariableDeclaration, + combinedParameterTypes: Type[] | undefined, + funktionDependencies: ParameterDependency[], + declaredFunctionsMap: Map, + functions: FunctionDefinition[], +): NodeSchema[] => { + if (!combinedParameterTypes) { + return [] + } + + return combinedParameterTypes.map((parameterType, index) => ({ + schema: getSchema( + checker, + node, + parameterType, + Array.from(declaredFunctionsMap.values()), + functions, + ), + blockedBy: funktionDependencies + .filter((dep) => dep.parameterIndex === index) + .map((dep) => dep.dependsOnIndex), + })) +} + diff --git a/src/suggestion/getNodeSuggestions.ts b/src/suggestion/getNodeSuggestions.ts deleted file mode 100644 index 034dd2e..0000000 --- a/src/suggestion/getNodeSuggestions.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {DataType, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; -import {createCompilerHost, getSharedTypeDeclarations} from "../utils"; -import {DataTypeVariant, getTypeVariant} from "../extraction/getTypeVariant"; - -/** - * 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 const getNodeSuggestions = ( - type?: string, - functions?: FunctionDefinition[], - dataTypes?: DataType[] -): NodeFunction[] => { - - let functionToSuggest = functions - - const typeVariant = type ? getTypeVariant(type, dataTypes)[0].variant : null; - - if (type && functions && typeVariant !== DataTypeVariant.NODE) { - function getGenericsCount(input: string): number { - const match = input.trim().match(/^<([^>]+)>/); - if (!match) return 0; - return match[1].split(',').map(s => s.trim()).filter(Boolean).length; - } - - 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 diagnostics = program.getSemanticDiagnostics(); - const errorLines = new Set(); - diagnostics.forEach(diag => { - if (diag.file === sourceFile && diag.start !== undefined) { - errorLines.add(sourceFile.getLineAndCharacterOfPosition(diag.start).line); - } - }); - - 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); - }); - } - - - return functionToSuggest?.map(f => { - const node: NodeFunction = { - __typename: "NodeFunction", - id: `gid://sagittarius/NodeFunction/1`, - functionDefinition: { - __typename: "FunctionDefinition", - id: f.id, - identifier: f.identifier, - }, - parameters: { - __typename: "NodeParameterConnection", - nodes: (f.parameterDefinitions?.nodes || []).map(p => ({ - __typename: "NodeParameter", - parameterDefinition: { - __typename: "ParameterDefinition", - id: p?.id, - identifier: p?.identifier - }, - value: p?.defaultValue ? { - __typename: "LiteralValue", - value: p.defaultValue.value - } : null - })) - } - } - return node - }).filter((f): f is NodeFunction => f !== null) ?? []; -} diff --git a/src/suggestion/getReferenceSuggestions.ts b/src/suggestion/getReferenceSuggestions.ts deleted file mode 100644 index 1cb7465..0000000 --- a/src/suggestion/getReferenceSuggestions.ts +++ /dev/null @@ -1,256 +0,0 @@ -import ts from "typescript"; -import { - DataType, - Flow, - FunctionDefinition, - NodeFunction, - ReferencePath, - ReferenceValue -} from "@code0-tech/sagittarius-graphql-types"; -import {createCompilerHost, generateFlowSourceCode} from "../utils"; - -/** - * Determines whether a type is a real object type (not a primitive type). - * Excludes built-in primitive types like string, number, boolean, etc. - * to ensure only actual object properties are extracted. - * - * @param type The TypeScript type to check - * @returns true if the type is an object, false if it's a primitive - */ -const isRealObjectType = (type: ts.Type): boolean => { - const primitiveFlags = - ts.TypeFlags.String | - ts.TypeFlags.Number | - ts.TypeFlags.Boolean | - ts.TypeFlags.Undefined | - ts.TypeFlags.Null | - ts.TypeFlags.BigInt | - ts.TypeFlags.ESSymbol; - - return (type.flags & primitiveFlags) === 0; -}; - -/** - * Recursively extracts all nested properties of an object type that are assignable - * to the expected type, along with their access paths. - * - * For example, given an object type {user: {id: number, name: string}} and - * an expected type of 'number', this function returns: - * - { path: ['user', 'id'], type: number } - * - * @param type The type to extract properties from - * @param checker The TypeScript type checker - * @param expectedType The expected type to match against - * @param currentPath The current property path (used for recursion) - * @returns Array of matching properties with their paths - */ -const extractObjectProperties = ( - type: ts.Type, - checker: ts.TypeChecker, - expectedType: ts.Type, - currentPath: ReferencePath[] = [] -): Array<{ path: ReferencePath[]; type: ts.Type }> => { - const results: Array<{ path: ReferencePath[]; type: ts.Type }> = []; - - // Add the current type if it matches the expected type - if (checker.isTypeAssignableTo(type, expectedType)) { - results.push({ path: currentPath, type }); - } - - // Only extract properties from real object types, not primitives - if (isRealObjectType(type)) { - const properties = type.getProperties(); - if (properties && properties.length > 0) { - properties.forEach(property => { - const propType = checker.getTypeOfSymbolAtLocation(property, property.valueDeclaration!); - const propName = property.getName(); - const newPath = [...currentPath, {path: propName}]; - - // Recursively extract nested properties - results.push(...extractObjectProperties(propType, checker, expectedType, newPath)); - }); - } - } - - return results; -}; - -/** - * Calculates all available reference suggestions for a specific target parameter in a flow. - * - * This function analyzes the flow's generated source code to find all variables - * (node_ and p_ prefixed) that are in scope and compatible with the target parameter's - * expected type. For object types, it also extracts nested properties and their access paths. - * - * @param flow The flow configuration - * @param nodeId The ID of the node containing the target parameter - * @param targetIndex The index of the target parameter - * @param functions Available function definitions for type resolution - * @param dataTypes Available data type definitions - * @returns Array of ReferenceValue objects representing available suggestions - */ -export const getReferenceSuggestions = ( - flow?: Flow, - nodeId?: NodeFunction['id'], - targetIndex?: number, - functions?: FunctionDefinition[], - dataTypes?: DataType[] -): ReferenceValue[] => { - - const nodeIndex = flow?.nodes?.nodes?.findIndex(n => n?.id == nodeId); - - if (typeof nodeIndex == "number" && nodeIndex >= 0 && typeof targetIndex == "number" && targetIndex >= 0 && flow?.nodes?.nodes?.[nodeIndex]?.parameters?.nodes?.[targetIndex]) { - flow.nodes.nodes[nodeIndex].parameters.nodes[targetIndex].value = null - } - - const sourceCode = generateFlowSourceCode(flow, functions, dataTypes, true); - const fileName = "index.ts"; - const host = createCompilerHost(fileName, sourceCode); - const sourceFile = host.getSourceFile(fileName)!; - const program = host.languageService.getProgram()!; - const checker = program.getTypeChecker(); - - // Find the exact position of the target node using a marker comment - const fullText = sourceFile.getFullText(); - const commentPattern = `/* @pos ${nodeId} ${targetIndex} */`; - const commentIndex = fullText.indexOf(commentPattern); - const targetPos = commentIndex + commentPattern.length; - - /** - * Recursively finds the smallest AST node at the given position - */ - function findNodeAtPosition(node: ts.Node, pos: number): ts.Node { - let found = node; - ts.forEachChild(node, child => { - if (child.getStart(sourceFile, true) <= pos && child.getEnd() >= pos) { - found = findNodeAtPosition(child, pos); - } - }); - return found; - } - - let targetNode = findNodeAtPosition(sourceFile, targetPos); - const targetExpression = targetNode as ts.Expression; - - // Find the enclosing function call - let parentCall: ts.CallExpression | undefined; - if (ts.isCallExpression(targetExpression)) { - parentCall = targetExpression; - } - - if (!parentCall) { - return []; - } - - // Get the signature and expected type of the target parameter - const signature = checker.getResolvedSignature(parentCall); - if (!signature) return []; - - const params = signature.getParameters(); - const paramSymbol = params[targetIndex!] || params[params.length - 1]; - const expectedType = checker.getTypeOfSymbolAtLocation(paramSymbol, targetExpression); - - // Collect all variables in scope (node_ and p_ prefixed) - const allSymbols = checker.getSymbolsInScope(targetExpression, ts.SymbolFlags.Variable); - const referenceValues: ReferenceValue[] = []; - - allSymbols.forEach(symbol => { - const name = symbol.getName(); - if (!name.startsWith("node_") && !name.startsWith("p_") && !name.startsWith("flow_")) return; - - // Get the variable declaration - const declaration = symbol.valueDeclaration || symbol.declarations?.[0]; - if (!declaration) return; - - // Skip variables declared after the target position - if (declaration.getEnd() >= targetPos) { - return; - } - - const symbolType = checker.getTypeOfSymbolAtLocation(symbol, targetExpression); - - // Handle node_ variables (node function results) - if (name.startsWith("node_")) { - if (!((symbolType.flags & ts.TypeFlags.Void) !== 0)) { - const nodeFunctionId = name - .replace("node_", "") - .replace(/___/g, "://") - .replace(/__/g, "/") - .replace(/_/g, "/"); - - // Extract all compatible properties including nested ones - const propertyPaths = extractObjectProperties(symbolType, checker, expectedType); - - propertyPaths.forEach(({ path }) => { - const referenceValue: ReferenceValue = { - __typename: 'ReferenceValue', - nodeFunctionId: nodeFunctionId as any - }; - - if (path.length > 0) { - referenceValue.referencePath = path; - } - - referenceValues.push(referenceValue); - }); - } - } - // Handle p_ variables (parameter/input values) - else if (name.startsWith("p_")) { - const idPart = name.replace("p_", ""); - const lastUnderscoreIndex = idPart.lastIndexOf("_"); - const rawId = idPart.substring(0, lastUnderscoreIndex); - const paramIndexFromName = parseInt(idPart.substring(lastUnderscoreIndex + 1), 10); - - const nodeFunctionId = rawId - .replace("p_", "") - .replace(/___/g, "://") - .replace(/__/g, "/") - .replace(/_/g, "/"); - - // Handle tuple types (e.g., destructured parameters like [item, index]) - if (checker.isTupleType(symbolType)) { - const typeReference = symbolType as ts.TypeReference; - const typeArguments = checker.getTypeArguments(typeReference); - - typeArguments.forEach((tupleElementType, tupleIndex) => { - // Extract all compatible properties for this tuple element - const propertyPaths = extractObjectProperties(tupleElementType, checker, expectedType); - - propertyPaths.forEach(({ path }) => { - const referenceValue: ReferenceValue = { - __typename: 'ReferenceValue', - nodeFunctionId: nodeFunctionId as any, - parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName, - inputIndex: tupleIndex, - inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[tupleIndex].name.getText() - }; - - if (path.length > 0) { - referenceValue.referencePath = path; - } - - referenceValues.push(referenceValue); - }); - }); - } - } - else if (name.startsWith("flow_")) { - const propertyPaths = extractObjectProperties(symbolType, checker, expectedType) - propertyPaths.forEach(({ path }) => { - const referenceValue: ReferenceValue = { - __typename: 'ReferenceValue', - nodeFunctionId: null - }; - - if (path.length > 0) { - referenceValue.referencePath = path; - } - - referenceValues.push(referenceValue); - }) - } - }); - - return referenceValues; -} diff --git a/src/suggestion/getSchema.ts b/src/suggestion/getSchema.ts deleted file mode 100644 index 7a7d67b..0000000 --- a/src/suggestion/getSchema.ts +++ /dev/null @@ -1,495 +0,0 @@ -import { - DataType, - Flow, - FunctionDefinition, - LiteralValue, - NodeFunction, - ReferencePath, - ReferenceValue -} from "@code0-tech/sagittarius-graphql-types" -import {createCompilerHost, generateFlowSourceCode, sanitizeId} from "../utils" -import ts, {NumberLiteralType, StringLiteralType, Type} from "typescript" - -interface Input { - input?: string - suggestions?: (NodeFunction | ReferenceValue | LiteralValue)[] -} - -interface GenericInput { - input?: "generic" -} - -interface SubFlowInput { - input?: "sub-flow" -} - -interface PrimitiveInput extends Input { - input?: "boolean" | "number" | "text" | "select" -} - -interface DataInput extends Input { - input?: "data" - properties?: Record - required?: string[] -} - -interface ListInput extends Input { - input?: "list" - items?: Schema | Schema[] -} - -interface TypeInput extends Input { - input?: "type" - properties?: Record - required?: string[] -} - -type Schema = PrimitiveInput | DataInput | ListInput | TypeInput | SubFlowInput | GenericInput - -interface ParameterSchema { - schema: Schema - blockedBy?: number[] -} - -export const getSchema = ( - flow: Flow, - dataTypes: DataType[], - functions: FunctionDefinition[], - nodeId?: NodeFunction['id'], - schema?: (type: ts.Type) => Schema | null -): ParameterSchema[] => { - - const sourceCode = generateFlowSourceCode(flow, functions, dataTypes) - - const fileName = "index.ts" - const host = createCompilerHost(fileName, sourceCode) - const sourceFile = host.getSourceFile(fileName)! - const program = host.languageService.getProgram()! - const checker = program.getTypeChecker() - - const node2 = flow.nodes?.nodes?.find(n => n?.id === nodeId) - const functionId = `fn_${node2?.functionDefinition?.identifier?.replace(/::/g, '_')}` - const realNodeId = `node_${sanitizeId(nodeId || "")}` - - const declaredFunctionsMap = new Map( - sourceFile.statements - .filter(ts.isFunctionDeclaration) - .map(node => [node.name!.getText(), node]) - ) - - const constantNames = new Map( - sourceFile.statements - .flatMap(node => { - const results: ts.VariableDeclaration[] = [] - - node.forEachChild(function visitor(child) { - if (ts.isVariableDeclaration(child)) { - if ((child.parent.flags & ts.NodeFlags.Const) !== 0) { - results.push(child) - } - } - child.forEachChild(visitor) - }) - - return results - }) - .map(decl => [decl.name.getText(), decl] as [string, ts.VariableDeclaration]) - ) - - const node = constantNames.get(realNodeId) - const funktion = declaredFunctionsMap.get(functionId) - - const nodeParameterTypes: Type[] | undefined = checker.getResolvedSignature(node?.initializer as ts.CallExpression)?.parameters.map(p => { - return checker.getTypeOfSymbolAtLocation(p, node?.initializer as ts.CallExpression) - }) - - const funktionParameterTypes: Type[] | undefined = funktion?.parameters?.map(p => { - const symbol = checker.getSymbolAtLocation(p.name) - return checker.getTypeOfSymbolAtLocation(symbol!, node?.initializer as ts.CallExpression) - }) - - const combinedParameterTypes: Type[] | undefined = funktionParameterTypes?.map((p, i) => { - const nodeType = nodeParameterTypes?.[i] - if (!nodeType) return p - - const pSymbol = p.getSymbol() - const nodeSymbol = nodeType.getSymbol() - - if (pSymbol && nodeSymbol && pSymbol === nodeSymbol) { - return nodeType - } - - if (p.isTypeParameter()) { - const constraint = checker.getBaseConstraintOfType(p) - if (!constraint || checker.isTypeAssignableTo(nodeType, constraint)) { - return nodeType - } - } - - if (checker.isTypeAssignableTo(nodeType, p)) { - return nodeType - } - - return p - }) - - const generateSchema = (type: ts.Type): Schema => { - - const literalValueSuggestions = getLiteralValueSuggestions(type) - const referenceSuggestions = getReferenceSuggestions(checker, node!, type, checker.getSymbolsInScope(node!, ts.SymbolFlags.Variable)) - const nodeSuggestions = getNodeSuggestions(checker, Array.from(declaredFunctionsMap.values()), functions, type) - const suggestions = { - suggestions: [...literalValueSuggestions, ...referenceSuggestions, ...nodeSuggestions], - } - - const customSchema = schema?.(type) - if (customSchema) return customSchema - - if (isPrimitiveLiteralUnion(type)) return {input: "select", ...suggestions} - - if (isBoolean(type)) return {input: "boolean", ...suggestions} - if (isNumber(type)) return {input: "number", ...suggestions} - if (isString(type)) return {input: "text", ...suggestions} - - if (isSubFlow(type)) return {input: "sub-flow", ...suggestions} - - if (isArrayType(checker, type)) { - const itemType = checker.getTypeArguments(type as ts.TypeReference)[0] - const itemTypes = itemType.isUnion() ? itemType.types : [itemType] - const itemSchemas = itemTypes.map(itemType => generateSchema(itemType)) - - return { - input: "list", - items: itemSchemas.length === 1 ? itemSchemas[0] : itemSchemas, - ...suggestions - } - } - - if ( - (type.flags & ts.TypeFlags.Object) !== 0 - ) { - const properties: Record = {} - const required: string[] = [] - - for (const property of checker.getPropertiesOfType(type)) { - const declaration = property.valueDeclaration ?? property.declarations?.[0] - - if (!declaration) continue - - const propertyType = checker.getTypeOfSymbolAtLocation(property, declaration) - - const isOptional = - (property.flags & ts.SymbolFlags.Optional) !== 0 || - ( - propertyType.isUnion() && - propertyType.types.some(t => (t.flags & ts.TypeFlags.Undefined) !== 0) - ) - - const propertyTypes = propertyType.isUnion() - ? propertyType.types.filter(t => (t.flags & ts.TypeFlags.Undefined) === 0) - : [propertyType] - - const propertySchemas = propertyTypes.map(generateSchema) - - properties[property.name] = - propertySchemas.length === 1 ? propertySchemas[0] : propertySchemas - - if (!isOptional) { - required.push(property.name) - } - } - - return { - input: "data", - properties, - required, - ...suggestions - } - } - - return { - input: "generic" - } - } - - const funktionDependencies = getParameterDependencies(funktion!) - - return combinedParameterTypes?.map((value, index) => { - return { - schema: generateSchema(value), - blockedBy: funktionDependencies - .filter(dep => dep.parameterIndex === index) - .map(dep => dep.dependsOnIndex) - } - }) || [] -} - -function getLiteralValueSuggestions(type: ts.Type): LiteralValue[] { - - if (type.isUnion()) { - return type.types.flatMap(getLiteralValueSuggestions) - } - - if (type.isStringLiteral()) return [{ - value: (type as StringLiteralType).value, - __typename: "LiteralValue" - }] - - if (type.isNumberLiteral()) return [{ - value: (type as NumberLiteralType).value.toString(), - }] - - if ((type as any).intrinsicName === "true") return [{ - value: "true", - __typename: "LiteralValue" - }] - - if ((type as any).intrinsicName === "false") return [{ - value: "true", - __typename: "LiteralValue" - }] - - return [] -} - -function getNodeSuggestions(checker: ts.TypeChecker, functionDeclarations: ts.FunctionDeclaration[], functions: FunctionDefinition[], paramType: ts.Type): NodeFunction[] { - - if (isSubFlow(paramType)) return [] - - return functionDeclarations.flatMap(func => { - - const signature = checker.getSignatureFromDeclaration(func) - const returnType = checker.getReturnTypeOfSignature(signature!) - - const simplifiedReturnType = returnType.isTypeParameter() - ? (checker.getBaseConstraintOfType(returnType) || checker.getAnyType()) - : returnType - - if (checker.isTypeAssignableTo(simplifiedReturnType, paramType)) { - const functionName = func.name?.getText().replace("fn_", "").replace("_", "::").replace("_", "::") - const funktion = functions.find(f => f.identifier === functionName) - - const node: NodeFunction = { - __typename: "NodeFunction", - id: `gid://sagittarius/NodeFunction/1`, - functionDefinition: { - __typename: "FunctionDefinition", - id: funktion?.id, - identifier: funktion?.identifier, - }, - ...((funktion?.parameterDefinitions?.nodes?.length ?? 0) > 0 ? { - parameters: { - __typename: "NodeParameterConnection", - nodes: - (funktion?.parameterDefinitions?.nodes || []).map(p => ({ - __typename: "NodeParameter", - parameterDefinition: { - __typename: "ParameterDefinition", - id: p?.id, - identifier: p?.identifier - }, - value: p?.defaultValue ? { - __typename: "LiteralValue", - value: p.defaultValue.value - } : null - })) - } - } : {}), - } - return node - } - return [] - }) - -} - -function getReferenceSuggestions(checker: ts.TypeChecker, node: ts.VariableDeclaration, paramType: ts.Type, symbols: ts.Symbol[]): ReferenceValue[] { - - return symbols.flatMap(symbol => { - const name = symbol.getName() - - if (!name.startsWith("node_") && !name.startsWith("p_") && !name.startsWith("flow_")) return [] - - const symbolDeclaration = symbol.getDeclarations()?.[0] - if (!symbolDeclaration) return [] - if (symbolDeclaration.getEnd() >= node.getEnd()!) return [] - - const symbolType = checker.getTypeOfSymbolAtLocation(symbol, node) - - if (name.startsWith("node_")) { - if (!((symbolType.flags & ts.TypeFlags.Void) !== 0)) { - - const nodeFunctionId = name - .replace("node_", "") - .replace(/___/g, "://") - .replace(/__/g, "/") - .replace(/_/g, "/") - - const propertyPaths = extractObjectProperties(symbolType, checker, paramType) - - return propertyPaths.flatMap(({path}) => { - const referenceValue: ReferenceValue = { - __typename: 'ReferenceValue', - nodeFunctionId: nodeFunctionId as any - } - - if (path.length > 0) referenceValue.referencePath = path - - return referenceValue - }) - - } - } else if (name.startsWith("p_")) { - - const idPart = name.replace("p_", "") - const lastUnderscoreIndex = idPart.lastIndexOf("_") - const rawId = idPart.substring(0, lastUnderscoreIndex) - const paramIndexFromName = parseInt(idPart.substring(lastUnderscoreIndex + 1), 10) - - const nodeFunctionId = rawId - .replace("p_", "") - .replace(/___/g, "://") - .replace(/__/g, "/") - .replace(/_/g, "/") - - if (checker.isTupleType(symbolType)) { - const typeReference = symbolType as ts.TypeReference - const typeArguments = checker.getTypeArguments(typeReference) - - return typeArguments.flatMap((tupleElementType, tupleIndex) => { - const propertyPaths = extractObjectProperties(tupleElementType, checker, paramType) - - return propertyPaths.flatMap(({path}) => { - const referenceValue: ReferenceValue = { - __typename: 'ReferenceValue', - nodeFunctionId: nodeFunctionId as any, - parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName, - inputIndex: tupleIndex, - inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[tupleIndex].name.getText() - } - - if (path.length > 0) { - referenceValue.referencePath = path - } - - return referenceValue - }) - - }) - } - - } else if (name.startsWith("flow_")) { - const propertyPaths = extractObjectProperties(symbolType, checker, paramType) - - return propertyPaths.flatMap(({path}) => { - const referenceValue: ReferenceValue = { - __typename: 'ReferenceValue', - nodeFunctionId: null - } - - if (path.length > 0) referenceValue.referencePath = path - - return referenceValue - }) - } - - return [] - }) - -} - -function isBoolean(type: ts.Type): boolean { - return ( - (type.flags & ts.TypeFlags.Boolean) !== 0 || - (type.flags & ts.TypeFlags.BooleanLiteral) !== 0 - ) -} - -function isSubFlow(type: ts.Type): boolean { - return ( - type.getCallSignatures().length > 0 - ) -} - -function isNumber(type: ts.Type): boolean { - return ( - (type.flags & ts.TypeFlags.Number) !== 0 || - (type.flags & ts.TypeFlags.NumberLiteral) !== 0 - ) -} - -function isString(type: ts.Type): boolean { - return ( - (type.flags & ts.TypeFlags.String) !== 0 || - (type.flags & ts.TypeFlags.StringLiteral) !== 0 - ) -} - -function isPrimitive(type: ts.Type): boolean { - return isString(type) || isNumber(type) || isBoolean(type) -} - -function isPrimitiveLiteralUnion(type: ts.Type): boolean { - if (!type.isUnion()) return false - return type.types.every(isPrimitive) -} - -function isArrayType(checker: ts.TypeChecker, type: ts.Type): boolean { - return checker.isArrayType(type) || checker.isTupleType(type) -} - -function getParameterDependencies(node: ts.FunctionDeclaration) { - const typeParamNames = node.typeParameters?.map(tp => tp.name.getText()) || [] - const usage: Record = {} - - node.parameters.forEach((p, i) => { - const text = p.type?.getText() || "" - typeParamNames.forEach(t => { - if (text.includes(t)) (usage[t] ??= []).push(i) - }) - }) - - return Object.values(usage) - .filter(indices => indices.length > 1) - .map(([first, ...rest]) => rest.map(idx => ({parameterIndex: idx, dependsOnIndex: first}))) - .flat() -} - -const extractObjectProperties = ( - type: ts.Type, - checker: ts.TypeChecker, - expectedType: ts.Type, - currentPath: ReferencePath[] = [] -): Array<{ path: ReferencePath[], type: ts.Type }> => { - const results: Array<{ path: ReferencePath[], type: ts.Type }> = [] - - if (checker.isTypeAssignableTo(type, expectedType)) results.push({path: currentPath, type}) - - if (isRealObjectType(type)) { - const properties = type.getProperties() - if (properties && properties.length > 0) { - properties.forEach(property => { - const propType = checker.getTypeOfSymbolAtLocation(property, property.valueDeclaration!) - const propName = property.getName() - const newPath = [...currentPath, {path: propName}] - - results.push(...extractObjectProperties(propType, checker, expectedType, newPath)) - }) - } - } - - return results -} - -const isRealObjectType = (type: ts.Type): boolean => { - const primitiveFlags = - ts.TypeFlags.String | - ts.TypeFlags.Number | - ts.TypeFlags.Boolean | - ts.TypeFlags.Undefined | - ts.TypeFlags.Null | - ts.TypeFlags.BigInt | - ts.TypeFlags.ESSymbol - - return (type.flags & primitiveFlags) === 0 -} \ No newline at end of file diff --git a/src/suggestion/getValueSuggestions.ts b/src/suggestion/getValueSuggestions.ts deleted file mode 100644 index ea34ecb..0000000 --- a/src/suggestion/getValueSuggestions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import ts from "typescript"; -import {DataType, LiteralValue} from "@code0-tech/sagittarius-graphql-types"; -import {createCompilerHost, getSharedTypeDeclarations} from "../utils"; - -/** - * Extracts possible literal values from a type string to provide suggestions. - */ -export const getValueSuggestions = ( - type?: string, - dataTypes?: DataType[] -): LiteralValue[] => { - if (!type) return []; - - const sourceCode = ` - ${getSharedTypeDeclarations(dataTypes)} - type VALUE = ${type}; const val: VALUE = {} as any; - `; - - const fileName = "index.ts"; - const host = createCompilerHost(fileName, sourceCode); - const sourceFile = host.getSourceFile(fileName)!; - const program = host.languageService.getProgram()!; - const checker = program.getTypeChecker(); - - // Find the VALUE type alias (not the first one, but the one we defined) - const typeAlias = sourceFile.statements.find( - node => ts.isTypeAliasDeclaration(node) && node.name.text === 'VALUE' - ); - if (!typeAlias || !ts.isTypeAliasDeclaration(typeAlias)) return []; - - const typeFound = checker.getTypeAtLocation(typeAlias); - - /** - * Recursively extracts literal values from a TypeScript type. - */ - const extractValues = (t: ts.Type): string[] => { - if (t.isUnion()) { - return t.types.flatMap(extractValues); - } - - if (t.isStringLiteral()) return [t.value]; - if (t.isNumberLiteral()) return [t.value.toString()]; - - if ((t as any).intrinsicName === "true") return ["true"]; - if ((t as any).intrinsicName === "false") return ["false"]; - - return []; - }; - - // Use a Set to ensure uniqueness. - const uniqueValues = Array.from(new Set(extractValues(typeFound))); - - return uniqueValues.map(value => ({ - __typename: "LiteralValue", - value - })); -}; diff --git a/test/getReferenceSuggestions.test.ts b/test/getReferenceSuggestions.test.ts deleted file mode 100644 index c3980d1..0000000 --- a/test/getReferenceSuggestions.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import {describe, it} from 'vitest'; -import {getReferenceSuggestions} from '../src/suggestion/getReferenceSuggestions'; -import {Flow} from "@code0-tech/sagittarius-graphql-types"; -import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data"; -import {getSchema} from "../src/suggestion/getSchema"; - -describe('getReferenceSuggestions', () => { - it('sd', () => { - const flow: Flow = { - startingNodeId: "gid://sagittarius/NodeFunction/1", - nodes: { - nodes: [ - { - id: "gid://sagittarius/NodeFunction/1", - functionDefinition: {identifier: "std::number::add"}, - parameters: { - nodes: [ - {value: {__typename: "LiteralValue", value: 0}}, - {value: {__typename: "LiteralValue", value: 0}} - ] - }, - nextNodeId: "gid://sagittarius/NodeFunction/2" - }, - { - id: "gid://sagittarius/NodeFunction/2", - functionDefinition: {identifier: "std::number::add"}, - parameters: { - nodes: [ - { - value: { - __typename: "ReferenceValue", - nodeFunctionId: "gid://sagittarius/NodeFunction/1" - } - }, - { - value: { - __typename: "LiteralValue", - value: 10, - } - } - ] - } - }, - { - id: "gid://sagittarius/NodeFunction/3", - functionDefinition: {identifier: "std::number::add"}, - parameters: { - nodes: [ - { - value: { - __typename: "ReferenceValue", - nodeFunctionId: "gid://sagittarius/NodeFunction/1" - } - }, - { - value: { - __typename: "LiteralValue", - value: 10, - } - } - ] - } - } - ] - } - }; - - const schema = getSchema(flow, DATA_TYPES, FUNCTION_SIGNATURES, "gid://sagittarius/NodeFunction/3"); - - console.dir(schema, { depth: null, colors: true }); - }); - - it('2', () => { - const flow: Flow = { - "__typename": "Flow", - "id": "gid://sagittarius/Flow/1", - "createdAt": "2026-04-13T13:49:03Z", - "name": "/test/Test", - "signature": "(httpURL: HTTP_URL, httpMethod: HTTP_METHOD): REST_ADAPTER_INPUT<{}>", - "nodes": { - "__typename": "NodeFunctionConnection", - "nodes": [ - { - "__typename": "NodeFunction", - "id": "gid://sagittarius/NodeFunction/1", - "functionDefinition": { - "__typename": "FunctionDefinition", - "id": "gid://sagittarius/FunctionDefinition/42", - "identifier": "http::response::create" - }, - "parameters": { - "__typename": "NodeParameterConnection", - "nodes": [ - { - "__typename": "NodeParameter", - "parameterDefinition": { - "__typename": "ParameterDefinition", - "id": "gid://sagittarius/ParameterDefinition/66", - "identifier": "http_status_code" - }, - "value": null - }, - { - "__typename": "NodeParameter", - "parameterDefinition": { - "__typename": "ParameterDefinition", - "id": "gid://sagittarius/ParameterDefinition/67", - "identifier": "headers" - }, - "value": { - "__typename": "LiteralValue", - "value": {} - } - }, - { - "__typename": "NodeParameter", - "parameterDefinition": { - "__typename": "ParameterDefinition", - "id": "gid://sagittarius/ParameterDefinition/68", - "identifier": "payload" - }, - "value": null - } - ] - }, - "nextNodeId": "gid://sagittarius/NodeFunction/2" - }, - { - "__typename": "NodeFunction", - "id": "gid://sagittarius/NodeFunction/2", - "functionDefinition": { - "__typename": "FunctionDefinition", - "id": "gid://sagittarius/FunctionDefinition/114", - "identifier": "rest::control::respond" - }, - "parameters": { - "__typename": "NodeParameterConnection", - "nodes": [ - { - "__typename": "NodeParameter", - "parameterDefinition": { - "__typename": "ParameterDefinition", - "id": "gid://sagittarius/ParameterDefinition/177", - "identifier": "http_response" - }, - "value": { - "__typename": "LiteralValue", - "value": { - "body": null, - "headers": {}, - "status_code": 0 - } - } - } - ] - } - } - ] - }, - "project": { - "__typename": "NamespaceProject", - "id": "gid://sagittarius/NamespaceProject/1" - }, - "settings": { - "__typename": "FlowSettingConnection", - "count": 2, - "nodes": [ - { - "__typename": "FlowSetting", - "id": "gid://sagittarius/FlowSetting/1", - "createdAt": "2026-04-13T13:50:15Z", - "updatedAt": "2026-04-13T13:50:15Z", - "flowSettingIdentifier": "httpURL", - "value": "/test" - }, - { - "__typename": "FlowSetting", - "id": "gid://sagittarius/FlowSetting/2", - "createdAt": "2026-04-13T13:50:15Z", - "updatedAt": "2026-04-13T13:50:15Z", - "flowSettingIdentifier": "httpMethod", - "value": "GET" - } - ], - "pageInfo": { - "__typename": "PageInfo", - "endCursor": "Mg", - "hasNextPage": false - } - }, - "startingNodeId": "gid://sagittarius/NodeFunction/1", - "type": { - "__typename": "FlowType", - "id": "gid://sagittarius/FlowType/1" - }, - "disabledReason": null, - "userAbilities": { - "__typename": "FlowUserAbilities", - "deleteFlow": true - } - }; - - const suggestions = getReferenceSuggestions(flow, "gid://sagittarius/NodeFunction/2", 0, FUNCTION_SIGNATURES, DATA_TYPES); - - //expect(suggestions.some(s => !s.nodeFunctionId)).toBe(true); - }); -}); diff --git a/test/nodeSuggestion.test.ts b/test/nodeSuggestion.test.ts deleted file mode 100644 index 5254654..0000000 --- a/test/nodeSuggestion.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {describe, expect, it} from "vitest"; -import {getNodeSuggestions} from "../src/suggestion/getNodeSuggestions"; -import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data"; - -describe("getNodeSuggestions", () => { - it("should suggest functions with compatible return types and prioritize exact matches", () => { - - // We are looking for suggestions for a 'number' type - const suggestions = getNodeSuggestions("TEXT", FUNCTION_SIGNATURES, DATA_TYPES); - - const map = suggestions.map(s => s.functionDefinition?.identifier) - - expect(map).toEqual(expect.arrayContaining([ - 'std::list::first', - 'std::list::at', - 'std::list::last', - 'std::list::pop', - 'std::list::find_last', - 'std::list::join', - 'std::list::find', - 'std::number::as_text', - 'std::object::get', - 'std::boolean::as_text', - 'std::text::decode', - 'std::text::trim', - 'std::text::reverse', - 'std::text::replace_last', - 'std::text::lowercase', - 'std::text::encode', - 'std::text::remove', - 'std::text::swapcase', - 'std::text::append', - 'std::text::insert', - 'std::text::prepend', - 'std::text::hex', - 'std::text::replace', - 'std::text::from_ascii', - 'std::text::octal', - 'std::text::capitalize', - 'std::text::replace_first', - 'std::text::uppercase', - 'std::text::at', - 'std::control::return', - 'std::control::value' - ])) - - expect(map).toEqual(expect.not.arrayContaining([ - "std::number::add" - ])) - - }); - - it("should suggest functions with compatible return types and prioritize exact matches for boolean", () => { - - // We are looking for suggestions for a 'number' type - const suggestions = getNodeSuggestions("BOOLEAN", FUNCTION_SIGNATURES, DATA_TYPES); - - const map = suggestions.map(s => s.functionDefinition?.identifier) - - expect(map).toEqual(expect.arrayContaining([ - 'std::list::first', - 'std::list::at', - 'std::list::last', - 'std::list::pop', - 'std::list::find_last', - 'std::list::is_empty', - 'std::list::find', - 'std::number::is_zero', - 'std::number::has_digits', - 'std::number::is_greater', - 'std::number::is_equal', - 'std::number::is_positive', - 'std::number::is_less', - 'std::object::contains_key', - 'std::object::get', - 'std::boolean::from_number', - 'std::boolean::is_equal', - 'std::boolean::from_text', - 'std::boolean::negate', - 'std::text::is_equal', - 'std::text::ends_with', - 'std::text::start_with', - 'std::text::contains', - 'std::control::return', - 'std::control::value' - ])) - - }); -}); From 6f7adda8a9a201053c4bd591252b8a4f2143f0d5 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 00:56:36 +0200 Subject: [PATCH 05/12] feat: add getNodes utility for filtering and transforming TypeScript function declarations into compatible node functions --- src/util/nodes.util.ts | 234 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/util/nodes.util.ts diff --git a/src/util/nodes.util.ts b/src/util/nodes.util.ts new file mode 100644 index 0000000..8f0828e --- /dev/null +++ b/src/util/nodes.util.ts @@ -0,0 +1,234 @@ +import ts from "typescript"; +import {FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import {isSubFlow} from "./schema.util"; + +/** + * Filters and transforms function declarations into a collection of compatible node functions. + * + * This utility function analyzes TypeScript function declarations and matches them against + * a target parameter type. It returns node functions for all functions whose return types + * are assignable to the specified parameter type. Each node function is enriched with + * metadata including function definitions and parameter information. + * + * @param {ts.TypeChecker} checker - The TypeScript type checker instance used to analyze + * type information and verify type compatibility + * @param {ts.FunctionDeclaration[]} functionDeclarations - Array of TypeScript function + * declarations to be filtered and analyzed + * @param {FunctionDefinition[]} functions - Array of function definitions containing + * metadata about functions, including identifiers and parameter definitions + * @param {ts.Type} paramType - The target parameter type used to filter compatible + * functions. Only functions with return types assignable to this type are included + * + * @returns {NodeFunction[]} Array of node functions that are compatible with the + * specified parameter type. Returns an empty array if the parameter type + * is a sub-flow or if no compatible functions are found + * + * @example + * const compatibleNodes = getNodes(checker, funcDecls, funcDefs, stringType); + */ +export const getNodes = ( + checker: ts.TypeChecker, + functionDeclarations: ts.FunctionDeclaration[], + functions: FunctionDefinition[], + paramType: ts.Type +): NodeFunction[] => { + // Early exit: if the parameter type is a sub-flow, no node functions are applicable + if (isSubFlow(paramType)) { + return []; + } + + // Transform each function declaration into a node function if it matches the parameter type + return functionDeclarations.flatMap((func) => { + const nodeFunction = createNodeFunctionIfCompatible(checker, func, functions, paramType); + return nodeFunction ? [nodeFunction] : []; + }); +}; + +/** + * Creates a node function from a function declaration if its return type is compatible + * with the specified parameter type. + * + * This helper function handles the type checking logic and node function construction. + * It extracts the function signature, resolves type parameters, verifies type compatibility, + * and builds a complete node function object with parameter definitions. + * + * @param {ts.TypeChecker} checker - The TypeScript type checker for type analysis + * @param {ts.FunctionDeclaration} func - The function declaration to process + * @param {FunctionDefinition[]} functions - Array of function definitions for metadata lookup + * @param {ts.Type} paramType - The target parameter type for compatibility check + * + * @returns {NodeFunction | null} A node function object if the function is compatible + * with the parameter type, otherwise null + * + * @private + */ +const createNodeFunctionIfCompatible = ( + checker: ts.TypeChecker, + func: ts.FunctionDeclaration, + functions: FunctionDefinition[], + paramType: ts.Type +): NodeFunction | null => { + // Extract the function signature and its return type + const signature = checker.getSignatureFromDeclaration(func); + const returnType = checker.getReturnTypeOfSignature(signature!); + + // Simplify the return type by resolving type parameters + const simplifiedReturnType = resolveReturnType(checker, returnType); + + // Only proceed if the return type is assignable to the target parameter type + if (!checker.isTypeAssignableTo(simplifiedReturnType, paramType)) { + return null; + } + + // Extract and normalize the function name + const functionName = normalizeFunctionName(func.name?.getText()); + const functionDefinition = functions.find((f) => f.identifier === functionName); + + // Build and return the node function object + return buildNodeFunction(functionDefinition); +}; + +/** + * Resolves a return type by handling type parameters with their base constraints. + * + * If the provided type is a type parameter, this function retrieves its base constraint. + * If no base constraint exists, it falls back to the `any` type. Otherwise, it returns + * the type as-is. + * + * @param {ts.TypeChecker} checker - The TypeScript type checker + * @param {ts.Type} returnType - The return type to resolve + * + * @returns {ts.Type} The resolved return type with type parameters replaced by their + * base constraints or the `any` type as a fallback + * + * @private + */ +const resolveReturnType = (checker: ts.TypeChecker, returnType: ts.Type): ts.Type => { + if (returnType.isTypeParameter()) { + return checker.getBaseConstraintOfType(returnType) || checker.getAnyType(); + } + return returnType; +}; + +/** + * Normalizes a function name by removing prefixes and replacing underscores with + * double colons (::). + * + * This function applies the following transformations: + * 1. Removes the "fn_" prefix + * 2. Replaces the first underscore with "::" + * 3. Replaces the second underscore with "::" + * + * Example: "fn_module_submodule" becomes "module::submodule" + * + * @param {string | undefined} rawName - The raw function name from the declaration + * + * @returns {string} The normalized function name, or an empty string if the input + * is undefined + * + * @private + */ +const normalizeFunctionName = (rawName: string | undefined): string => { + if (!rawName) { + return ""; + } + return rawName + .replace("fn_", "") + .replace("_", "::") + .replace("_", "::"); +}; + +/** + * Builds a complete node function object with all required metadata and parameters. + * + * Constructs a GraphQL-compatible node function structure that includes: + * - GraphQL type information (__typename and id) + * - Function definition metadata (identifier and id) + * - Parameter definitions with default values if applicable + * + * @param {FunctionDefinition | undefined} functionDefinition - The function definition + * containing metadata and parameter information. If undefined, the node function + * will still be created with null references for the definition. + * + * @returns {NodeFunction} A fully constructed node function object ready for use in + * the GraphQL schema + * + * @private + */ +const buildNodeFunction = ( + functionDefinition: FunctionDefinition | undefined +): NodeFunction => { + const hasParameters = + (functionDefinition?.parameterDefinitions?.nodes?.length ?? 0) > 0; + + const baseNode: NodeFunction = { + __typename: "NodeFunction", + id: `gid://sagittarius/NodeFunction/1`, + functionDefinition: { + __typename: "FunctionDefinition", + id: functionDefinition?.id, + identifier: functionDefinition?.identifier, + }, + }; + + if (hasParameters) { + baseNode.parameters = buildParameterConnection( + functionDefinition?.parameterDefinitions?.nodes || [] + ) as any; + } + + return baseNode; +}; + +/** + * Builds a parameter connection object containing all parameter definitions. + * + * Transforms an array of parameter definitions into a GraphQL-compatible parameter + * connection structure. Each parameter is enriched with its definition metadata and + * default value if available. + * + * @param {any[]} parameterNodes - Array of parameter definition nodes to be transformed + * + * @returns {Object} A parameter connection object with __typename and an array of + * node parameters, each containing parameter definition and default value + * + * @private + */ +const buildParameterConnection = (parameterNodes: any[]) => { + return { + __typename: "NodeParameterConnection", + nodes: parameterNodes.map((p) => buildNodeParameter(p)), + }; +}; + +/** + * Builds a single node parameter object from a parameter definition. + * + * Constructs a GraphQL-compatible parameter node that includes: + * - Parameter definition metadata (id and identifier) + * - Default value if available, otherwise null + * + * @param {any} parameterDef - The parameter definition object containing id, + * identifier, and optional defaultValue + * + * @returns {Object} A node parameter object with __typename, parameterDefinition, + * and default value information + * + * @private + */ +const buildNodeParameter = (parameterDef: any) => { + return { + __typename: "NodeParameter", + parameterDefinition: { + __typename: "ParameterDefinition", + id: parameterDef?.id, + identifier: parameterDef?.identifier, + }, + value: parameterDef?.defaultValue + ? { + __typename: "LiteralValue", + value: parameterDef.defaultValue.value, + } + : null, + }; +}; From c769394385e74b102819bc008313c7f08d46f8ce Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 00:56:41 +0200 Subject: [PATCH 06/12] feat: add getReferences utility for extracting reference values from TypeScript symbols --- src/util/references.util.ts | 308 ++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 src/util/references.util.ts diff --git a/src/util/references.util.ts b/src/util/references.util.ts new file mode 100644 index 0000000..987bc43 --- /dev/null +++ b/src/util/references.util.ts @@ -0,0 +1,308 @@ +import ts from "typescript"; +import {ReferencePath, ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; + +/** + * Extracts reference values from a collection of TypeScript symbols. + * + * This function processes symbols with specific prefixes (node_, p_, flow_) and creates + * corresponding ReferenceValue objects. It filters out invalid symbols and symbols that + * are declared after the current node position. + * + * @param checker - TypeScript TypeChecker instance for type information resolution + * @param node - The VariableDeclaration context in which symbols are analyzed + * @param paramType - The expected parameter type for property matching during extraction + * @param symbols - The array of symbols to process and analyze + * @returns An array of ReferenceValue objects representing extracted references with their paths + * + * @example + * // Extracts all references from symbols prefixed with node_, p_, or flow_ + * const references = getReferences(checker, variableDecl, expectedType, [symbol1, symbol2]); + */ +export const getReferences = ( + checker: ts.TypeChecker, + node: ts.VariableDeclaration, + paramType: ts.Type, + symbols: ts.Symbol[] +): ReferenceValue[] => { + return symbols.flatMap((symbol) => { + const name = symbol.getName(); + + // Filter symbols by required prefix + if (!isValidSymbolPrefix(name)) { + return []; + } + + // Validate symbol declaration exists and is declared before current node + const symbolDeclaration = symbol.getDeclarations()?.[0]; + if (!symbolDeclaration || symbolDeclaration.getEnd() >= node.getEnd()!) { + return []; + } + + const symbolType = checker.getTypeOfSymbolAtLocation(symbol, node); + + // Process symbol based on its prefix + if (name.startsWith("node_")) { + return processNodeSymbol(name, symbolType, checker, paramType); + } else if (name.startsWith("p_")) { + return processParameterSymbol(name, symbolType, checker, paramType); + } else if (name.startsWith("flow_")) { + return processFlowSymbol(symbolType, checker, paramType); + } + + return []; + }); +}; + +/** + * Checks if a symbol name has a valid prefix for reference extraction. + * + * Valid prefixes are: node_, p_, flow_ + * + * @param name - The symbol name to validate + * @returns True if the name starts with a valid prefix, false otherwise + */ +const isValidSymbolPrefix = (name: string): boolean => { + return name.startsWith("node_") || name.startsWith("p_") || name.startsWith("flow_"); +}; + +/** + * Processes a node symbol and creates corresponding reference values. + * + * Node symbols represent references to node functions. The function extracts the node ID + * from the symbol name by reversing the name encoding, then extracts all matching object + * properties from the symbol type. + * + * @param name - The symbol name starting with "node_" + * @param symbolType - The TypeScript type of the symbol + * @param checker - TypeScript TypeChecker for type operations + * @param paramType - The expected parameter type for property matching + * @returns Array of ReferenceValue objects for this node symbol + */ +const processNodeSymbol = ( + name: string, + symbolType: ts.Type, + checker: ts.TypeChecker, + paramType: ts.Type +): ReferenceValue[] => { + // Skip void types + if ((symbolType.flags & ts.TypeFlags.Void) !== 0) { + return []; + } + + // Decode the node function ID from the symbol name + const nodeFunctionId = decodeIdentifier(name.replace("node_", "")); + + // Extract all properties that match the expected parameter type + const propertyPaths = extractObjectProperties(symbolType, checker, paramType); + + return propertyPaths.flatMap(({ path }) => { + const referenceValue: ReferenceValue = { + __typename: "ReferenceValue", + nodeFunctionId: nodeFunctionId as any, + }; + + if (path.length > 0) { + referenceValue.referencePath = path; + } + + return referenceValue; + }); +}; + +/** + * Processes a parameter symbol and creates corresponding reference values. + * + * Parameter symbols represent references to function parameters. They contain encoded + * information about the node function ID, parameter index, and input index. If the symbol + * type is a tuple, each tuple element is processed separately. + * + * @param name - The symbol name starting with "p_" + * @param symbolType - The TypeScript type of the symbol + * @param checker - TypeScript TypeChecker for type operations + * @param paramType - The expected parameter type for property matching + * @returns Array of ReferenceValue objects for this parameter symbol + */ +const processParameterSymbol = ( + name: string, + symbolType: ts.Type, + checker: ts.TypeChecker, + paramType: ts.Type +): ReferenceValue[] => { + // Decode parameter information from symbol name + const { nodeFunctionId, paramIndexFromName } = decodeParameterName(name); + + // If the symbol type is not a tuple, return empty array + if (!checker.isTupleType(symbolType)) { + return []; + } + + const typeReference = symbolType as ts.TypeReference; + const typeArguments = checker.getTypeArguments(typeReference); + + return typeArguments.flatMap((tupleElementType, tupleIndex) => { + const propertyPaths = extractObjectProperties(tupleElementType, checker, paramType); + + return propertyPaths.flatMap(({ path }) => { + const referenceValue: ReferenceValue = { + __typename: "ReferenceValue", + nodeFunctionId: nodeFunctionId as any, + parameterIndex: isNaN(paramIndexFromName) ? 0 : paramIndexFromName, + inputIndex: tupleIndex, + inputTypeIdentifier: (typeReference.target as any).labeledElementDeclarations?.[ + tupleIndex + ].name.getText(), + }; + + if (path.length > 0) { + referenceValue.referencePath = path; + } + + return referenceValue; + }); + }); +}; + +/** + * Processes a flow symbol and creates corresponding reference values. + * + * Flow symbols represent references within data flows. They do not have an associated + * node function ID (it is null) and extract properties that match the expected type. + * + * @param symbolType - The TypeScript type of the symbol + * @param checker - TypeScript TypeChecker for type operations + * @param paramType - The expected parameter type for property matching + * @returns Array of ReferenceValue objects for this flow symbol + */ +const processFlowSymbol = ( + symbolType: ts.Type, + checker: ts.TypeChecker, + paramType: ts.Type +): ReferenceValue[] => { + const propertyPaths = extractObjectProperties(symbolType, checker, paramType); + + return propertyPaths.flatMap(({ path }) => { + const referenceValue: ReferenceValue = { + __typename: "ReferenceValue", + nodeFunctionId: null, + }; + + if (path.length > 0) { + referenceValue.referencePath = path; + } + + return referenceValue; + }); +}; + +/** + * Decodes an encoded identifier string back to its original form. + * + * The encoding scheme is: + * - ___ → :// + * - __ → / + * - _ → / + * + * @param encoded - The encoded identifier string + * @returns The decoded identifier + */ +const decodeIdentifier = (encoded: string): string => { + return encoded.replace(/___/g, "://").replace(/__/g, "/").replace(/_/g, "/"); +}; + +/** + * Decodes parameter name to extract node function ID and parameter index. + * + * The parameter name format is: p__ + * The rawId is then decoded using decodeIdentifier. + * + * @param name - The parameter symbol name + * @returns Object containing decoded nodeFunctionId and paramIndexFromName + */ +const decodeParameterName = (name: string): { nodeFunctionId: string; paramIndexFromName: number } => { + const idPart = name.replace("p_", ""); + const lastUnderscoreIndex = idPart.lastIndexOf("_"); + const rawId = idPart.substring(0, lastUnderscoreIndex); + const paramIndexFromName = parseInt(idPart.substring(lastUnderscoreIndex + 1), 10); + + const nodeFunctionId = decodeIdentifier(rawId); + + return { nodeFunctionId, paramIndexFromName }; +}; + +/** + * Recursively extracts object properties from a type that match an expected type. + * + * This function performs a depth-first traversal of an object's property tree. It collects + * all paths (property chains) where the type at that path is assignable to the expected type. + * For example, if type is {a: {b: string}} and expectedType is string, it returns the path [a, b]. + * + * The recursion continues into nested object properties, building up the path as it traverses. + * Primitive types (string, number, boolean, etc.) are treated as leaf nodes. + * + * @param type - The type to extract properties from + * @param checker - TypeScript TypeChecker for type comparison operations + * @param expectedType - The target type to match when extracting properties + * @param currentPath - The current property path being built during recursion (default: empty array) + * @returns An array of objects containing the property path and the type at that path + * + * @example + * // For type {user: {name: string, age: number}} with expectedType = string + * // Returns: [{path: [{path: 'user'}, {path: 'name'}], type: stringType}] + */ +const extractObjectProperties = ( + type: ts.Type, + checker: ts.TypeChecker, + expectedType: ts.Type, + currentPath: ReferencePath[] = [] +): Array<{ path: ReferencePath[]; type: ts.Type }> => { + const results: Array<{ path: ReferencePath[]; type: ts.Type }> = []; + + // Check if the current type matches the expected type + if (checker.isTypeAssignableTo(type, expectedType)) { + results.push({ path: currentPath, type }); + } + + // Recursively traverse into object properties + if (isRealObjectType(type)) { + const properties = type.getProperties(); + if (properties && properties.length > 0) { + properties.forEach((property) => { + const propType = checker.getTypeOfSymbolAtLocation(property, property.valueDeclaration!); + const propName = property.getName(); + const newPath = [...currentPath, { path: propName }]; + + // Recurse into nested properties + results.push(...extractObjectProperties(propType, checker, expectedType, newPath)); + }); + } + } + + return results; +}; + +/** + * Determines whether a type is a real object type or a primitive. + * + * This function checks if a type is NOT one of the primitive types (string, number, boolean, + * undefined, null, bigint, symbol). Any type that is not a primitive is considered a real object type. + * + * @param type - The type to check + * @returns True if the type is a real object type, false if it's a primitive type + * + * @example + * isRealObjectType(stringType) // false + * isRealObjectType(objectType) // true + * isRealObjectType(numberType) // false + */ +const isRealObjectType = (type: ts.Type): boolean => { + const primitiveFlags = + ts.TypeFlags.String | + ts.TypeFlags.Number | + ts.TypeFlags.Boolean | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.BigInt | + ts.TypeFlags.ESSymbol; + + return (type.flags & primitiveFlags) === 0; +}; From 61afdf8e96755d1367f40d1f92c8937ec738637f Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 00:56:46 +0200 Subject: [PATCH 07/12] feat: implement getSchema function for generating structured schemas from TypeScript types --- src/util/schema.util.ts | 339 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 src/util/schema.util.ts diff --git a/src/util/schema.util.ts b/src/util/schema.util.ts new file mode 100644 index 0000000..6e74daa --- /dev/null +++ b/src/util/schema.util.ts @@ -0,0 +1,339 @@ +import ts, { FunctionDeclaration } from "typescript"; +import { getValues } from "./values.util"; +import { getReferences } from "./references.util"; +import { getNodes } from "./nodes.util"; +import { + FunctionDefinition, + LiteralValue, + NodeFunction, + ReferenceValue, +} from "@code0-tech/sagittarius-graphql-types"; + + +/** + * Base interface for all input types. + * Provides common properties for suggestions and input metadata. + */ +interface Input { + /** The type of input (string representation) */ + input?: string; + /** Array of suggested values (functions, references, or literals) */ + suggestions?: (NodeFunction | ReferenceValue | LiteralValue)[]; +} + +/** + * Represents a generic input type with no specific structure. + * Used as a fallback when the type cannot be determined. + */ +interface GenericInput { + input?: "generic"; +} + +/** + * Represents a sub-flow input type (callable/function type). + * Used for types that have call signatures. + */ +interface SubFlowInput { + input?: "sub-flow"; +} + +/** + * Represents primitive input types: boolean, number, text, or select. + * Extends the base Input interface to include suggestions. + */ +interface PrimitiveInput extends Input { + input?: "boolean" | "number" | "text" | "select"; +} + +/** + * Represents a data object input type with structured properties. + * Includes property definitions and required field tracking. + */ +interface DataInput extends Input { + input?: "data"; + /** Record mapping property names to their schemas */ + properties?: Record; + /** Array of required property names */ + required?: string[]; +} + +/** + * Represents a list/array input type with item schemas. + * Supports homogeneous or heterogeneous arrays. + */ +interface ListInput extends Input { + input?: "list"; + /** Schema or array of schemas for list items */ + items?: Schema | Schema[]; +} + +/** + * Represents a complex type input with properties and required fields. + * Similar to DataInput but used for type definitions. + */ +interface TypeInput extends Input { + input?: "type"; + /** Record mapping property names to their schemas */ + properties?: Record; + /** Array of required property names */ + required?: string[]; +} + +/** + * Union type representing all possible schema input types. + * Discriminated union based on the 'input' field. + */ +export type Schema = + | PrimitiveInput + | DataInput + | ListInput + | TypeInput + | SubFlowInput + | GenericInput; + + +/** + * Generates a schema definition for a given TypeScript type. + * + * This function analyzes a TypeScript type and produces a structured schema + * that describes how the type should be presented and validated. It handles: + * - Primitive types (boolean, number, string) + * - Union types of primitives + * - Array/Tuple types + * - Complex object types with properties + * - Sub-flow types (callables) + * + * For each type, the function also collects suggestions from: + * - Literal values from the type definition + * - Variable references available in scope + * - Function node suggestions based on parameter type + * + * @param checker - TypeScript type checker for type analysis + * @param node - The variable declaration node being analyzed + * @param parameterType - The type to generate a schema for + * @param functionDeclarations - Array of function declaration nodes + * @param functions - Array of function definitions for matching + * @returns A Schema object describing how to handle the parameter type + */ +export const getSchema = ( + checker: ts.TypeChecker, + node: ts.VariableDeclaration, + parameterType: ts.Type, + functionDeclarations: FunctionDeclaration[], + functions: FunctionDefinition[] +): Schema => { + // Collect all available suggestions for this parameter + const literalValueSuggestions = getValues(parameterType); + const referenceSuggestions = getReferences( + checker, + node!, + parameterType, + checker.getSymbolsInScope(node!, ts.SymbolFlags.Variable) + ); + const nodeSuggestions = getNodes( + checker, + functionDeclarations, + functions, + parameterType + ); + const suggestions = { + suggestions: [ + ...literalValueSuggestions, + ...referenceSuggestions, + ...nodeSuggestions, + ], + }; + + // Check primitive literal union first (e.g., "a" | "b" | "c") + if (isPrimitiveLiteralUnion(parameterType)) { + return { input: "select", ...suggestions }; + } + + // Check individual primitive types + if (isBoolean(parameterType)) { + return { input: "boolean", ...suggestions }; + } + if (isNumber(parameterType)) { + return { input: "number", ...suggestions }; + } + if (isString(parameterType)) { + return { input: "text", ...suggestions }; + } + + // Check if type has call signatures (is callable/sub-flow) + if (isSubFlow(parameterType)) { + return { input: "sub-flow", ...suggestions }; + } + + // Handle array and tuple types + if (isArrayType(checker, parameterType)) { + const itemType = checker.getTypeArguments( + parameterType as ts.TypeReference + )[0]; + const itemTypes = itemType.isUnion() ? itemType.types : [itemType]; + const itemSchemas = itemTypes.map((itemType) => + getSchema(checker, node, itemType, functionDeclarations, functions) + ); + + return { + input: "list", + items: itemSchemas.length === 1 ? itemSchemas[0] : itemSchemas, + ...suggestions, + }; + } + + // Handle complex object types with properties + if ((parameterType.flags & ts.TypeFlags.Object) !== 0) { + const properties: Record = {}; + const required: string[] = []; + + // Iterate through all properties of the object type + for (const property of checker.getPropertiesOfType(parameterType)) { + const declaration = + property.valueDeclaration ?? property.declarations?.[0]; + + if (!declaration) continue; + + const propertyType = checker.getTypeOfSymbolAtLocation( + property, + declaration + ); + + // Determine if the property is optional + const isOptional = + (property.flags & ts.SymbolFlags.Optional) !== 0 || + (propertyType.isUnion() && + propertyType.types.some( + (t) => (t.flags & ts.TypeFlags.Undefined) !== 0 + )); + + // Filter out undefined type from union types + const propertyTypes = propertyType.isUnion() + ? propertyType.types.filter( + (t) => (t.flags & ts.TypeFlags.Undefined) === 0 + ) + : [propertyType]; + + // Recursively generate schemas for property types + const propertySchemas = propertyTypes.map((type) => + getSchema(checker, node, type, functionDeclarations, functions) + ); + + properties[property.name] = + propertySchemas.length === 1 ? propertySchemas[0] : propertySchemas; + + // Track required properties + if (!isOptional) { + required.push(property.name); + } + } + + return { + input: "data", + properties, + required, + ...suggestions, + }; + } + + // Fallback for unknown or generic types + return { + input: "generic", + }; +}; + +/** + * Checks if a type is a boolean type (either boolean or boolean literal). + * + * This function checks for both the general boolean type and specific boolean + * literal types (true, false). + * + * @param type - The type to check + * @returns True if the type is a boolean or boolean literal, false otherwise + */ +function isBoolean(type: ts.Type): boolean { + return ( + (type.flags & ts.TypeFlags.Boolean) !== 0 || + (type.flags & ts.TypeFlags.BooleanLiteral) !== 0 + ); +} + +/** + * Checks if a type is a number type (either number or number literal). + * + * This function checks for both the general number type and specific numeric + * literal types (42, 3.14, etc.). + * + * @param type - The type to check + * @returns True if the type is a number or number literal, false otherwise + */ +function isNumber(type: ts.Type): boolean { + return ( + (type.flags & ts.TypeFlags.Number) !== 0 || + (type.flags & ts.TypeFlags.NumberLiteral) !== 0 + ); +} + +/** + * Checks if a type is a string type (either string or string literal). + * + * This function checks for both the general string type and specific string + * literal types ("hello", "world", etc.). + * + * @param type - The type to check + * @returns True if the type is a string or string literal, false otherwise + */ +function isString(type: ts.Type): boolean { + return ( + (type.flags & ts.TypeFlags.String) !== 0 || + (type.flags & ts.TypeFlags.StringLiteral) !== 0 + ); +} + +/** + * Checks if a type is any primitive type (string, number, or boolean). + * + * @param type - The type to check + * @returns True if the type is a string, number, or boolean, false otherwise + */ +function isPrimitive(type: ts.Type): boolean { + return isString(type) || isNumber(type) || isBoolean(type); +} + +/** + * Checks if a type is a union of primitive types only. + * + * This function is used to identify union types that can be represented as + * a select input with predefined options (e.g., "a" | "b" | "c" or 1 | 2 | 3). + * + * @param type - The type to check + * @returns True if the type is a union where all members are primitives, false otherwise + */ +function isPrimitiveLiteralUnion(type: ts.Type): boolean { + if (!type.isUnion()) return false; + return type.types.every(isPrimitive); +} + +/** + * Checks if a type is an array or tuple type. + * + * @param checker - TypeScript type checker for type analysis + * @param type - The type to check + * @returns True if the type is an array or tuple, false otherwise + */ +function isArrayType(checker: ts.TypeChecker, type: ts.Type): boolean { + return checker.isArrayType(type) || checker.isTupleType(type); +} + +/** + * Checks if a type is a callable type (has call signatures). + * + * A type with call signatures can be invoked like a function. This is used + * to identify sub-flow types that represent workflow steps. + * + * @param type - The type to check + * @returns True if the type has call signatures, false otherwise + */ +export function isSubFlow(type: ts.Type): boolean { + return type.getCallSignatures().length > 0; +} \ No newline at end of file From e07a58f4e8377c0b9bb171b9bb10feab52e52afc Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 00:56:51 +0200 Subject: [PATCH 08/12] feat: add getValues function for extracting literal values from TypeScript types --- src/util/values.util.ts | 71 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/util/values.util.ts diff --git a/src/util/values.util.ts b/src/util/values.util.ts new file mode 100644 index 0000000..db55717 --- /dev/null +++ b/src/util/values.util.ts @@ -0,0 +1,71 @@ +import ts, { NumberLiteralType, StringLiteralType } from "typescript"; +import { LiteralValue } from "@code0-tech/sagittarius-graphql-types"; + +/** + * Extracts literal values from a TypeScript type. + * + * This function recursively processes TypeScript types and extracts all literal values, + * including string literals, number literals, and boolean literals. For union types, + * it flattens and combines all literal values from each constituent type. + * + * @param type - The TypeScript type to extract values from + * @returns An array of LiteralValue objects representing all literal values found in the type + * + * @example + * // For a type like "red" | "blue" | 42 + * const values = getValues(type); + * // Returns: + * // [ + * // { value: "red", __typename: "LiteralValue" }, + * // { value: "blue", __typename: "LiteralValue" }, + * // { value: "42" } + * // ] + */ +export const getValues = (type: ts.Type): LiteralValue[] => { + // Handle union types by recursively extracting values from each constituent type + if (type.isUnion()) { + return type.types.flatMap(getValues); + } + + // Extract string literal values + if (type.isStringLiteral()) { + return [ + { + value: (type as StringLiteralType).value, + __typename: "LiteralValue", + }, + ]; + } + + // Extract number literal values, converting to string representation + if (type.isNumberLiteral()) { + return [ + { + value: (type as NumberLiteralType).value.toString(), + }, + ]; + } + + // Extract boolean true literal values + if ((type as any).intrinsicName === "true") { + return [ + { + value: "true", + __typename: "LiteralValue", + }, + ]; + } + + // Extract boolean false literal values + if ((type as any).intrinsicName === "false") { + return [ + { + value: "false", + __typename: "LiteralValue", + }, + ]; + } + + // Return empty array if no literal values are found + return []; +}; From 1c58340b1695bfe93dafc8b0a9e4c4fb54c5e44c Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 00:56:56 +0200 Subject: [PATCH 09/12] feat: update exports to reflect new schema organization for suggestions --- src/index.ts | 6 +-- test/valueSuggestions.test.ts | 94 ----------------------------------- 2 files changed, 3 insertions(+), 97 deletions(-) delete mode 100644 test/valueSuggestions.test.ts diff --git a/src/index.ts b/src/index.ts index 57eea90..c3cf1ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,8 @@ export * from './extraction/getTypeVariant'; export * from './extraction/getValueFromType'; export * from './extraction/getTypesFromNode'; export * from './extraction/getTypesFromFunction'; -export * from './suggestion/getNodeSuggestions'; -export * from './suggestion/getReferenceSuggestions'; -export * from './suggestion/getValueSuggestions'; +export * from './schema/getNodeSuggestions'; +export * from './schema/getReferenceSuggestions'; +export * from './schema/getValueSuggestions'; export * from './validation/getFlowValidation'; export * from './validation/getValueValidation'; diff --git a/test/valueSuggestions.test.ts b/test/valueSuggestions.test.ts deleted file mode 100644 index ad3fff5..0000000 --- a/test/valueSuggestions.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { getValueSuggestions } from '../src/suggestion/getValueSuggestions'; -import {DATA_TYPES} from "./data"; - -describe('getLanguageServiceSuggestions', () => { - - it('should suggest string literals from a simple union', () => { - const type = '"GET" | "POST" | "DELETE"'; - const suggestions = getValueSuggestions(type); - const values = suggestions.map(s => s.value); - - expect(values).toContain('GET'); - expect(values).toContain('POST'); - expect(values).toContain('DELETE'); - - expect(suggestions.length).toBe(3); - }); - - it('should suggest boolean values when type is boolean', () => { - const type = 'boolean'; - const suggestions = getValueSuggestions(type); - const values = suggestions.map(s => s.value); - - // TypeScript schlägt bei boolean oft true/false vor - expect(values).toContain('true'); - expect(values).toContain('false'); - }); - - it('should work with mixed string and number literals (as strings)', () => { - const type = '"A" | 1 | 2'; - const suggestions = getValueSuggestions(type); - const values = suggestions.map(s => s.value); - - expect(values).toContain('A'); - // Hinweis: Der Language Service gibt Namen oft als Strings zurück - expect(values).toContain('1'); - expect(values).toContain('2'); - }); - - it('should return empty array for general string type without literals', () => { - const type = 'string'; - const suggestions = getValueSuggestions(type); - const values = suggestions.map(s => s.value); - - // Wenn es nur 'string' ist, gibt es keine spezifischen Literale zu vervollständigen - // Der Language Service könnte globale Variablen vorschlagen, daher prüfen wir - // hier spezifisch auf das Ausbleiben deiner Literale. - expect(values).not.toContain('GET'); - }); - - it('should resolve suggestions from type aliases (Inference Check)', () => { - // Da wir den Language Service nutzen, können wir auch Aliase testen, - // sofern sie im Host-Content definiert sind. - // Für diesen Test müsste der Content im Service-Host erweitert werden. - const type = '"active" | "inactive"'; - const suggestions = getValueSuggestions(type); - const values = suggestions.map(s => s.value); - - expect(values).toEqual(expect.arrayContaining(['active', 'inactive'])); - }); - - it('should handle complex template literal types', () => { - const type = '`top-${"left" | "right"}`'; - const suggestions = getValueSuggestions(type); - const values = suggestions.map(s => s.value); - - // Das ist die Stärke des Language Service! - expect(values).toContain('top-left'); - expect(values).toContain('top-right'); - }); - - it('should handle empty or invalid types gracefully', () => { - const type = ''; - const suggestions = getValueSuggestions(type); - const values = suggestions.map(s => s.value); - - expect(Array.isArray(values)).toBe(true); - expect(values.length).toBe(0); - }); - - it('should suggest values from a type alias with dataTypes parameter', () => { - const type = 'HTTP_METHOD'; - const suggestions = getValueSuggestions(type, DATA_TYPES as any); - const values = suggestions.map(s => s.value); - - expect(values).toContain('GET'); - expect(values).toContain('POST'); - expect(values).toContain('DELETE'); - expect(values).toContain('PUT'); - expect(values).toContain('PATCH'); - expect(values).toContain('HEAD'); - expect(suggestions.length).toBe(6); - }); -}); \ No newline at end of file From 15ee95c6b173fc2cd529f624080b9f2c7f3ed168 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 17:44:53 +0200 Subject: [PATCH 10/12] feat: update exports and remove unused inferred types utility --- src/extraction/getTypeVariant.ts | 94 ----------------- src/extraction/getTypesFromFunction.ts | 137 ------------------------ src/extraction/getTypesFromNode.ts | 84 --------------- src/index.ts | 7 +- src/utils.ts | 69 ------------ test/getTypeVariant.test.ts | 67 ------------ test/getTypesFromFunction.test.ts | 66 ------------ test/getTypesFromNode.test.ts | 140 ------------------------- 8 files changed, 1 insertion(+), 663 deletions(-) delete mode 100644 src/extraction/getTypeVariant.ts delete mode 100644 src/extraction/getTypesFromFunction.ts delete mode 100644 src/extraction/getTypesFromNode.ts delete mode 100644 test/getTypeVariant.test.ts delete mode 100644 test/getTypesFromFunction.test.ts delete mode 100644 test/getTypesFromNode.test.ts diff --git a/src/extraction/getTypeVariant.ts b/src/extraction/getTypeVariant.ts deleted file mode 100644 index fd74200..0000000 --- a/src/extraction/getTypeVariant.ts +++ /dev/null @@ -1,94 +0,0 @@ -import ts from "typescript"; -import {createCompilerHost, getSharedTypeDeclarations} from "../utils"; -import {DataType} from "@code0-tech/sagittarius-graphql-types"; - -export enum DataTypeVariant { - PRIMITIVE, - TYPE, - ARRAY, - OBJECT, - NODE -} - -export interface TypeVariantResult { - identifier: string; - variant: DataTypeVariant; -} - -/** - * Determines the variant of a given TypeScript types string or DataType(s) using the TS compiler. - * - If types is a string: returns one result with the string as identifier - * - If types is a DataType: returns one result with DataType.identifier - * - If types is a DataType[]: returns results for each DataType with their identifiers - */ -export const getTypeVariant = ( - types?: string | DataType | DataType[], - dataTypes?: DataType[] -): TypeVariantResult[] => { - const typeDefs = getSharedTypeDeclarations(dataTypes); - - // Determine what we're analyzing - const isStringType = typeof types === "string"; - const isArrayType = Array.isArray(types); - const typesToAnalyze = isArrayType ? (types as DataType[]) : isStringType ? [{ identifier: types, type: types }] : [types]; - const identifiers = isArrayType - ? (types as DataType[]).map(t => t?.identifier) - : isStringType - ? [types as string] - : [(types as DataType)?.identifier]; - - const results: TypeVariantResult[] = []; - - for (let i = 0; i < typesToAnalyze.length; i++) { - const currentType = typesToAnalyze[i]; - const currentIdentifier = identifiers[i]; - const typeString = isStringType ? (types as string) : (currentType as DataType).type; - - // We declare a variable with the types to probe it - const sourceCode = ` - ${typeDefs} - ${typeString ? `type TargetType = ${typeString};` : ""} - const val: TargetType = {} as any; - `; - - const fileName = `index.ts`; - const host = createCompilerHost(fileName, sourceCode); - const sourceFile = host.getSourceFile(fileName)!; - const program = host.languageService.getProgram()!; - const checker = program.getTypeChecker(); - - let discoveredVariant: DataTypeVariant = DataTypeVariant.TYPE; - - const visit = (node: ts.Node) => { - if (ts.isVariableDeclaration(node) && node.name.getText() === "val") { - const type = checker.getTypeAtLocation(node); - - if (type.getCallSignatures().length > 0) { - discoveredVariant = DataTypeVariant.NODE; - } else if (checker.isArrayType(type)) { - discoveredVariant = DataTypeVariant.ARRAY; - } else if ( - type.isStringLiteral() || - type.isNumberLiteral() || - (type.getFlags() & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.EnumLiteral | ts.TypeFlags.BigInt | ts.TypeFlags.ESSymbol)) !== 0 - ) { - discoveredVariant = DataTypeVariant.PRIMITIVE; - } else if (type.isClassOrInterface() || (type.getFlags() & ts.TypeFlags.Object) !== 0) { - discoveredVariant = DataTypeVariant.OBJECT; - } else { - discoveredVariant = DataTypeVariant.TYPE; - } - } - ts.forEachChild(node, visit); - }; - - visit(sourceFile); - - results.push({ - identifier: currentIdentifier!, - variant: discoveredVariant - }); - } - - return results; -}; diff --git a/src/extraction/getTypesFromFunction.ts b/src/extraction/getTypesFromFunction.ts deleted file mode 100644 index c906a45..0000000 --- a/src/extraction/getTypesFromFunction.ts +++ /dev/null @@ -1,137 +0,0 @@ -import {FunctionDefinition} from "@code0-tech/sagittarius-graphql-types"; - -export interface FunctionTypes { - parameters: string[]; - returnType: string; -} - -/** - * Resolves the types of the parameters and the return type of a NodeFunction. - */ -export const getTypesFromFunction = ( - functionDefinition: FunctionDefinition -): FunctionTypes => { - const signature = functionDefinition.signature; - - if (!signature) { - return {parameters: [], returnType: "any"}; - } - - let searchStart = 0; - // Skip generics - if (signature.trim().startsWith('<')) { - let depth = 0; - for (let i = 0; i < signature.length; i++) { - const char = signature[i]; - if (char === '<') depth++; - else if (char === '>') { - depth--; - if (depth === 0) { - searchStart = i + 1; - break; - } - } - } - } - - const paramStart = signature.indexOf('(', searchStart); - if (paramStart === -1) { - return {parameters: [], returnType: "any"}; - } - - // Find matching closing paren - let paramEnd = -1; - let depth = 0; - for (let i = paramStart; i < signature.length; i++) { - const char = signature[i]; - if (char === '(') depth++; - else if (char === ')') { - depth--; - if (depth === 0) { - paramEnd = i; - break; - } - } - } - - if (paramEnd === -1) { - return {parameters: [], returnType: "any"}; - } - - const paramsString = signature.substring(paramStart + 1, paramEnd); - let returnTypeString = signature.substring(paramEnd + 1).trim(); - - // Parse return type - if (returnTypeString.startsWith(':')) { - returnTypeString = returnTypeString.substring(1).trim(); - } - const returnType = returnTypeString || "void"; - - // Parse parameters - const parameters: string[] = []; - if (paramsString.trim()) { - let currentParam = ""; - let pDepthParen = 0; - let pDepthAngle = 0; - let pDepthBrace = 0; - let pDepthBracket = 0; - - const pushParam = (p: string) => { - // Extract type from "name: Type" - // Scan for first colon at top level - let colonIndex = -1; - - let cDepthBrace = 0; - let cDepthBracket = 0; - let cDepthParen = 0; - let cDepthAngle = 0; - - for (let i = 0; i < p.length; i++) { - const c = p[i]; - if (c === '{') cDepthBrace++; - else if (c === '}') cDepthBrace--; - else if (c === '[') cDepthBracket++; - else if (c === ']') cDepthBracket--; - else if (c === '(') cDepthParen++; - else if (c === ')') cDepthParen--; - else if (c === '<') cDepthAngle++; - else if (c === '>') cDepthAngle--; - else if (c === ':' && cDepthBrace === 0 && cDepthBracket === 0 && cDepthParen === 0 && cDepthAngle === 0) { - colonIndex = i; - break; - } - } - - if (colonIndex !== -1) { - parameters.push(p.substring(colonIndex + 1).trim()); - } else { - parameters.push("any"); - } - }; - - for (const char of paramsString) { - if (char === '(') pDepthParen++; - else if (char === ')') pDepthParen--; - else if (char === '<') pDepthAngle++; - else if (char === '>') pDepthAngle--; - else if (char === '{') pDepthBrace++; - else if (char === '}') pDepthBrace--; - else if (char === '[') pDepthBracket++; - else if (char === ']') pDepthBracket--; - else if (char === ',' && pDepthParen === 0 && pDepthAngle === 0 && pDepthBrace === 0 && pDepthBracket === 0) { - pushParam(currentParam.trim()); - currentParam = ""; - continue; - } - currentParam += char; - } - if (currentParam.trim()) { - pushParam(currentParam.trim()); - } - } - - return { - parameters, - returnType - }; -}; diff --git a/src/extraction/getTypesFromNode.ts b/src/extraction/getTypesFromNode.ts deleted file mode 100644 index b807b0d..0000000 --- a/src/extraction/getTypesFromNode.ts +++ /dev/null @@ -1,84 +0,0 @@ -import {DataType, Flow, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; -import {getInferredTypesFromFlow, sanitizeId} from "../utils"; - -export interface NodeTypes { - parameters: string[]; - returnType: string; -} - -/** - * Resolves the types of the parameters and the return type of a NodeFunction. - */ -export const getTypesFromNode = ( - node?: NodeFunction, - functions?: FunctionDefinition[], - dataTypes?: DataType[] -): NodeTypes => { - if (!node) return { parameters: [], returnType: "any" }; - - const nodeId = node.id || "temp_node_id"; - - // To ensure generics and triangulated types are resolved without hardcoding, - // we must ensure the compiler has enough context. - // If some parameters are missing values, the compiler might return 'any'. - // We create a version of the node where missing values are filled with a marker - // that the TypeScript engine can use to infer the intended type from the signature. - - const nodeWithDefaults = { - ...node, - id: nodeId, - parameters: { - ...node.parameters, - nodes: node.parameters?.nodes?.map(p => { - if (p?.value) return p; - // If value is missing, we still want the compiler to see the argument position - return { ...p, value: null }; - }) || [] - } - }; - - // Create a version of the node with primitive literals removed - // This allows inferring the "expected" type of parameters (e.g. keyof T) - // rather than the specific type of the argument provided (e.g. "id"). - const nodeIdParams = nodeId + "_params"; - const nodeForParams = { - ...nodeWithDefaults, - id: nodeIdParams, - parameters: { - ...nodeWithDefaults.parameters, - nodes: nodeWithDefaults.parameters.nodes.map(p => { - // If it's a primitive literal, remove it to allow wider type inference for parameters - if (p.value?.__typename === "LiteralValue" && p.value.value !== null && typeof p.value.value !== 'object') { - return { ...p, value: null }; - } - return p; - }) - } - }; - - const mockFlow: Flow = { - id: "gid://sagittarius/Flow/0" as any, - nodes: { __typename: "NodeFunctionConnection", nodes: [nodeWithDefaults, nodeForParams] } - } as Flow; - - const inferred = getInferredTypesFromFlow(mockFlow, functions, dataTypes); - const sId = sanitizeId(nodeId); - const sIdParams = sanitizeId(nodeIdParams); - - const directParams = inferred.parameters.get(sId) || []; - const widenedParams = inferred.parameters.get(sIdParams) || []; - - // Merge parameters: prefer widened types unless they failed inference (any/unknown) - const parameters = directParams.map((p, i) => { - const wide = widenedParams[i]; - if (wide && wide !== "any" && wide !== "unknown") { - return wide; - } - return p; - }); - - return { - parameters, - returnType: inferred.nodes.get(sId) || "any", - }; -}; diff --git a/src/index.ts b/src/index.ts index c3cf1ed..d7dee69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,5 @@ export * from './extraction/getTypeFromValue'; -export * from './extraction/getTypeVariant'; export * from './extraction/getValueFromType'; -export * from './extraction/getTypesFromNode'; -export * from './extraction/getTypesFromFunction'; -export * from './schema/getNodeSuggestions'; -export * from './schema/getReferenceSuggestions'; -export * from './schema/getValueSuggestions'; export * from './validation/getFlowValidation'; export * from './validation/getValueValidation'; +export * from './schema/getNodeSchema'; diff --git a/src/utils.ts b/src/utils.ts index bf6924d..f031951 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -203,72 +203,3 @@ export function generateFlowSourceCode( )(); `; } - -export interface InferredTypes { - nodes: Map; - parameters: Map; -} - -/** - * Infers types for all nodes and parameters in a flow using the TypeScript compiler. - */ -export function getInferredTypesFromFlow( - flow?: Flow, - functions?: FunctionDefinition[], - dataTypes?: DataType[] -): InferredTypes { - const sourceCode = generateFlowSourceCode(flow, functions, dataTypes, true); - - const fileName = "index.ts"; - const host = createCompilerHost(fileName, sourceCode); - const sourceFile = host.getSourceFile(fileName)!; - const program = host.languageService.getProgram()!; - const checker = program.getTypeChecker(); - - const nodeTypes = new Map(); - const parameterTypes = new Map(); - const nodeIdToNode = new Map(); - - // Build a map of nodes for later lookup - const nodes = flow?.nodes?.nodes || []; - nodes.forEach(node => { - if (node?.id) { - nodeIdToNode.set(sanitizeId(node.id), node); - } - }); - - const visit = (n: ts.Node) => { - if (ts.isVariableDeclaration(n) && n.name.getText().startsWith("node_")) { - const nodeId = n.name.getText().replace("node_", ""); - const type = checker.getTypeAtLocation(n); - nodeTypes.set(nodeId, checker.typeToString(type, n, ts.TypeFormatFlags.NoTruncation)); - - if (n.initializer && ts.isCallExpression(n.initializer)) { - const sig = checker.getResolvedSignature(n.initializer); - if (sig) { - // getResolvedSignature returns the signature with generics resolved based on actual arguments - const resolvedParams = sig.getParameters().map((p) => { - const t = checker.getTypeOfSymbolAtLocation(p, n.initializer!); - return checker.typeToString(t, n.initializer, ts.TypeFormatFlags.NoTruncation); - }); - parameterTypes.set(nodeId, resolvedParams); - } - } - } - if (ts.isReturnStatement(n) && n.expression && ts.isCallExpression(n.expression)) { - // Special handling for std::control::return which doesn't have a node_ variable - const call = n.expression; - const sig = checker.getResolvedSignature(call); - if (sig) { - // We need to find the node ID from the context or a comment if possible, - // but since generateFlowSourceCode doesn't currently label them easily for return, - // we might need a small adjustment if we need types for return nodes specifically. - } - } - ts.forEachChild(n, visit); - }; - visit(sourceFile); - - return {nodes: nodeTypes, parameters: parameterTypes}; -} - diff --git a/test/getTypeVariant.test.ts b/test/getTypeVariant.test.ts deleted file mode 100644 index 8783c04..0000000 --- a/test/getTypeVariant.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getTypeVariant, DataTypeVariant } from '../src/extraction/getTypeVariant'; -import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data"; -import {getTypesFromNode} from "../src"; - -describe('getTypeVariant', () => { - it('identifies native TypeScript primitives (string, number, boolean) as PRIMITIVE variant', () => { - expect(getTypeVariant("string", DATA_TYPES)[0].variant).toBe(DataTypeVariant.PRIMITIVE); - expect(getTypeVariant("number", DATA_TYPES)[0].variant).toBe(DataTypeVariant.PRIMITIVE); - expect(getTypeVariant("boolean", DATA_TYPES)[0].variant).toBe(DataTypeVariant.PRIMITIVE); - }); - - it('recognizes both bracket notation and generic syntax for array types as ARRAY variant', () => { - expect(getTypeVariant("string[]", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY); - expect(getTypeVariant("Array", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY); - }); - - it('classifies object literals and interface-like structures as OBJECT variant', () => { - expect(getTypeVariant("{ name: string }", DATA_TYPES)[0].variant).toBe(DataTypeVariant.OBJECT); - expect(getTypeVariant("{}", DATA_TYPES)[0].variant).toBe(DataTypeVariant.OBJECT); - expect(getTypeVariant("OBJECT", DATA_TYPES)[0].variant).toBe(DataTypeVariant.OBJECT); - }); - - it('marks special types like void and any as TYPE variant', () => { - expect(getTypeVariant("void", DATA_TYPES)[0].variant).toBe(DataTypeVariant.TYPE); - expect(getTypeVariant("any", DATA_TYPES)[0].variant).toBe(DataTypeVariant.TYPE); - }); - - it('resolves generic LIST type aliases to ARRAY variant regardless of type parameter', () => { - // LIST is defined as T[] in data.ts, so all LIST variants should be arrays - expect(getTypeVariant("LIST", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY); - expect(getTypeVariant("LIST", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY); - expect(getTypeVariant("LIST", DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY); - }); - - it('identifies CONSUMER function types with generic parameters as NODE variant', () => { - // CONSUMER represents a callback function (T) => void - expect(getTypeVariant("CONSUMER", DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE); - }); - - it('identifies parameterless function types like RUNNABLE as NODE variant', () => { - // RUNNABLE represents () => void with no parameters - expect(getTypeVariant("RUNNABLE", DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE); - }); - - it('identifies PREDICATE function types with generic parameters as NODE variant', () => { - // PREDICATE represents a boolean-returning function (T) => boolean - expect(getTypeVariant("PREDICATE", DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE); - }); - - it('correctly identifies type variants when retrieved directly from DATA_TYPES registry', () => { - // Verify that types stored in DATA_TYPES are properly classified - expect(getTypeVariant(DATA_TYPES.find(dt => dt.identifier === "LIST"), DATA_TYPES)[0].variant).toBe(DataTypeVariant.ARRAY); - expect(getTypeVariant(DATA_TYPES.find(dt => dt.identifier === "HTTP_METHOD"), DATA_TYPES)[0].variant).toBe(DataTypeVariant.TYPE); - }); - - it('recognizes callback parameter types in real function signatures as NODE variant', () => { - // When extracting types from std::list::for_each, the second parameter (consumer) should be NODE - const types = getTypesFromNode({ - functionDefinition: { - identifier: "std::list::for_each" - } - }, FUNCTION_SIGNATURES, DATA_TYPES) - expect(getTypeVariant(types.parameters[1], DATA_TYPES)[0].variant).toBe(DataTypeVariant.NODE); - }); -}); - diff --git a/test/getTypesFromFunction.test.ts b/test/getTypesFromFunction.test.ts deleted file mode 100644 index 87c10b0..0000000 --- a/test/getTypesFromFunction.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {describe, expect, it} from "vitest"; -import {getTypesFromFunction} from "../src/extraction/getTypesFromFunction"; -import {FUNCTION_SIGNATURES} from "./data"; - -describe("getTypesFromFunction", () => { - - it("should correctly parse std::list::filter", () => { - const func = FUNCTION_SIGNATURES.find(f => f.identifier === "std::list::filter"); - expect(func).toBeDefined(); - if (func) { - const result = getTypesFromFunction(func); - expect(result.parameters).toEqual(["LIST", "PREDICATE"]); - expect(result.returnType).toBe("LIST"); - } - }); - - it("should correctly parse std::list::first", () => { - const func = FUNCTION_SIGNATURES.find(f => f.identifier === "std::list::first"); - expect(func).toBeDefined(); - if (func) { - const result = getTypesFromFunction(func); - expect(result.parameters).toEqual(["LIST"]); - expect(result.returnType).toBe("T"); - } - }); - - it("should correctly parse std::list::for_each", () => { - const func = FUNCTION_SIGNATURES.find(f => f.identifier === "std::list::for_each"); - expect(func).toBeDefined(); - if (func) { - const result = getTypesFromFunction(func); - expect(result.parameters).toEqual(["LIST", "CONSUMER"]); - expect(result.returnType).toBe("void"); - } - }); - - it("should correctly parse std::list::map", () => { - const func = FUNCTION_SIGNATURES.find(f => f.identifier === "std::list::map"); - expect(func).toBeDefined(); - if (func) { - const result = getTypesFromFunction(func); - expect(result.parameters).toEqual(["LIST", "TRANSFORM"]); - expect(result.returnType).toBe("LIST"); - } - }); - - it("should correctly parse std::list::at", () => { - const func = FUNCTION_SIGNATURES.find(f => f.identifier === "std::list::at"); - expect(func).toBeDefined(); - if (func) { - const result = getTypesFromFunction(func); - expect(result.parameters).toEqual(["LIST", "NUMBER"]); - expect(result.returnType).toBe("T"); - } - }); - - it("should correctly parse std::list::sort_reverse", () => { - const func = FUNCTION_SIGNATURES.find(f => f.identifier === "std::list::sort_reverse"); - expect(func).toBeDefined(); - if (func) { - const result = getTypesFromFunction(func); - expect(result.parameters).toEqual(["LIST", "COMPARATOR"]); - expect(result.returnType).toBe("LIST"); - } - }); -}); diff --git a/test/getTypesFromNode.test.ts b/test/getTypesFromNode.test.ts deleted file mode 100644 index 14192b0..0000000 --- a/test/getTypesFromNode.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import {describe, expect, it} from 'vitest'; -import {getTypesFromNode} from '../src/extraction/getTypesFromNode'; -import {FUNCTION_SIGNATURES, DATA_TYPES} from "./data"; -import {NodeFunction} from "@code0-tech/sagittarius-graphql-types"; - -describe('getTypesFromNode', () => { - it('should resolve types for std::list::at with string array', () => { - const node = { - functionDefinition: { - identifier: "std::list::at" as any - }, - parameters: { - nodes: [{ - value: { - __typename: "LiteralValue", - value: ["a", "b", "c"] - } - }, { - value: { - __typename: "LiteralValue", - value: 1 - } - }] - } - }; - const result = getTypesFromNode(node as any, FUNCTION_SIGNATURES, DATA_TYPES); - - expect(result.returnType).toBe("string"); - expect(result.parameters).toContain("LIST"); - expect(result.parameters).toContain("number"); - }); - - it('should resolve types for std::list::at with string array', () => { - const node = { - functionDefinition: { - identifier: "std::list::at" as any - }, - parameters: { - nodes: [{ - value: null - }, { - value: { - __typename: "LiteralValue", - value: 1 - } - }] - } - }; - const result = getTypesFromNode(node as any, FUNCTION_SIGNATURES, DATA_TYPES); - - expect(result.returnType).toBe("unknown"); - expect(result.parameters).toContain("LIST"); - expect(result.parameters).toContain("number"); - }); - - it('should resolve types for std::math::add', () => { - const node = { - functionDefinition: { - identifier: "std::number::add" as any - }, - parameters: { - nodes: [{ - value: { - __typename: "LiteralValue", - value: 5 - } - }, { - value: { - __typename: "LiteralValue", - value: 10 - } - }] - } - }; - const result = getTypesFromNode(node as any, FUNCTION_SIGNATURES, DATA_TYPES); - - expect(result.returnType).toBe("number"); - expect(result.parameters).toEqual(["number", "number"]); - }); - - it('should return any for unknown function', () => { - const node = { - functionDefinition: { - identifier: "unknown::func" as any - } - }; - const result = getTypesFromNode(node as any, FUNCTION_SIGNATURES, DATA_TYPES); - - expect(result.returnType).toBe("any"); - expect(result.parameters).toEqual([]); - }); - - it('should return std::object::get', () => { - const node: NodeFunction = { - functionDefinition: { - identifier: "std::object::get" - }, - parameters: { - nodes: [{ - value: { - __typename: "LiteralValue", - value: { id: 1, name: "Test" } - } - }, { - value: null - }] - } - }; - const result = getTypesFromNode(node, FUNCTION_SIGNATURES, DATA_TYPES); - - expect(result.returnType).toBe("string | number"); - expect(result.parameters[1]).toEqual('"id" | "name"'); - }); - - it('should return std::object::get with all values', () => { - const node: NodeFunction = { - functionDefinition: { - identifier: "std::object::get" - }, - parameters: { - nodes: [{ - value: { - __typename: "LiteralValue", - value: { id: 1, name: "Test" } - } - }, { - value: { - __typename: "LiteralValue", - value: "id" - } - }] - } - }; - const result = getTypesFromNode(node, FUNCTION_SIGNATURES, DATA_TYPES); - - expect(result.returnType).toBe("number"); - expect(result.parameters[1]).toEqual('"id" | "name"'); - }); -}); - From 860569a0d7ee4f1e82528f127a4ca9895e08e7f3 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Mon, 27 Apr 2026 18:12:28 +0200 Subject: [PATCH 11/12] feat: rename getNodeSchema to getSignatureSchema and update exports --- src/index.ts | 2 +- src/schema/{getNodeSchema.ts => getSignatureSchema.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/schema/{getNodeSchema.ts => getSignatureSchema.ts} (99%) diff --git a/src/index.ts b/src/index.ts index d7dee69..446c232 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,4 @@ export * from './extraction/getTypeFromValue'; export * from './extraction/getValueFromType'; export * from './validation/getFlowValidation'; export * from './validation/getValueValidation'; -export * from './schema/getNodeSchema'; +export * from './schema/getSignatureSchema'; diff --git a/src/schema/getNodeSchema.ts b/src/schema/getSignatureSchema.ts similarity index 99% rename from src/schema/getNodeSchema.ts rename to src/schema/getSignatureSchema.ts index cd626fc..31a89e9 100644 --- a/src/schema/getNodeSchema.ts +++ b/src/schema/getSignatureSchema.ts @@ -45,7 +45,7 @@ interface ParameterDependency { * console.log(`Parameter schema: ${schema}, blocked by: ${blockedBy?.join(',')}`); * }); */ -export const getNodeSchema = ( +export const getSignatureSchema = ( flow: Flow, dataTypes: DataType[], functions: FunctionDefinition[], From c62050bc5217753c83c7f0b00dd250d2dbeedfa3 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Tue, 28 Apr 2026 15:50:10 +0200 Subject: [PATCH 12/12] feat: enhance getSignatureSchema to handle flow signatures and improve ID sanitization --- src/schema/getSignatureSchema.ts | 10 +-- src/utils.ts | 2 +- test/schema/schema.test.ts | 119 +++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 test/schema/schema.test.ts diff --git a/src/schema/getSignatureSchema.ts b/src/schema/getSignatureSchema.ts index 31a89e9..6fda9d1 100644 --- a/src/schema/getSignatureSchema.ts +++ b/src/schema/getSignatureSchema.ts @@ -25,6 +25,8 @@ interface ParameterDependency { dependsOnIndex: number } +//TODO: Need to work also for flow signature +//TODO: needs a way to declare custom datatypes with there schema /** * Generates node schemas for all parameters of a specified function node. * @@ -63,8 +65,8 @@ export const getSignatureSchema = ( // Retrieve and identify the target node const targetNode = flow.nodes?.nodes?.find((n) => n?.id === nodeId) - const functionId = `fn_${targetNode?.functionDefinition?.identifier?.replace(/::/g, "_")}` - const realNodeId = `node_${sanitizeId(nodeId || "")}` + const functionId = nodeId ? `fn_${targetNode?.functionDefinition?.identifier?.replace(/::/g, "_")}` : `flow` + const realNodeId = nodeId ? `node_${sanitizeId(nodeId)}` : `flow_${sanitizeId(flow.id!)}` // Build map of declared functions for easy lookup const declaredFunctionsMap = createFunctionMap(sourceFile) @@ -98,8 +100,8 @@ export const getSignatureSchema = ( node!, combinedParameterTypes, funktionDependencies, - declaredFunctionsMap, - functions, + nodeId ? declaredFunctionsMap : new Map(), + nodeId ? functions : [], ) } diff --git a/src/utils.ts b/src/utils.ts index f031951..4dc207f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -96,7 +96,7 @@ export function getSharedTypeDeclarations(dataTypes?: DataType[], genericType: s /** * Sanitizes an ID for use as a TypeScript variable name. */ -export const sanitizeId = (id: string) => id.replace(/[^a-zA-Z0-9]/g, '_'); +export const sanitizeId = (id: string) => id?.replace(/[^a-zA-Z0-9]/g, '_'); /** * Generates TypeScript source code for a flow, suitable for validation and type inference. diff --git a/test/schema/schema.test.ts b/test/schema/schema.test.ts new file mode 100644 index 0000000..fc10904 --- /dev/null +++ b/test/schema/schema.test.ts @@ -0,0 +1,119 @@ +import {describe, expect, it} from "vitest"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import {getFlowValidation, getSignatureSchema} from "../../src"; +import {DATA_TYPES, FUNCTION_SIGNATURES} from "../data"; + +describe("Schema", () => { + + it('1', () => { + + const flow: Flow = { + id: "gid://sagittarius/Flow/1", + startingNodeId: "gid://sagittarius/NodeFunction/1", + nodes: { + nodes: [ + { + id: "gid://sagittarius/NodeFunction/1", + functionDefinition: {identifier: "std::number::add"}, + parameters: { + nodes: [ + {value: {__typename: "LiteralValue", value: 1}}, + {value: {__typename: "LiteralValue", value: 0}} + ] + }, + nextNodeId: "gid://sagittarius/NodeFunction/2" + }, + { + id: "gid://sagittarius/NodeFunction/2", + functionDefinition: {identifier: "std::list::for_each"}, + parameters: { + nodes: [ + { + value: { + __typename: "LiteralValue", + value: [{test: 1}] + } + }, + { + value: { + __typename: "NodeFunctionIdWrapper", + id: "gid://sagittarius/NodeFunction/3" + } + } + ] + } + }, + { + id: "gid://sagittarius/NodeFunction/3", + functionDefinition: {identifier: "std::number::add"}, + parameters: { + nodes: [ + { + value: { + __typename: "ReferenceValue", + nodeFunctionId: "gid://sagittarius/NodeFunction/2", + parameterIndex: 1, + inputIndex: 0, + referencePath: [{path: "test"}] + } + }, + { + value: {__typename: "LiteralValue", value: 10} + } + ] + } + } + ] + }, + signature: "(test: HTTP_METHOD): void" + }; + + const result = getSignatureSchema(flow, DATA_TYPES, FUNCTION_SIGNATURES); + + //console.dir(result, {depth: null}) + }); + + it('2', () => { + + const flow: Flow = { + nodes: { + nodes: [ + { + id: "gid://sagittarius/NodeFunction/1", + functionDefinition: {identifier: "std::list::at"}, + parameters: { + nodes: [ + {value: null}, + {value: {__typename: "LiteralValue", value: 0}} + ] + }, + nextNodeId: "gid://sagittarius/NodeFunction/2" + }, + { + id: "gid://sagittarius/NodeFunction/2", + functionDefinition: {identifier: "std::number::add"}, + parameters: { + nodes: [ + { + value: { + __typename: "ReferenceValue", + nodeFunctionId: "gid://sagittarius/NodeFunction/1" + } + }, + { + value: null + } + ] + } + } + ] + } + }; + + const result = getSignatureSchema(flow, DATA_TYPES, FUNCTION_SIGNATURES, "gid://sagittarius/NodeFunction/2"); + + //console.dir(result, {depth: null}) + + }); + +}) \ No newline at end of file