1+ import ts , { FunctionDeclaration } from "typescript" ;
2+ import { getValues } from "./values.util" ;
3+ import { getReferences } from "./references.util" ;
4+ import { getNodes } from "./nodes.util" ;
5+ import {
6+ FunctionDefinition ,
7+ LiteralValue ,
8+ NodeFunction ,
9+ ReferenceValue ,
10+ } from "@code0-tech/sagittarius-graphql-types" ;
11+
12+
13+ /**
14+ * Base interface for all input types.
15+ * Provides common properties for suggestions and input metadata.
16+ */
17+ interface Input {
18+ /** The type of input (string representation) */
19+ input ?: string ;
20+ /** Array of suggested values (functions, references, or literals) */
21+ suggestions ?: ( NodeFunction | ReferenceValue | LiteralValue ) [ ] ;
22+ }
23+
24+ /**
25+ * Represents a generic input type with no specific structure.
26+ * Used as a fallback when the type cannot be determined.
27+ */
28+ interface GenericInput {
29+ input ?: "generic" ;
30+ }
31+
32+ /**
33+ * Represents a sub-flow input type (callable/function type).
34+ * Used for types that have call signatures.
35+ */
36+ interface SubFlowInput {
37+ input ?: "sub-flow" ;
38+ }
39+
40+ /**
41+ * Represents primitive input types: boolean, number, text, or select.
42+ * Extends the base Input interface to include suggestions.
43+ */
44+ interface PrimitiveInput extends Input {
45+ input ?: "boolean" | "number" | "text" | "select" ;
46+ }
47+
48+ /**
49+ * Represents a data object input type with structured properties.
50+ * Includes property definitions and required field tracking.
51+ */
52+ interface DataInput extends Input {
53+ input ?: "data" ;
54+ /** Record mapping property names to their schemas */
55+ properties ?: Record < string , Schema | Schema [ ] > ;
56+ /** Array of required property names */
57+ required ?: string [ ] ;
58+ }
59+
60+ /**
61+ * Represents a list/array input type with item schemas.
62+ * Supports homogeneous or heterogeneous arrays.
63+ */
64+ interface ListInput extends Input {
65+ input ?: "list" ;
66+ /** Schema or array of schemas for list items */
67+ items ?: Schema | Schema [ ] ;
68+ }
69+
70+ /**
71+ * Represents a complex type input with properties and required fields.
72+ * Similar to DataInput but used for type definitions.
73+ */
74+ interface TypeInput extends Input {
75+ input ?: "type" ;
76+ /** Record mapping property names to their schemas */
77+ properties ?: Record < string , Schema | Schema [ ] > ;
78+ /** Array of required property names */
79+ required ?: string [ ] ;
80+ }
81+
82+ /**
83+ * Union type representing all possible schema input types.
84+ * Discriminated union based on the 'input' field.
85+ */
86+ export type Schema =
87+ | PrimitiveInput
88+ | DataInput
89+ | ListInput
90+ | TypeInput
91+ | SubFlowInput
92+ | GenericInput ;
93+
94+
95+ /**
96+ * Generates a schema definition for a given TypeScript type.
97+ *
98+ * This function analyzes a TypeScript type and produces a structured schema
99+ * that describes how the type should be presented and validated. It handles:
100+ * - Primitive types (boolean, number, string)
101+ * - Union types of primitives
102+ * - Array/Tuple types
103+ * - Complex object types with properties
104+ * - Sub-flow types (callables)
105+ *
106+ * For each type, the function also collects suggestions from:
107+ * - Literal values from the type definition
108+ * - Variable references available in scope
109+ * - Function node suggestions based on parameter type
110+ *
111+ * @param checker - TypeScript type checker for type analysis
112+ * @param node - The variable declaration node being analyzed
113+ * @param parameterType - The type to generate a schema for
114+ * @param functionDeclarations - Array of function declaration nodes
115+ * @param functions - Array of function definitions for matching
116+ * @returns A Schema object describing how to handle the parameter type
117+ */
118+ export const getSchema = (
119+ checker : ts . TypeChecker ,
120+ node : ts . VariableDeclaration ,
121+ parameterType : ts . Type ,
122+ functionDeclarations : FunctionDeclaration [ ] ,
123+ functions : FunctionDefinition [ ]
124+ ) : Schema => {
125+ // Collect all available suggestions for this parameter
126+ const literalValueSuggestions = getValues ( parameterType ) ;
127+ const referenceSuggestions = getReferences (
128+ checker ,
129+ node ! ,
130+ parameterType ,
131+ checker . getSymbolsInScope ( node ! , ts . SymbolFlags . Variable )
132+ ) ;
133+ const nodeSuggestions = getNodes (
134+ checker ,
135+ functionDeclarations ,
136+ functions ,
137+ parameterType
138+ ) ;
139+ const suggestions = {
140+ suggestions : [
141+ ...literalValueSuggestions ,
142+ ...referenceSuggestions ,
143+ ...nodeSuggestions ,
144+ ] ,
145+ } ;
146+
147+ // Check primitive literal union first (e.g., "a" | "b" | "c")
148+ if ( isPrimitiveLiteralUnion ( parameterType ) ) {
149+ return { input : "select" , ...suggestions } ;
150+ }
151+
152+ // Check individual primitive types
153+ if ( isBoolean ( parameterType ) ) {
154+ return { input : "boolean" , ...suggestions } ;
155+ }
156+ if ( isNumber ( parameterType ) ) {
157+ return { input : "number" , ...suggestions } ;
158+ }
159+ if ( isString ( parameterType ) ) {
160+ return { input : "text" , ...suggestions } ;
161+ }
162+
163+ // Check if type has call signatures (is callable/sub-flow)
164+ if ( isSubFlow ( parameterType ) ) {
165+ return { input : "sub-flow" , ...suggestions } ;
166+ }
167+
168+ // Handle array and tuple types
169+ if ( isArrayType ( checker , parameterType ) ) {
170+ const itemType = checker . getTypeArguments (
171+ parameterType as ts . TypeReference
172+ ) [ 0 ] ;
173+ const itemTypes = itemType . isUnion ( ) ? itemType . types : [ itemType ] ;
174+ const itemSchemas = itemTypes . map ( ( itemType ) =>
175+ getSchema ( checker , node , itemType , functionDeclarations , functions )
176+ ) ;
177+
178+ return {
179+ input : "list" ,
180+ items : itemSchemas . length === 1 ? itemSchemas [ 0 ] : itemSchemas ,
181+ ...suggestions ,
182+ } ;
183+ }
184+
185+ // Handle complex object types with properties
186+ if ( ( parameterType . flags & ts . TypeFlags . Object ) !== 0 ) {
187+ const properties : Record < string , Schema | Schema [ ] > = { } ;
188+ const required : string [ ] = [ ] ;
189+
190+ // Iterate through all properties of the object type
191+ for ( const property of checker . getPropertiesOfType ( parameterType ) ) {
192+ const declaration =
193+ property . valueDeclaration ?? property . declarations ?. [ 0 ] ;
194+
195+ if ( ! declaration ) continue ;
196+
197+ const propertyType = checker . getTypeOfSymbolAtLocation (
198+ property ,
199+ declaration
200+ ) ;
201+
202+ // Determine if the property is optional
203+ const isOptional =
204+ ( property . flags & ts . SymbolFlags . Optional ) !== 0 ||
205+ ( propertyType . isUnion ( ) &&
206+ propertyType . types . some (
207+ ( t ) => ( t . flags & ts . TypeFlags . Undefined ) !== 0
208+ ) ) ;
209+
210+ // Filter out undefined type from union types
211+ const propertyTypes = propertyType . isUnion ( )
212+ ? propertyType . types . filter (
213+ ( t ) => ( t . flags & ts . TypeFlags . Undefined ) === 0
214+ )
215+ : [ propertyType ] ;
216+
217+ // Recursively generate schemas for property types
218+ const propertySchemas = propertyTypes . map ( ( type ) =>
219+ getSchema ( checker , node , type , functionDeclarations , functions )
220+ ) ;
221+
222+ properties [ property . name ] =
223+ propertySchemas . length === 1 ? propertySchemas [ 0 ] : propertySchemas ;
224+
225+ // Track required properties
226+ if ( ! isOptional ) {
227+ required . push ( property . name ) ;
228+ }
229+ }
230+
231+ return {
232+ input : "data" ,
233+ properties,
234+ required,
235+ ...suggestions ,
236+ } ;
237+ }
238+
239+ // Fallback for unknown or generic types
240+ return {
241+ input : "generic" ,
242+ } ;
243+ } ;
244+
245+ /**
246+ * Checks if a type is a boolean type (either boolean or boolean literal).
247+ *
248+ * This function checks for both the general boolean type and specific boolean
249+ * literal types (true, false).
250+ *
251+ * @param type - The type to check
252+ * @returns True if the type is a boolean or boolean literal, false otherwise
253+ */
254+ function isBoolean ( type : ts . Type ) : boolean {
255+ return (
256+ ( type . flags & ts . TypeFlags . Boolean ) !== 0 ||
257+ ( type . flags & ts . TypeFlags . BooleanLiteral ) !== 0
258+ ) ;
259+ }
260+
261+ /**
262+ * Checks if a type is a number type (either number or number literal).
263+ *
264+ * This function checks for both the general number type and specific numeric
265+ * literal types (42, 3.14, etc.).
266+ *
267+ * @param type - The type to check
268+ * @returns True if the type is a number or number literal, false otherwise
269+ */
270+ function isNumber ( type : ts . Type ) : boolean {
271+ return (
272+ ( type . flags & ts . TypeFlags . Number ) !== 0 ||
273+ ( type . flags & ts . TypeFlags . NumberLiteral ) !== 0
274+ ) ;
275+ }
276+
277+ /**
278+ * Checks if a type is a string type (either string or string literal).
279+ *
280+ * This function checks for both the general string type and specific string
281+ * literal types ("hello", "world", etc.).
282+ *
283+ * @param type - The type to check
284+ * @returns True if the type is a string or string literal, false otherwise
285+ */
286+ function isString ( type : ts . Type ) : boolean {
287+ return (
288+ ( type . flags & ts . TypeFlags . String ) !== 0 ||
289+ ( type . flags & ts . TypeFlags . StringLiteral ) !== 0
290+ ) ;
291+ }
292+
293+ /**
294+ * Checks if a type is any primitive type (string, number, or boolean).
295+ *
296+ * @param type - The type to check
297+ * @returns True if the type is a string, number, or boolean, false otherwise
298+ */
299+ function isPrimitive ( type : ts . Type ) : boolean {
300+ return isString ( type ) || isNumber ( type ) || isBoolean ( type ) ;
301+ }
302+
303+ /**
304+ * Checks if a type is a union of primitive types only.
305+ *
306+ * This function is used to identify union types that can be represented as
307+ * a select input with predefined options (e.g., "a" | "b" | "c" or 1 | 2 | 3).
308+ *
309+ * @param type - The type to check
310+ * @returns True if the type is a union where all members are primitives, false otherwise
311+ */
312+ function isPrimitiveLiteralUnion ( type : ts . Type ) : boolean {
313+ if ( ! type . isUnion ( ) ) return false ;
314+ return type . types . every ( isPrimitive ) ;
315+ }
316+
317+ /**
318+ * Checks if a type is an array or tuple type.
319+ *
320+ * @param checker - TypeScript type checker for type analysis
321+ * @param type - The type to check
322+ * @returns True if the type is an array or tuple, false otherwise
323+ */
324+ function isArrayType ( checker : ts . TypeChecker , type : ts . Type ) : boolean {
325+ return checker . isArrayType ( type ) || checker . isTupleType ( type ) ;
326+ }
327+
328+ /**
329+ * Checks if a type is a callable type (has call signatures).
330+ *
331+ * A type with call signatures can be invoked like a function. This is used
332+ * to identify sub-flow types that represent workflow steps.
333+ *
334+ * @param type - The type to check
335+ * @returns True if the type has call signatures, false otherwise
336+ */
337+ export function isSubFlow ( type : ts . Type ) : boolean {
338+ return type . getCallSignatures ( ) . length > 0 ;
339+ }
0 commit comments