diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 7cedf04348bf..c5778aa6493d 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -44,6 +44,7 @@ "monaco-editor-auto-typings": "^0.4.5", "passport": "^0.7.0", "psl": "^1.9.0", + "ts-morph": "^24.0.0", "tsconfig-paths": "^4.2.0", "typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch", "unzipper": "^0.12.3", diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index 191dc9edf414..7e0fcbf9deec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -4,19 +4,24 @@ import { InjectRepository } from '@nestjs/typeorm'; import { basename, dirname, join } from 'path'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { Repository } from 'typeorm'; import deepEqual from 'deep-equal'; +import { Repository } from 'typeorm'; import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; -import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content'; +import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; +import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; +import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; +import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; +import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; import { ServerlessFunctionEntity, @@ -27,11 +32,6 @@ import { ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { isDefined } from 'src/utils/is-defined'; -import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; -import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; -import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; -import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; -import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; @Injectable() export class ServerlessFunctionService extends TypeOrmQueryService { diff --git a/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts new file mode 100644 index 000000000000..8829699154b5 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts @@ -0,0 +1,106 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { CodeIntrospectionException } from 'src/modules/code-introspection/code-introspection.exception'; +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; + +describe('CodeIntrospectionService', () => { + let service: CodeIntrospectionService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CodeIntrospectionService], + }).compile(); + + service = module.get(CodeIntrospectionService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('analyze', () => { + it('should analyze a function declaration correctly', () => { + const fileContent = ` + function testFunction(param1: string, param2: number): void { + console.log(param1, param2); + } + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string' }, + { name: 'param2', type: 'number' }, + ]); + }); + + it('should analyze an arrow function correctly', () => { + const fileContent = ` + const testArrowFunction = (param1: string, param2: number): void => { + console.log(param1, param2); + }; + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string' }, + { name: 'param2', type: 'number' }, + ]); + }); + + it('should return an empty array for files without functions', () => { + const fileContent = ` + const x = 5; + console.log(x); + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([]); + }); + + it('should throw an exception for multiple function declarations', () => { + const fileContent = ` + function func1(param1: string) {} + function func2(param2: number) {} + `; + + expect(() => service.analyze(fileContent)).toThrow( + CodeIntrospectionException, + ); + expect(() => service.analyze(fileContent)).toThrow( + 'Only one function is allowed', + ); + }); + + it('should throw an exception for multiple arrow functions', () => { + const fileContent = ` + const func1 = (param1: string) => {}; + const func2 = (param2: number) => {}; + `; + + expect(() => service.analyze(fileContent)).toThrow( + CodeIntrospectionException, + ); + expect(() => service.analyze(fileContent)).toThrow( + 'Only one arrow function is allowed', + ); + }); + + it('should correctly analyze complex types', () => { + const fileContent = ` + function complexFunction(param1: string[], param2: { key: number }): Promise { + return Promise.resolve(true); + } + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string[]' }, + { name: 'param2', type: '{ key: number; }' }, + ]); + }); + }); +}); diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts new file mode 100644 index 000000000000..22ebbd7bf300 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts @@ -0,0 +1,12 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class CodeIntrospectionException extends CustomException { + code: CodeIntrospectionExceptionCode; + constructor(message: string, code: CodeIntrospectionExceptionCode) { + super(message, code); + } +} + +export enum CodeIntrospectionExceptionCode { + ONLY_ONE_FUNCTION_ALLOWED = 'ONLY_ONE_FUNCTION_ALLOWED', +} diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts new file mode 100644 index 000000000000..d12a94ecf4ee --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; + +@Module({ + providers: [CodeIntrospectionService], + exports: [CodeIntrospectionService], +}) +export class CodeIntrospectionModule {} diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts new file mode 100644 index 000000000000..31e18b25fe09 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; + +import { + ArrowFunction, + FunctionDeclaration, + ParameterDeclaration, + Project, + SyntaxKind, +} from 'ts-morph'; + +import { + CodeIntrospectionException, + CodeIntrospectionExceptionCode, +} from 'src/modules/code-introspection/code-introspection.exception'; + +type FunctionParameter = { + name: string; + type: string; +}; + +@Injectable() +export class CodeIntrospectionService { + private project: Project; + + constructor() { + this.project = new Project(); + } + + public analyze( + fileContent: string, + fileName = 'temp.ts', + ): FunctionParameter[] { + const sourceFile = this.project.createSourceFile(fileName, fileContent, { + overwrite: true, + }); + + const functionDeclarations = sourceFile.getFunctions(); + + if (functionDeclarations.length > 0) { + return this.analyzeFunctions(functionDeclarations); + } + + const arrowFunctions = sourceFile.getDescendantsOfKind( + SyntaxKind.ArrowFunction, + ); + + if (arrowFunctions.length > 0) { + return this.analyzeArrowFunctions(arrowFunctions); + } + + return []; + } + + private analyzeFunctions( + functionDeclarations: FunctionDeclaration[], + ): FunctionParameter[] { + if (functionDeclarations.length > 1) { + throw new CodeIntrospectionException( + 'Only one function is allowed', + CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED, + ); + } + + const functionDeclaration = functionDeclarations[0]; + + return functionDeclaration.getParameters().map(this.buildFunctionParameter); + } + + private analyzeArrowFunctions( + arrowFunctions: ArrowFunction[], + ): FunctionParameter[] { + if (arrowFunctions.length > 1) { + throw new CodeIntrospectionException( + 'Only one arrow function is allowed', + CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED, + ); + } + + const arrowFunction = arrowFunctions[0]; + + return arrowFunction.getParameters().map(this.buildFunctionParameter); + } + + private buildFunctionParameter( + parameter: ParameterDeclaration, + ): FunctionParameter { + return { + name: parameter.getName(), + type: parameter.getType().getText(), + }; + } +} diff --git a/yarn.lock b/yarn.lock index d3400e4a26b3..9d8eabefb6c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15073,6 +15073,17 @@ __metadata: languageName: node linkType: hard +"@ts-morph/common@npm:~0.25.0": + version: 0.25.0 + resolution: "@ts-morph/common@npm:0.25.0" + dependencies: + minimatch: "npm:^9.0.4" + path-browserify: "npm:^1.0.1" + tinyglobby: "npm:^0.2.9" + checksum: 10c0/c67e66db678e44886e9823e6482834acebfae0ea52ccbfa2af1ca9abfe5a9774dad6e852c8f480909bc196175f17e15454af71d7a41a1c137db09e74f046a830 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -22021,6 +22032,13 @@ __metadata: languageName: node linkType: hard +"code-block-writer@npm:^13.0.3": + version: 13.0.3 + resolution: "code-block-writer@npm:13.0.3" + checksum: 10c0/87db97b37583f71cfd7eced8bf3f0a0a0ca53af912751a734372b36c08cd27f3e8a4878ec05591c0cd9ae11bea8add1423e132d660edd86aab952656dd41fd66 + languageName: node + linkType: hard + "code-point-at@npm:^1.0.0": version: 1.1.0 resolution: "code-point-at@npm:1.1.0" @@ -26491,6 +26509,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.0": + version: 6.4.0 + resolution: "fdir@npm:6.4.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/9a03efa1335d78ea386b701799b08ad9e7e8da85d88567dc162cd28dd8e9486e8c269b3e95bfeb21dd6a5b14ebf69d230eb6e18f49d33fbda3cd97432f648c48 + languageName: node + linkType: hard + "fetch-retry@npm:^5.0.2": version: 5.0.6 resolution: "fetch-retry@npm:5.0.6" @@ -43048,6 +43078,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.9": + version: 0.2.9 + resolution: "tinyglobby@npm:0.2.9" + dependencies: + fdir: "npm:^6.4.0" + picomatch: "npm:^4.0.2" + checksum: 10c0/f65f847afe70f56de069d4f1f9c3b0c1a76aaf2b0297656754734a83b9bac8e105b5534dfbea8599560476b88f7b747d0855370a957a07246d18b976addb87ec + languageName: node + linkType: hard + "tinypool@npm:^0.8.2": version: 0.8.4 resolution: "tinypool@npm:0.8.4" @@ -43474,6 +43514,16 @@ __metadata: languageName: node linkType: hard +"ts-morph@npm:^24.0.0": + version: 24.0.0 + resolution: "ts-morph@npm:24.0.0" + dependencies: + "@ts-morph/common": "npm:~0.25.0" + code-block-writer: "npm:^13.0.3" + checksum: 10c0/2a0813ba428a154966d4038901f6c32457a60870936b23778f2629433257f87d1881fc4ecae7b791a223a88c2edf96aaac9fb0f88bf34d3c652af8c09c4f43bc + languageName: node + linkType: hard + "ts-node@npm:10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -43817,6 +43867,7 @@ __metadata: passport: "npm:^0.7.0" psl: "npm:^1.9.0" rimraf: "npm:^5.0.5" + ts-morph: "npm:^24.0.0" tsconfig-paths: "npm:^4.2.0" typeorm: "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch" typescript: "npm:5.3.3"