1+ import {
2+ DataType ,
3+ Flow ,
4+ FunctionDefinition ,
5+ LiteralValue ,
6+ NodeFunction ,
7+ ReferenceValue
8+ } from "@code0-tech/sagittarius-graphql-types"
9+ import { createCompilerHost , generateFlowSourceCode , sanitizeId } from "../utils"
10+ import ts , { NumberLiteralType , StringLiteralType , Type } from "typescript"
11+
12+ export interface TemporaryLiteralValue extends LiteralValue {
13+ references ?: Record < string , ReferenceValue >
14+ }
15+
16+ interface Input {
17+ input ?: string
18+ suggestions ?: ( NodeFunction | ReferenceValue | LiteralValue ) [ ]
19+ }
20+
21+ interface GenericInput {
22+ input ?: "generic"
23+ }
24+
25+ interface SubFlowInput {
26+ input ?: "sub-flow"
27+ }
28+
29+ interface PrimitiveInput extends Input {
30+ input ?: "boolean" | "number" | "text" | "select"
31+ }
32+
33+ interface DataInput extends Input {
34+ input ?: "data"
35+ properties ?: Record < string , Schema | Schema [ ] >
36+ required ?: string [ ]
37+ }
38+
39+ interface ListInput extends Input {
40+ input ?: "list"
41+ items ?: Schema | Schema [ ]
42+ }
43+
44+ interface TypeInput extends Input {
45+ input ?: "type"
46+ properties ?: Record < string , Schema | Schema [ ] >
47+ required ?: string [ ]
48+ }
49+
50+ type Schema = PrimitiveInput | DataInput | ListInput | TypeInput | SubFlowInput | GenericInput
51+
52+ interface ParameterSchema {
53+ schema : Schema
54+ blockedBy ?: number [ ]
55+ }
56+
57+ export const getSchema = (
58+ flow : Flow ,
59+ dataTypes : DataType [ ] ,
60+ functions : FunctionDefinition [ ] ,
61+ nodeId ?: NodeFunction [ 'id' ]
62+ ) : ParameterSchema [ ] => {
63+
64+ const sourceCode = generateFlowSourceCode ( flow , functions , dataTypes )
65+
66+ const fileName = "index.ts"
67+ const host = createCompilerHost ( fileName , sourceCode )
68+ const sourceFile = host . getSourceFile ( fileName ) !
69+ const program = host . languageService . getProgram ( ) !
70+ const checker = program . getTypeChecker ( )
71+
72+ const node2 = flow . nodes ?. nodes ?. find ( n => n ?. id === nodeId )
73+ const functionId = `fn_${ node2 ?. functionDefinition ?. identifier ?. replace ( / : : / g, '_' ) } `
74+ const realNodeId = `node_${ sanitizeId ( nodeId || "" ) } `
75+
76+ const declaredFunctionsMap = new Map < string , ts . FunctionDeclaration > (
77+ sourceFile . statements
78+ . filter ( ts . isFunctionDeclaration )
79+ . map ( node => [ node . name ! . getText ( ) , node ] )
80+ )
81+
82+ const constantNames = new Map < string , ts . VariableDeclaration > (
83+ sourceFile . statements
84+ . flatMap ( node => {
85+ const results : ts . VariableDeclaration [ ] = [ ]
86+
87+ node . forEachChild ( function visitor ( child ) {
88+ if ( ts . isVariableDeclaration ( child ) ) {
89+ if ( ( child . parent . flags & ts . NodeFlags . Const ) !== 0 ) {
90+ results . push ( child )
91+ }
92+ }
93+ child . forEachChild ( visitor )
94+ } )
95+
96+ return results
97+ } )
98+ . map ( decl => [ decl . name . getText ( ) , decl ] as [ string , ts . VariableDeclaration ] )
99+ )
100+
101+ const node = constantNames . get ( realNodeId )
102+ const funktion = declaredFunctionsMap . get ( functionId )
103+
104+ const nodeParameterTypes : Type [ ] | undefined = checker . getResolvedSignature ( node ?. initializer as ts . CallExpression ) ?. parameters . map ( p => {
105+ return checker . getTypeOfSymbolAtLocation ( p , node ?. initializer as ts . CallExpression )
106+ } )
107+
108+ const funktionParameterTypes : Type [ ] | undefined = funktion ?. parameters ?. map ( p => {
109+ const symbol = checker . getSymbolAtLocation ( p . name )
110+ return checker . getTypeOfSymbolAtLocation ( symbol ! , funktion )
111+ } )
112+
113+ const combinedParameterTypes : Type [ ] | undefined = funktionParameterTypes ?. map ( ( p , i ) => {
114+ const nodeType = nodeParameterTypes ?. [ i ] ;
115+ if ( ! nodeType ) return p ;
116+
117+ const pSymbol = p . getSymbol ( ) ;
118+ const nodeSymbol = nodeType . getSymbol ( ) ;
119+
120+ if ( pSymbol && nodeSymbol && pSymbol === nodeSymbol ) {
121+ return nodeType ;
122+ }
123+
124+ if ( p . isTypeParameter ( ) ) {
125+ const constraint = checker . getBaseConstraintOfType ( p ) ;
126+ if ( ! constraint || checker . isTypeAssignableTo ( nodeType , constraint ) ) {
127+ return nodeType ;
128+ }
129+ }
130+
131+ if ( checker . isTypeAssignableTo ( nodeType , p ) ) {
132+ return nodeType ;
133+ }
134+
135+ return p ;
136+ } )
137+
138+ const generateSchema = ( type : ts . Type ) : Schema => {
139+
140+ const literalValueSuggestions = getLiteralValueSuggestions ( type )
141+ const suggestions = {
142+ suggestions : [ ...literalValueSuggestions ]
143+ }
144+
145+ if ( isPrimitiveLiteralUnion ( type ) ) return { input : "select" , ...suggestions }
146+
147+ if ( isBoolean ( type ) ) return { input : "boolean" , ...suggestions }
148+ if ( isNumber ( type ) ) return { input : "number" , ...suggestions }
149+ if ( isString ( type ) ) return { input : "text" , ...suggestions }
150+
151+ if ( isSubFlow ( type ) ) return { input : "sub-flow" , ...suggestions }
152+
153+ if ( isArrayType ( checker , type ) ) {
154+ const itemType = checker . getTypeArguments ( type as ts . TypeReference ) [ 0 ]
155+ const itemTypes = itemType . isUnion ( ) ? itemType . types : [ itemType ]
156+ const itemSchemas = itemTypes . map ( itemType => generateSchema ( itemType ) )
157+
158+ return {
159+ input : "list" ,
160+ items : itemSchemas . length === 1 ? itemSchemas [ 0 ] : itemSchemas ,
161+ ...suggestions
162+ }
163+ }
164+
165+ if (
166+ ( type . flags & ts . TypeFlags . Object ) !== 0
167+ ) {
168+ const properties : Record < string , Schema | Schema [ ] > = { }
169+ const required : string [ ] = [ ]
170+
171+ for ( const property of checker . getPropertiesOfType ( type ) ) {
172+ const declaration = property . valueDeclaration ?? property . declarations ?. [ 0 ]
173+
174+ if ( ! declaration ) continue
175+
176+ const propertyType = checker . getTypeOfSymbolAtLocation ( property , declaration )
177+
178+ const isOptional =
179+ ( property . flags & ts . SymbolFlags . Optional ) !== 0 ||
180+ (
181+ propertyType . isUnion ( ) &&
182+ propertyType . types . some ( t => ( t . flags & ts . TypeFlags . Undefined ) !== 0 )
183+ )
184+
185+ const propertyTypes = propertyType . isUnion ( )
186+ ? propertyType . types . filter ( t => ( t . flags & ts . TypeFlags . Undefined ) === 0 )
187+ : [ propertyType ]
188+
189+ const propertySchemas = propertyTypes . map ( generateSchema )
190+
191+ properties [ property . name ] =
192+ propertySchemas . length === 1 ? propertySchemas [ 0 ] : propertySchemas
193+
194+ if ( ! isOptional ) {
195+ required . push ( property . name )
196+ }
197+ }
198+
199+ return {
200+ input : "data" ,
201+ properties,
202+ required,
203+ ...suggestions
204+ }
205+ }
206+
207+ return {
208+ input : "generic"
209+ }
210+ }
211+
212+ const funktionDependencies = getParameterDependencies ( funktion ! )
213+
214+ return combinedParameterTypes ?. map ( ( value , index ) => {
215+ return {
216+ schema : generateSchema ( value ) ,
217+ blockedBy : funktionDependencies
218+ . filter ( dep => dep . parameterIndex === index )
219+ . map ( dep => dep . dependsOnIndex )
220+ }
221+ } ) || [ ]
222+ }
223+
224+ function getLiteralValueSuggestions ( type : ts . Type ) : LiteralValue [ ] {
225+
226+ if ( type . isUnion ( ) ) {
227+ return type . types . flatMap ( getLiteralValueSuggestions )
228+ }
229+
230+ if ( type . isStringLiteral ( ) ) return [ {
231+ value : ( type as StringLiteralType ) . value ,
232+ __typename : "LiteralValue"
233+ } ]
234+
235+ if ( type . isNumberLiteral ( ) ) return [ {
236+ value : ( type as NumberLiteralType ) . value . toString ( ) ,
237+ } ]
238+
239+ if ( ( type as any ) . intrinsicName === "true" ) return [ {
240+ value : "true" ,
241+ __typename : "LiteralValue"
242+ } ]
243+
244+ if ( ( type as any ) . intrinsicName === "false" ) return [ {
245+ value : "true" ,
246+ __typename : "LiteralValue"
247+ } ]
248+
249+ return [ ]
250+ }
251+
252+ function isBoolean ( type : ts . Type ) : boolean {
253+ return (
254+ ( type . flags & ts . TypeFlags . Boolean ) !== 0 ||
255+ ( type . flags & ts . TypeFlags . BooleanLiteral ) !== 0
256+ )
257+ }
258+
259+ function isSubFlow ( type : ts . Type ) : boolean {
260+ return (
261+ type . getCallSignatures ( ) . length > 0
262+ )
263+ }
264+
265+ function isNumber ( type : ts . Type ) : boolean {
266+ return (
267+ ( type . flags & ts . TypeFlags . Number ) !== 0 ||
268+ ( type . flags & ts . TypeFlags . NumberLiteral ) !== 0
269+ )
270+ }
271+
272+ function isString ( type : ts . Type ) : boolean {
273+ return (
274+ ( type . flags & ts . TypeFlags . String ) !== 0 ||
275+ ( type . flags & ts . TypeFlags . StringLiteral ) !== 0
276+ )
277+ }
278+
279+ function isPrimitive ( type : ts . Type ) : boolean {
280+ return isString ( type ) || isNumber ( type ) || isBoolean ( type )
281+ }
282+
283+ function isPrimitiveLiteralUnion ( type : ts . Type ) : boolean {
284+ if ( ! type . isUnion ( ) ) return false
285+ return type . types . every ( isPrimitive )
286+ }
287+
288+ function isArrayType ( checker : ts . TypeChecker , type : ts . Type ) : boolean {
289+ return checker . isArrayType ( type ) || checker . isTupleType ( type )
290+ }
291+
292+ function getParameterDependencies ( node : ts . FunctionDeclaration ) {
293+ const typeParamNames = node . typeParameters ?. map ( tp => tp . name . getText ( ) ) || [ ] ;
294+ const usage : Record < string , number [ ] > = { } ;
295+
296+ node . parameters . forEach ( ( p , i ) => {
297+ const text = p . type ?. getText ( ) || "" ;
298+ typeParamNames . forEach ( t => {
299+ if ( text . includes ( t ) ) ( usage [ t ] ??= [ ] ) . push ( i ) ;
300+ } ) ;
301+ } ) ;
302+
303+ return Object . values ( usage )
304+ . filter ( indices => indices . length > 1 )
305+ . map ( ( [ first , ...rest ] ) => rest . map ( idx => ( { parameterIndex : idx , dependsOnIndex : first } ) ) )
306+ . flat ( ) ;
307+ }
0 commit comments