Skip to content

Commit cad50a6

Browse files
committed
feat: implement getNodeSchema function for generating node schemas with parameter dependencies
1 parent d69cf6c commit cad50a6

7 files changed

Lines changed: 323 additions & 1191 deletions

File tree

src/schema/getNodeSchema.ts

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import { DataType, Flow, FunctionDefinition, NodeFunction } from "@code0-tech/sagittarius-graphql-types"
2+
import { createCompilerHost, generateFlowSourceCode, sanitizeId } from "../utils"
3+
import ts, { Type } from "typescript"
4+
import { getSchema, Schema } from "../util/schema.util"
5+
6+
/**
7+
* Represents the schema information for a node parameter.
8+
* Includes the parameter's schema definition and any parameter dependencies that block it.
9+
*/
10+
interface NodeSchema {
11+
/** The schema definition for this node parameter */
12+
schema: Schema
13+
/** Array of parameter indices that must be resolved before this parameter */
14+
blockedBy?: number[]
15+
}
16+
17+
/**
18+
* Represents a parameter dependency relationship.
19+
* Indicates which parameters depend on type parameters defined in other parameters.
20+
*/
21+
interface ParameterDependency {
22+
/** The index of the parameter that has the dependency */
23+
parameterIndex: number
24+
/** The index of the parameter it depends on */
25+
dependsOnIndex: number
26+
}
27+
28+
/**
29+
* Generates node schemas for all parameters of a specified function node.
30+
*
31+
* This function analyzes a TypeScript flow's AST to extract type information for node parameters.
32+
* It resolves parameter types by combining information from both the node's call expression and
33+
* the function definition, accounting for type parameters and generic constraints.
34+
*
35+
* @param flow - The data flow object containing nodes and their relationships
36+
* @param dataTypes - Array of available data type definitions
37+
* @param functions - Array of available function definitions
38+
* @param nodeId - Optional specific node ID to analyze; if provided, only that node's schema is processed
39+
*
40+
* @returns Array of NodeSchema objects, each containing a schema and optional blocked dependencies
41+
*
42+
* @example
43+
* const schemas = getNodeSchema(flow, dataTypes, functions, nodeId);
44+
* schemas.forEach(({ schema, blockedBy }) => {
45+
* console.log(`Parameter schema: ${schema}, blocked by: ${blockedBy?.join(',')}`);
46+
* });
47+
*/
48+
export const getNodeSchema = (
49+
flow: Flow,
50+
dataTypes: DataType[],
51+
functions: FunctionDefinition[],
52+
nodeId?: NodeFunction["id"],
53+
): NodeSchema[] => {
54+
// Generate TypeScript source code from the flow definition
55+
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes)
56+
57+
// Set up the TypeScript compiler environment
58+
const fileName = "index.ts"
59+
const host = createCompilerHost(fileName, sourceCode)
60+
const sourceFile = host.getSourceFile(fileName)!
61+
const program = host.languageService.getProgram()!
62+
const checker = program.getTypeChecker()
63+
64+
// Retrieve and identify the target node
65+
const targetNode = flow.nodes?.nodes?.find((n) => n?.id === nodeId)
66+
const functionId = `fn_${targetNode?.functionDefinition?.identifier?.replace(/::/g, "_")}`
67+
const realNodeId = `node_${sanitizeId(nodeId || "")}`
68+
69+
// Build map of declared functions for easy lookup
70+
const declaredFunctionsMap = createFunctionMap(sourceFile)
71+
72+
// Build map of constant variable declarations for easy lookup
73+
const constantNames = createConstantMap(sourceFile)
74+
75+
// Retrieve the node's variable declaration and its corresponding function
76+
const node = constantNames.get(realNodeId)
77+
const funktion = declaredFunctionsMap.get(functionId)
78+
79+
// Extract parameter types from the node's call expression
80+
const nodeParameterTypes = extractNodeParameterTypes(checker, node)
81+
82+
// Extract parameter types from the function definition
83+
const funktionParameterTypes = extractFunctionParameterTypes(checker, funktion, node)
84+
85+
// Combine node and function parameter types, preferring node types when assignable
86+
const combinedParameterTypes = mergeParameterTypes(
87+
checker,
88+
funktionParameterTypes,
89+
nodeParameterTypes,
90+
)
91+
92+
// Identify parameter dependencies based on type parameters
93+
const funktionDependencies = getParameterDependencies(funktion!)
94+
95+
// Generate schema for each parameter
96+
return generateNodeSchemas(
97+
checker,
98+
node!,
99+
combinedParameterTypes,
100+
funktionDependencies,
101+
declaredFunctionsMap,
102+
functions,
103+
)
104+
}
105+
106+
/**
107+
* Creates a map of all function declarations in the source file.
108+
*
109+
* @param sourceFile - The TypeScript source file to analyze
110+
* @returns Map with function names as keys and FunctionDeclaration nodes as values
111+
*/
112+
const createFunctionMap = (
113+
sourceFile: ts.SourceFile,
114+
): Map<string, ts.FunctionDeclaration> => {
115+
return new Map(
116+
sourceFile.statements
117+
.filter(ts.isFunctionDeclaration)
118+
.map((node) => [node.name!.getText(), node]),
119+
)
120+
}
121+
122+
/**
123+
* Creates a map of all constant variable declarations in the source file.
124+
* Recursively traverses the AST to find all const declarations.
125+
*
126+
* @param sourceFile - The TypeScript source file to analyze
127+
* @returns Map with variable names as keys and VariableDeclaration nodes as values
128+
*/
129+
const createConstantMap = (
130+
sourceFile: ts.SourceFile,
131+
): Map<string, ts.VariableDeclaration> => {
132+
const results: [string, ts.VariableDeclaration][] = []
133+
134+
sourceFile.statements.forEach((node) => {
135+
node.forEachChild(function visitor(child) {
136+
if (ts.isVariableDeclaration(child)) {
137+
// Check if this is a const declaration
138+
if ((child.parent.flags & ts.NodeFlags.Const) !== 0) {
139+
results.push([child.name.getText(), child])
140+
}
141+
}
142+
child.forEachChild(visitor)
143+
})
144+
})
145+
146+
return new Map(results)
147+
}
148+
149+
/**
150+
* Extracts parameter types from a node's call expression.
151+
* These types represent the actual types passed to the function at the node.
152+
*
153+
* @param checker - The TypeScript type checker
154+
* @param node - The variable declaration containing the call expression
155+
* @returns Array of resolved parameter types, or undefined if not available
156+
*/
157+
const extractNodeParameterTypes = (
158+
checker: ts.TypeChecker,
159+
node: ts.VariableDeclaration | undefined,
160+
): Type[] | undefined => {
161+
if (!node?.initializer || !ts.isCallExpression(node.initializer)) {
162+
return undefined
163+
}
164+
165+
const signature = checker.getResolvedSignature(node.initializer)
166+
return signature?.parameters.map((p) =>
167+
checker.getTypeOfSymbolAtLocation(p, node.initializer as ts.CallExpression),
168+
)
169+
}
170+
171+
/**
172+
* Extracts parameter types from the function definition.
173+
* These are the declared parameter types from the function signature.
174+
*
175+
* @param checker - The TypeScript type checker
176+
* @param funktion - The function declaration to analyze
177+
* @param node - The node's variable declaration (used as location context)
178+
* @returns Array of parameter types, or undefined if function not found
179+
*/
180+
const extractFunctionParameterTypes = (
181+
checker: ts.TypeChecker,
182+
funktion: ts.FunctionDeclaration | undefined,
183+
node: ts.VariableDeclaration | undefined,
184+
): Type[] | undefined => {
185+
if (!funktion || !node?.initializer) {
186+
return undefined
187+
}
188+
189+
return funktion.parameters.map((p) => {
190+
const symbol = checker.getSymbolAtLocation(p.name)
191+
return checker.getTypeOfSymbolAtLocation(
192+
symbol!,
193+
node.initializer as ts.CallExpression,
194+
)
195+
})
196+
}
197+
198+
/**
199+
* Merges function and node parameter types by applying type assignability rules.
200+
* Prefers node types when they are assignable to the function's parameter type.
201+
* Handles generic type parameters with constraints.
202+
*
203+
* @param checker - The TypeScript type checker
204+
* @param funktionParameterTypes - Parameter types from function definition
205+
* @param nodeParameterTypes - Parameter types from node's call expression
206+
* @returns Array of resolved parameter types
207+
*/
208+
const mergeParameterTypes = (
209+
checker: ts.TypeChecker,
210+
funktionParameterTypes: Type[] | undefined,
211+
nodeParameterTypes: Type[] | undefined,
212+
): Type[] | undefined => {
213+
if (!funktionParameterTypes) {
214+
return undefined
215+
}
216+
217+
return funktionParameterTypes.map((paramType, index) => {
218+
const nodeType = nodeParameterTypes?.[index]
219+
if (!nodeType) {
220+
return paramType
221+
}
222+
223+
// If both types refer to the same symbol, use the node type
224+
const paramSymbol = paramType.getSymbol()
225+
const nodeSymbol = nodeType.getSymbol()
226+
if (paramSymbol && nodeSymbol && paramSymbol === nodeSymbol) {
227+
return nodeType
228+
}
229+
230+
// Handle generic type parameters
231+
if (paramType.isTypeParameter()) {
232+
const constraint = checker.getBaseConstraintOfType(paramType)
233+
// Use node type if it satisfies the constraint
234+
if (!constraint || checker.isTypeAssignableTo(nodeType, constraint)) {
235+
return nodeType
236+
}
237+
}
238+
239+
// Use node type if assignable to parameter type
240+
if (checker.isTypeAssignableTo(nodeType, paramType)) {
241+
return nodeType
242+
}
243+
244+
// Default to function parameter type
245+
return paramType
246+
})
247+
}
248+
249+
/**
250+
* Identifies parameter dependencies based on shared type parameters.
251+
* Determines which parameters depend on type parameters declared in other parameters.
252+
*
253+
* @param node - The function declaration to analyze
254+
* @returns Array of ParameterDependency objects
255+
*/
256+
const getParameterDependencies = (node: ts.FunctionDeclaration): ParameterDependency[] => {
257+
// Extract all type parameter names from the function
258+
const typeParamNames = node.typeParameters?.map((tp) => tp.name.getText()) || []
259+
const usage: Record<string, number[]> = {}
260+
261+
// Track which parameters use each type parameter
262+
node.parameters.forEach((p, i) => {
263+
const typeText = p.type?.getText() || ""
264+
typeParamNames.forEach((typeParam) => {
265+
if (typeText.includes(typeParam)) {
266+
if (!usage[typeParam]) {
267+
usage[typeParam] = []
268+
}
269+
usage[typeParam].push(i)
270+
}
271+
})
272+
})
273+
274+
// Extract dependencies: type params used by multiple parameters
275+
return Object.values(usage)
276+
.filter((indices) => indices.length > 1)
277+
.map(([firstIndex, ...otherIndices]) =>
278+
otherIndices.map((depIndex) => ({
279+
parameterIndex: depIndex,
280+
dependsOnIndex: firstIndex,
281+
})),
282+
)
283+
.flat()
284+
}
285+
286+
/**
287+
* Generates node schemas for all parameters.
288+
* Creates schema objects for each parameter with their dependencies.
289+
*
290+
* @param checker - The TypeScript type checker
291+
* @param node - The node's variable declaration
292+
* @param combinedParameterTypes - Merged parameter types to use for schema generation
293+
* @param funktionDependencies - Parameter dependencies to link with each parameter
294+
* @param declaredFunctionsMap - Map of available functions for schema context
295+
* @param functions - Array of function definitions
296+
* @returns Array of NodeSchema objects
297+
*/
298+
const generateNodeSchemas = (
299+
checker: ts.TypeChecker,
300+
node: ts.VariableDeclaration,
301+
combinedParameterTypes: Type[] | undefined,
302+
funktionDependencies: ParameterDependency[],
303+
declaredFunctionsMap: Map<string, ts.FunctionDeclaration>,
304+
functions: FunctionDefinition[],
305+
): NodeSchema[] => {
306+
if (!combinedParameterTypes) {
307+
return []
308+
}
309+
310+
return combinedParameterTypes.map((parameterType, index) => ({
311+
schema: getSchema(
312+
checker,
313+
node,
314+
parameterType,
315+
Array.from(declaredFunctionsMap.values()),
316+
functions,
317+
),
318+
blockedBy: funktionDependencies
319+
.filter((dep) => dep.parameterIndex === index)
320+
.map((dep) => dep.dependsOnIndex),
321+
}))
322+
}
323+

0 commit comments

Comments
 (0)