From da9e2d02423862e557b3232c549cf2f6b0325844 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 16 Oct 2024 17:55:37 +0200 Subject: [PATCH 1/3] Build service with ts-morph --- packages/twenty-server/package.json | 1 + .../code-introspection.exception.ts | 12 +++ .../code-introspection.module.ts | 9 ++ .../code-introspection.service.ts | 90 +++++++++++++++++++ .../serverless-function.module.ts | 2 + .../serverless-function.service.ts | 18 ++-- yarn.lock | 51 +++++++++++ 7 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.exception.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service.ts 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/code-introspection/code-introspection.exception.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.exception.ts new file mode 100644 index 000000000000..22ebbd7bf300 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/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/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module.ts new file mode 100644 index 000000000000..9d2acb250403 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { CodeIntrospectionService } from 'src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service'; + +@Module({ + providers: [CodeIntrospectionService], + exports: [CodeIntrospectionService], +}) +export class CodeIntrospectionModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service.ts new file mode 100644 index 000000000000..77ed32b63be2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; + +import { + ArrowFunction, + FunctionDeclaration, + Project, + SyntaxKind, +} from 'ts-morph'; + +import { + CodeIntrospectionException, + CodeIntrospectionExceptionCode, +} from 'src/engine/metadata-modules/serverless-function/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((parameter) => { + return { + name: parameter.getName(), + type: parameter.getType().getText(), + }; + }); + } + + private analyzeArrowFunctions(arrowFunctions: ArrowFunction[]) { + 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((parameter) => { + return { + name: parameter.getName(), + type: parameter.getType().getText(), + }; + }); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts index 076343f4d206..95cadf27c682 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts @@ -14,6 +14,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module'; import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { CodeIntrospectionModule } from 'src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module'; import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver'; @@ -31,6 +32,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), FileModule, ThrottlerModule, + CodeIntrospectionModule, ], services: [ServerlessFunctionService], resolvers: [ 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..db162ac12af3 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 { @@ -81,9 +81,11 @@ export class ServerlessFunctionService extends TypeOrmQueryService Date: Thu, 17 Oct 2024 10:09:45 +0200 Subject: [PATCH 2/3] Move to modules and add test --- .../serverless-function.module.ts | 2 - .../serverless-function.service.ts | 4 +- .../code-introspection.service.spec.ts | 106 ++++++++++++++++++ .../code-introspection.exception.ts | 0 .../code-introspection.module.ts | 2 +- .../code-introspection.service.ts | 2 +- 6 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts rename packages/twenty-server/src/{engine/metadata-modules/serverless-function => modules}/code-introspection/code-introspection.exception.ts (100%) rename packages/twenty-server/src/{engine/metadata-modules/serverless-function => modules}/code-introspection/code-introspection.module.ts (56%) rename packages/twenty-server/src/{engine/metadata-modules/serverless-function => modules}/code-introspection/code-introspection.service.ts (95%) diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts index 95cadf27c682..076343f4d206 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts @@ -14,7 +14,6 @@ import { FileModule } from 'src/engine/core-modules/file/file.module'; import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { CodeIntrospectionModule } from 'src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module'; import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver'; @@ -32,7 +31,6 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), FileModule, ThrottlerModule, - CodeIntrospectionModule, ], services: [ServerlessFunctionService], resolvers: [ 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 db162ac12af3..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 @@ -81,11 +81,9 @@ export class ServerlessFunctionService extends TypeOrmQueryService { + 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/engine/metadata-modules/serverless-function/code-introspection/code-introspection.exception.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.exception.ts rename to packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts similarity index 56% rename from packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module.ts rename to packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts index 9d2acb250403..d12a94ecf4ee 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.module.ts +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { CodeIntrospectionService } from 'src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service'; +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; @Module({ providers: [CodeIntrospectionService], diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts similarity index 95% rename from packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service.ts rename to packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts index 77ed32b63be2..de7b976d1d2e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.service.ts +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts @@ -10,7 +10,7 @@ import { import { CodeIntrospectionException, CodeIntrospectionExceptionCode, -} from 'src/engine/metadata-modules/serverless-function/code-introspection/code-introspection.exception'; +} from 'src/modules/code-introspection/code-introspection.exception'; type FunctionParameter = { name: string; From 7e12296d5a3aa6b774c79a38dbdfa5a47e927dc4 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 17 Oct 2024 13:51:15 +0200 Subject: [PATCH 3/3] Factorise function param building --- .../code-introspection.service.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) 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 index de7b976d1d2e..31e18b25fe09 100644 --- a/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { ArrowFunction, FunctionDeclaration, + ParameterDeclaration, Project, SyntaxKind, } from 'ts-morph'; @@ -62,15 +63,12 @@ export class CodeIntrospectionService { const functionDeclaration = functionDeclarations[0]; - return functionDeclaration.getParameters().map((parameter) => { - return { - name: parameter.getName(), - type: parameter.getType().getText(), - }; - }); + return functionDeclaration.getParameters().map(this.buildFunctionParameter); } - private analyzeArrowFunctions(arrowFunctions: ArrowFunction[]) { + private analyzeArrowFunctions( + arrowFunctions: ArrowFunction[], + ): FunctionParameter[] { if (arrowFunctions.length > 1) { throw new CodeIntrospectionException( 'Only one arrow function is allowed', @@ -80,11 +78,15 @@ export class CodeIntrospectionService { const arrowFunction = arrowFunctions[0]; - return arrowFunction.getParameters().map((parameter) => { - return { - name: parameter.getName(), - type: parameter.getType().getText(), - }; - }); + return arrowFunction.getParameters().map(this.buildFunctionParameter); + } + + private buildFunctionParameter( + parameter: ParameterDeclaration, + ): FunctionParameter { + return { + name: parameter.getName(), + type: parameter.getType().getText(), + }; } }