Skip to content

Commit 3357329

Browse files
author
Nico Sammito
authored
Merge pull request #4 from code0-tech/feat/validation-suggestion
Implementation of fundamental utils to work with ts types
2 parents 1254045 + 13cbdb1 commit 3357329

27 files changed

Lines changed: 5408 additions & 2 deletions

package-lock.json

Lines changed: 2501 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,31 @@
1313
"license": "ISC",
1414
"author": "CodeZero",
1515
"type": "module",
16-
"main": "index.ts",
16+
"main": "./dist/triangulum.cjs.js",
17+
"module": "./dist/triangulum.es.js",
18+
"types": "./dist/index.d.ts",
19+
"exports": {
20+
".": {
21+
"types": "./dist/index.d.ts",
22+
"import": "./dist/triangulum.es.js",
23+
"require": "./dist/triangulum.cjs.js"
24+
}
25+
},
26+
"files": [
27+
"dist"
28+
],
1729
"scripts": {
18-
"test": "echo \"Error: no test specified\" && exit 1"
30+
"test": "vitest run",
31+
"build": "vite build"
1932
},
2033
"dependencies": {
2134
"typescript": "^5.9.3"
35+
},
36+
"devDependencies": {
37+
"@code0-tech/sagittarius-graphql-types": "0.0.0-experimental-2342308809-931efb40b4bf3245999c53abbdd9164cea82e82d",
38+
"@types/node": "^25.3.2",
39+
"vite": "^7.3.1",
40+
"vite-plugin-dts": "^4.5.4",
41+
"vitest": "^4.0.18"
2242
}
2343
}

src/extraction/getTypeFromValue.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import ts from "typescript";
2+
import {createCompilerHost} from "../utils";
3+
import {LiteralValue} from "@code0-tech/sagittarius-graphql-types";
4+
5+
/**
6+
* Uses the TypeScript compiler to generate a precise type string from any runtime value.
7+
*/
8+
export const getTypeFromValue = (value: LiteralValue): string => {
9+
// 1. Serialize value to a JSON string for embedding in source code.
10+
const literal = JSON.stringify(value.value);
11+
12+
// 2. Wrap value in virtual source code.
13+
const sourceCode = `const tempValue = ${literal};`;
14+
const fileName = "temp_value.ts";
15+
const sourceFile = ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.Latest);
16+
17+
// 3. Setup a minimal compiler host.
18+
const host = createCompilerHost(fileName, sourceCode, sourceFile);
19+
20+
const program = ts.createProgram([fileName], {target: ts.ScriptTarget.Latest, noEmit: true}, host);
21+
const checker = program.getTypeChecker();
22+
23+
let inferredType = "any";
24+
25+
// 4. Extract type using the TypeChecker.
26+
const visit = (node: ts.Node) => {
27+
if (ts.isVariableDeclaration(node) && node.name.getText() === "tempValue") {
28+
const type = checker.getTypeAtLocation(node);
29+
inferredType = checker.typeToString(
30+
type,
31+
node,
32+
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseFullyQualifiedType
33+
);
34+
}
35+
ts.forEachChild(node, visit);
36+
};
37+
38+
visit(sourceFile);
39+
40+
return inferredType;
41+
};

src/extraction/getTypeVariant.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import ts from "typescript";
2+
import {ExtendedDataType, getSharedTypeDeclarations, createCompilerHost, DEFAULT_COMPILER_OPTIONS} from "../utils";
3+
4+
export enum DataTypeVariant {
5+
PRIMITIVE,
6+
TYPE,
7+
ARRAY,
8+
OBJECT,
9+
}
10+
11+
/**
12+
* Determines the variant of a given TypeScript type string using the TS compiler.
13+
*/
14+
export const getTypeVariant = (
15+
type: string,
16+
dataTypes: ExtendedDataType[]
17+
): DataTypeVariant => {
18+
const typeDefs = getSharedTypeDeclarations(dataTypes);
19+
const fileName = `type_probe_${Math.random().toString(36).substring(7)}.ts`;
20+
21+
// We declare a variable with the type to probe it
22+
const sourceCode = `
23+
${typeDefs}
24+
type TargetType = ${type};
25+
const val: TargetType = {} as any;
26+
`;
27+
28+
const sourceFile = ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.Latest);
29+
const host = createCompilerHost(fileName, sourceCode, sourceFile);
30+
const program = ts.createProgram([fileName], DEFAULT_COMPILER_OPTIONS, host);
31+
const checker = program.getTypeChecker();
32+
33+
let discoveredVariant: DataTypeVariant = DataTypeVariant.TYPE;
34+
35+
const visit = (node: ts.Node) => {
36+
if (ts.isVariableDeclaration(node) && node.name.getText() === "val") {
37+
const type = checker.getTypeAtLocation(node);
38+
39+
if (checker.isArrayType(type)) {
40+
discoveredVariant = DataTypeVariant.ARRAY;
41+
} else if (
42+
type.isStringLiteral() ||
43+
type.isNumberLiteral() ||
44+
(type.getFlags() & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.EnumLiteral | ts.TypeFlags.BigInt | ts.TypeFlags.ESSymbol)) !== 0
45+
) {
46+
discoveredVariant = DataTypeVariant.PRIMITIVE;
47+
} else if (type.isClassOrInterface() || (type.getFlags() & ts.TypeFlags.Object) !== 0) {
48+
// Check if it's literally just a type alias to something else or a complex object
49+
if (type.getProperties().length > 0) {
50+
discoveredVariant = DataTypeVariant.OBJECT;
51+
} else {
52+
discoveredVariant = DataTypeVariant.TYPE;
53+
}
54+
} else {
55+
discoveredVariant = DataTypeVariant.TYPE;
56+
}
57+
}
58+
ts.forEachChild(node, visit);
59+
};
60+
61+
visit(sourceFile);
62+
return discoveredVariant;
63+
};

