Skip to content

Commit 9ffffa2

Browse files
committed
feat: implement getSchema function for enhanced flow schema generation
1 parent 236412c commit 9ffffa2

2 files changed

Lines changed: 311 additions & 3 deletions

File tree

src/suggestion/getSchema.ts

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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+
}

test/getReferenceSuggestions.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {describe, it} from 'vitest';
22
import {getReferenceSuggestions} from '../src/suggestion/getReferenceSuggestions';
33
import {Flow} from "@code0-tech/sagittarius-graphql-types";
44
import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data";
5+
import {getSchema} from "../src/suggestion/getSchema";
56

67
describe('getReferenceSuggestions', () => {
78
it('sd', () => {
@@ -75,7 +76,7 @@ describe('getReferenceSuggestions', () => {
7576
"identifier": "value"
7677
},
7778
"value": null
78-
}
79+
},
7980
]
8081
}
8182
}
@@ -123,9 +124,9 @@ describe('getReferenceSuggestions', () => {
123124
}
124125
};
125126

126-
const suggestions = getReferenceSuggestions(flow, "gid://sagittarius/NodeFunction/4", 0, FUNCTION_SIGNATURES, DATA_TYPES);
127+
const schema = getSchema(flow, DATA_TYPES, FUNCTION_SIGNATURES, "gid://sagittarius/NodeFunction/3");
127128

128-
//expect(suggestions.some(s => !s.nodeFunctionId)).toBe(true);
129+
console.dir(schema, { depth: null, colors: true });
129130
});
130131

131132
it('2', () => {

0 commit comments

Comments
 (0)