import { type ArrayTypeNode, type ArrowFunction, createSourceFile, type FunctionDeclaration, type FunctionLikeDeclaration, type Identifier, type LiteralTypeNode, type Node, type PropertySignature, ScriptTarget, type StringLiteral, SyntaxKind, type TypeNode, type TypeReferenceNode, type UnionTypeNode, type VariableStatement, } from 'typescript'; import { type InputJsonSchema } from '@/logic-function'; import { isDefined } from '@/utils/validation/isDefined'; const TWENTY_RECORD_TYPE_NAME = 'TwentyRecord'; const getObjectUniversalIdentifierFromTypeArgument = ( typeReferenceNode: TypeReferenceNode, ): string | undefined => { const typeArgument = typeReferenceNode.typeArguments?.[0]; if ( isDefined(typeArgument) && typeArgument.kind === SyntaxKind.LiteralType && (typeArgument as LiteralTypeNode).literal.kind === SyntaxKind.StringLiteral ) { return ((typeArgument as LiteralTypeNode).literal as StringLiteral).text; } return undefined; }; const buildArraySchemaFromItems = (items: InputJsonSchema): InputJsonSchema => items.type === 'record' ? { type: 'records', objectUniversalIdentifier: items.objectUniversalIdentifier, } : { type: 'array', items }; const getTypeString = (typeNode: TypeNode): InputJsonSchema => { switch (typeNode.kind) { case SyntaxKind.NumberKeyword: return { type: 'number' }; case SyntaxKind.StringKeyword: return { type: 'string' }; case SyntaxKind.BooleanKeyword: return { type: 'boolean' }; case SyntaxKind.ArrayType: return buildArraySchemaFromItems( getTypeString((typeNode as ArrayTypeNode).elementType), ); case SyntaxKind.TypeReference: { const typeReferenceNode = typeNode as TypeReferenceNode; const typeName = typeReferenceNode.typeName.kind === SyntaxKind.Identifier ? (typeReferenceNode.typeName as Identifier).text : undefined; if (typeName === 'Array' || typeName === 'ReadonlyArray') { const elementType = typeReferenceNode.typeArguments?.[0]; return buildArraySchemaFromItems( isDefined(elementType) ? getTypeString(elementType) : {}, ); } if (typeName === TWENTY_RECORD_TYPE_NAME) { const objectUniversalIdentifier = getObjectUniversalIdentifierFromTypeArgument(typeReferenceNode); if (isDefined(objectUniversalIdentifier)) { return { type: 'record', objectUniversalIdentifier }; } } return {}; } case SyntaxKind.ObjectKeyword: return { type: 'object' }; case SyntaxKind.TypeLiteral: { const properties: InputJsonSchema['properties'] = {}; (typeNode as any).members.forEach((member: PropertySignature) => { if (isDefined(member.name) && isDefined(member.type)) { const memberName = (member.name as any).text; properties[memberName] = getTypeString(member.type); } }); return { type: 'object', properties }; } case SyntaxKind.UnionType: { const unionNode = typeNode as UnionTypeNode; const enumValues: string[] = []; let isEnum = true; unionNode.types.forEach((subType) => { if (subType.kind === SyntaxKind.LiteralType) { const literal = (subType as LiteralTypeNode).literal; if (literal.kind === SyntaxKind.StringLiteral) { enumValues.push((literal as StringLiteral).text); } else { isEnum = false; } } else { isEnum = false; } }); if (isEnum) { return { type: 'string', enum: enumValues }; } return {}; } default: return {}; } }; const computeFunctionParameters = ( funcNode: FunctionDeclaration | FunctionLikeDeclaration | ArrowFunction, schema: InputJsonSchema[], ): InputJsonSchema[] => { const params = funcNode.parameters; return params.reduce((updatedSchema, param) => { const typeNode = param.type; if (isDefined(typeNode)) { return [...updatedSchema, getTypeString(typeNode)]; } else { return [...updatedSchema, {}]; } }, schema); }; const extractFunctions = (node: Node): FunctionLikeDeclaration[] => { if (node.kind === SyntaxKind.FunctionDeclaration) { return [node as FunctionDeclaration]; } if (node.kind === SyntaxKind.VariableStatement) { const varStatement = node as VariableStatement; return varStatement.declarationList.declarations .filter( (declaration) => isDefined(declaration.initializer) && declaration.initializer.kind === SyntaxKind.ArrowFunction, ) .map((declaration) => declaration.initializer as ArrowFunction); } return []; }; export const getFunctionInputSchema = ( fileContent: string, ): InputJsonSchema[] => { const sourceFile = createSourceFile( 'temp.ts', fileContent, ScriptTarget.ESNext, true, ); let schema: InputJsonSchema[] = []; sourceFile.forEachChild((node) => { if ( node.kind === SyntaxKind.FunctionDeclaration || node.kind === SyntaxKind.VariableStatement ) { const functions = extractFunctions(node); functions.forEach((func) => { schema = computeFunctionParameters(func, schema); }); } }); return schema; };