src/extraction/getTypesFromNode.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import ts from "typescript";
2+
import {Flow, NodeFunction, NodeParameter} from "@code0-tech/sagittarius-graphql-types";
3+
import {
4+
createCompilerHost,
5+
DEFAULT_COMPILER_OPTIONS,
6+
ExtendedDataType,
7+
ExtendedFunction,
8+
getParameterCode,
9+
getSharedTypeDeclarations,
10+
} from "../utils";
11+
import {getNodeValidation} from "../validation/getNodeValidation"; // Wieder hinzugefügt
12+
13+
export interface NodeTypes {
14+
parameters: string[];
15+
returnType: string;
16+
}
17+
18+
/**
19+
* Resolves the types of the parameters and the return type of a NodeFunction.
20+
*/
21+
export const getTypesFromNode = (
22+
node: NodeFunction,
23+
functions: ExtendedFunction[],
24+
dataTypes: ExtendedDataType[]
25+
): NodeTypes => {
26+
const funcMap = new Map(functions.map(f => [f.identifier, f]));
27+
const funcDef = funcMap.get(node.functionDefinition?.identifier);
28+
29+
if (!funcDef) {
30+
return {
31+
parameters: [],
32+
returnType: "any",
33+
};
34+
}
35+
36+
const mockFlow: Flow = {
37+
id: "gid://sagittarius/Flow/0" as any,
38+
nodes: { __typename: "NodeFunctionConnection", nodes: [node] }
39+
} as Flow;
40+
41+
const params = (node.parameters?.nodes as NodeParameter[]) || [];
42+
const paramCodes = params.map(param => getParameterCode(param, mockFlow, (f, n) => getNodeValidation(f, n, functions, dataTypes)));
43+
44+
const funcCallArgs = paramCodes.map(code => code === 'undefined' ? '({} as any)' : code).join(", ");
45+
46+
const signature = funcDef.signature;
47+
const sourceCode = `
48+
${getSharedTypeDeclarations(dataTypes)}
49+
declare function testFunc${signature};
50+
const result = testFunc(${funcCallArgs});
51+
`;
52+
53+
const fileName = "node_types_virtual.ts";
54+
const sourceFile = ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.Latest);
55+
const host = createCompilerHost(fileName, sourceCode, sourceFile);
56+
57+
const program = ts.createProgram([fileName], DEFAULT_COMPILER_OPTIONS, host);
58+
const checker = program.getTypeChecker();
59+
60+
let inferredReturnType = "any";
61+
let inferredParameterTypes: string[] = [];
62+
63+
const visitor = (n: ts.Node) => {
64+
if (ts.isVariableDeclaration(n) && n.name.getText() === "result") {
65+
const resultType = checker.getTypeAtLocation(n);
66+
inferredReturnType = checker.typeToString(
67+
resultType,
68+
n,
69+
ts.TypeFormatFlags.NoTruncation
70+
);
71+
72+
if (ts.isCallExpression(n.initializer!)) {
73+
const callExpr = n.initializer;
74+
const signature = checker.getResolvedSignature(callExpr);
75+
if (signature) {
76+
inferredParameterTypes = signature.getParameters().map(p => {
77+
const type = checker.getTypeOfSymbolAtLocation(p, callExpr);
78+
return checker.typeToString(
79+
type,
80+
callExpr,
81+
ts.TypeFormatFlags.NoTruncation
82+
);
83+
});
84+
}
85+
}
86+
}
87+
ts.forEachChild(n, visitor);
88+
};
89+
90+
visitor(sourceFile);
91+
92+
return {
93+
parameters: inferredParameterTypes,
94+
returnType: inferredReturnType,
95+
};
96+
};

src/extraction/getValueFromType.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import ts from "typescript";
2+
import {LiteralValue} from "@code0-tech/sagittarius-graphql-types";
3+
import {createCompilerHost, DEFAULT_COMPILER_OPTIONS, ExtendedDataType, getSharedTypeDeclarations} from "../utils";
4+
5+
/**
6+
* Generates a sample LiteralValue from a TypeScript type string.
7+
*/
8+
export const getValueFromType = (
9+
type: string,
10+
dataTypes: ExtendedDataType[]
11+
): LiteralValue => {
12+
// 1. Prepare declarations.
13+
const sourceCode = `
14+
${getSharedTypeDeclarations(dataTypes)}
15+
type Target = ${type};
16+
`;
17+
18+
const fileName = "temp_type_to_value.ts";
19+
const sourceFile = ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.Latest, true);
20+
21+
// 2. Setup the compiler context.
22+
const host = createCompilerHost(fileName, sourceCode, sourceFile);
23+
24+
const program = ts.createProgram([fileName], DEFAULT_COMPILER_OPTIONS, host);
25+
26+
const checker = program.getTypeChecker();
27+
28+
// 3. Find the Target type alias.
29+
const targetNode = sourceFile.statements.find(
30+
(s): s is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(s) && s.name.text === "Target"
31+
);
32+
33+
if (!targetNode) {
34+
return {__typename: 'LiteralValue', value: null};
35+
}
36+
37+
const typeFound = checker.getTypeAtLocation(targetNode.type);
38+
39+
/**
40+
* Recursively generates a sample JavaScript value for a given TypeScript Type.
41+
*/
42+
const generateSample = (t: ts.Type, node: ts.Node, visited = new Set<ts.Type>()): any => {
43+
if (visited.has(t)) return null;
44+
visited.add(t);
45+
46+
const flags = t.getFlags();
47+
48+
// 1. Handle Union Types (e.g., "A" | "B" or string | number)
49+
if (t.isUnion()) {
50+
// Pick types based on precedence to ensure deterministic results.
51+
// If the user provided multiple types in the union, pick the first non-null/undefined one.
52+
const typeNode = (node as any).type;
53+
if (ts.isTypeAliasDeclaration(node) && node.type && ts.isUnionTypeNode(node.type)) {
54+
// Try to follow the order in the source code if we are at the top level
55+
const firstType = checker.getTypeFromTypeNode(node.type.types[0]);
56+
return generateSample(firstType, node, visited);
57+
}
58+
59+
const filteredTypes = t.types.filter(subType => {
60+
const f = subType.getFlags();
61+
return !(f & ts.TypeFlags.Undefined) && !(f & ts.TypeFlags.Null);
62+
});
63+
const typeToUse = filteredTypes.length > 0 ? filteredTypes[0] : t.types[0];
64+
return generateSample(typeToUse, node, visited);
65+
}
66+
67+
// 2. Handle Primitives and Literals
68+
if (flags & ts.TypeFlags.StringLiteral) return (t as ts.StringLiteralType).value;
69+
if (flags & ts.TypeFlags.String) return "sample";
70+
71+
if (flags & ts.TypeFlags.NumberLiteral) return (t as ts.NumberLiteralType).value;
72+
if (flags & ts.TypeFlags.Number) return 1;
73+
74+
if (flags & ts.TypeFlags.BooleanLiteral) return (t as any).intrinsicName === "true";
75+
if (flags & ts.TypeFlags.Boolean) return false;
76+
77+
// 3. Handle Arrays
78+
if (checker.isArrayType(t)) {
79+
const typeRef = t as ts.TypeReference;
80+
const elementType = typeRef.typeArguments?.[0] || checker.getAnyType();
81+
return [generateSample(elementType, node, visited)];
82+
}
83+
84+
// 4. Handle Objects / Interfaces
85+
if (t.isClassOrInterface() || (flags & ts.TypeFlags.Object) || t.getProperties().length > 0) {
86+
const obj: any = {};
87+
const props = t.getProperties();
88+
89+
props.forEach(prop => {
90+
const propType = checker.getTypeOfSymbolAtLocation(prop, node);
91+
if (propType) {
92+
obj[prop.getName()] = generateSample(propType, node, visited);
93+
}
94+
});
95+
return obj;
96+
}
97+
98+
return null;
99+
};
100+
101+
const sample = generateSample(typeFound, targetNode);
102+
103+
// Test Expectation: result.value should be null for unknown types.
104+
// However, if we return null from the function, result.value access fails.
105+
// So we return an object { value: null } if sample is null.
106+
return {
107+
value: sample
108+
};
109+
};

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export * from './validation/getFlowValidation';
2+
export * from './suggestion/getNodeSuggestions';
3+
export * from './validation/getNodeValidation';
4+
export * from './suggestion/getReferenceSuggestions';
5+
export * from './extraction/getTypeFromValue';
6+
export * from './extraction/getTypeVariant';
7+
export * from './extraction/getValueFromType';
8+
export * from './suggestion/getValueSuggestions';
9+
export * from './validation/getValueValidation';
10+
export * from './extraction/getTypesFromNode';

0 commit comments

Comments
 (0)