From e35b4b86cb533c65cb026c58d0a9820ab266bb41 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:32:22 +0100 Subject: [PATCH] Migrate serverless function service to v2 (#17285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Introduction In this PR we're migrating the serverless function service that was using the SF repo directly to the v2 build and runner. The whole serverless engine now deals with flat entities only ## Resolvers Refactored the resolvers ( serverlessFunction, route, database and cron trigger) : - return types to `dto` - Standardized the flat to dto transpilation within the resolvers - Find and findMany passing by the cached data ## Services Refactored the services ( serverlessFunction, route, database and cron trigger) : - return type to be `flat` - always calling v2 and computing cache ## New additional caches - application variables ( cf https://github.com/twentyhq/core-team-issues/issues/2116 ) - serverless function layer ## What to test: - CRUD ( database trigger ✅ , route trigger, cron trigger, serverless function through workflows ✅ ) - Duplicating a workflow with a serverless function code node ✅ ## Concerns We need to implement the cron that will hard delete soft deleted s3 serverless functions, not in this PR though ( cf https://github.com/twentyhq/twenty/pull/17285#discussion_r2709168570 and https://github.com/twentyhq/core-team-issues/issues/2118 ) --- .../application/application-sync.service.ts | 2 + .../application-variable.module.ts | 13 +- .../application-variable.resolver.ts | 10 +- .../application-variable.service.ts | 16 + ...-application-variable-map-cache.service.ts | 68 ++ .../application-variable-cache-maps.type.ts | 6 + .../types/flat-application-variable.type.ts | 4 + ...ntity-to-flat-application-variable.util.ts | 15 + .../interfaces/serverless-driver.interface.ts | 12 +- .../serverless/drivers/lambda.driver.ts | 91 ++- .../serverless/drivers/local.driver.ts | 45 +- .../serverless/drivers/utils/build-env-var.ts | 14 +- .../utils/copy-and-build-dependencies.ts | 8 +- .../serverless/serverless.service.ts | 21 +- .../utils/serverless-get-folder.utils.ts | 18 +- .../resolvers/cron-trigger.resolver.ts | 91 ++- .../services/cron-trigger-v2.service.ts | 6 +- ...gger-graphql-api-exception-handler.util.ts | 2 +- ...t-cron-trigger-to-cron-trigger-dto.util.ts | 11 + .../database-event-trigger.resolver.ts | 104 ++- .../database-event-trigger-v2.service.ts | 6 +- ...ger-graphql-api-exception-handler.utils.ts | 2 +- ...gger-to-database-event-trigger-dto.util.ts | 11 + ...rties-to-compare-and-stringify.constant.ts | 6 +- .../resolvers/route-trigger.resolver.ts | 91 ++- .../services/route-trigger-v2.service.ts | 6 +- ...route-trigger-to-route-trigger-dto.util.ts | 14 + ...ger-graphql-api-exception-handler.utils.ts | 2 +- .../serverless-function-layer.module.ts | 14 +- .../serverless-function-layer.service.ts | 24 +- ...erless-function-layer-map-cache.service.ts | 47 ++ .../flat-serverless-function-layer.type.ts | 5 + ...rverless-function-layer-cache-maps.type.ts | 5 + ...-to-flat-serverless-function-layer.util.ts | 15 + .../serverless-function.module.ts | 2 + .../serverless-function.resolver.ts | 248 +++++-- .../serverless-function.service.ts | 621 ++++++++++++------ ...t-serverless-function-map-cache.service.ts | 2 +- ...-flat-serverless-function-or-throw.util.ts | 33 + ...-input-to-flat-serverless-function.util.ts | 4 +- ...unction-to-serverless-function-dto.util.ts | 59 ++ ...ntity-to-flat-serverless-function.util.ts} | 0 ...erless-function-to-update-or-throw.util.ts | 23 +- .../types/workspace-cache-key.type.ts | 6 + ...verless-function-action-handler.service.ts | 10 +- ...verless-function-action-handler.service.ts | 18 +- ...verless-function-action-handler.service.ts | 24 +- .../workflow-common.workspace-service.ts | 1 + ...-step-operations.workspace-service.spec.ts | 63 +- ...rsion-step-operations.workspace-service.ts | 1 + 50 files changed, 1415 insertions(+), 505 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/applicationVariable/types/flat-application-variable.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts rename packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/{from-serverless-function-entity-to-flat-serverless-function.type.ts => from-serverless-function-entity-to-flat-serverless-function.util.ts} (100%) diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index 8bd046ca72c..136a3566722 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -177,6 +177,7 @@ export class ApplicationSyncService { packageJson, yarnLock, }, + workspaceId, ); } @@ -184,6 +185,7 @@ export class ApplicationSyncService { { applicationVariables: manifest.application.applicationVariables, applicationId: application.id, + workspaceId, }, ); diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts index e00580d8452..979d050c28b 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts @@ -1,21 +1,30 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; -import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; import { ApplicationVariableEntityResolver } from 'src/engine/core-modules/applicationVariable/application-variable.resolver'; +import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; +import { WorkspaceApplicationVariableMapCacheService } from 'src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ NestjsQueryTypeOrmModule.forFeature([ApplicationVariableEntity]), + TypeOrmModule.forFeature([ApplicationVariableEntity]), PermissionsModule, + WorkspaceCacheModule, ], providers: [ ApplicationVariableEntityService, ApplicationVariableEntityResolver, + WorkspaceApplicationVariableMapCacheService, + ], + exports: [ + ApplicationVariableEntityService, + WorkspaceApplicationVariableMapCacheService, ], - exports: [ApplicationVariableEntityService], }) export class ApplicationVariableEntityModule {} diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts index 211a27cb2ea..84514b78720 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts @@ -6,6 +6,8 @@ import { PermissionFlagType } from 'twenty-shared/constants'; import { ApplicationVariableEntityExceptionFilter } from 'src/engine/core-modules/applicationVariable/application-variable-exception-filter'; import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; import { UpdateApplicationVariableEntityInput } from 'src/engine/core-modules/applicationVariable/dtos/update-application-variable.input'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; @@ -23,8 +25,14 @@ export class ApplicationVariableEntityResolver { @Mutation(() => Boolean) async updateOneApplicationVariable( @Args() { key, value, applicationId }: UpdateApplicationVariableEntityInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { - await this.applicationVariableService.update({ key, value, applicationId }); + await this.applicationVariableService.update({ + key, + value, + applicationId, + workspaceId, + }); return true; } diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts index f16096a585e..addec34de07 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts @@ -1,3 +1,4 @@ +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; @@ -5,19 +6,24 @@ import { In, Not, Repository } from 'typeorm'; import { ApplicationVariables } from 'twenty-shared/application'; import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +@Injectable() export class ApplicationVariableEntityService { constructor( @InjectRepository(ApplicationVariableEntity) private readonly applicationVariableRepository: Repository, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async update({ key, value, applicationId, + workspaceId, }: Pick & { applicationId: string; + workspaceId: string; }) { await this.applicationVariableRepository.update( { key, applicationId }, @@ -25,14 +31,20 @@ export class ApplicationVariableEntityService { value, }, ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'applicationVariableMaps', + ]); } async upsertManyApplicationVariableEntities({ applicationVariables, applicationId, + workspaceId, }: { applicationVariables?: ApplicationVariables; applicationId: string; + workspaceId: string; }) { if (!isDefined(applicationVariables)) { return; @@ -74,5 +86,9 @@ export class ApplicationVariableEntityService { applicationId, key: Not(In(Object.keys(applicationVariables))), }); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'applicationVariableMaps', + ]); } } diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service.ts new file mode 100644 index 00000000000..0e84bbb9d9e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; +import { isDefined } from 'twenty-shared/utils'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type'; +import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; + +@Injectable() +@WorkspaceCache('applicationVariableMaps') +export class WorkspaceApplicationVariableMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ApplicationVariableEntity) + private readonly applicationVariableRepository: Repository, + ) { + super(); + } + + async computeForCache( + workspaceId: string, + ): Promise { + const applicationVariableEntities = await this.applicationVariableRepository + .createQueryBuilder('applicationVariable') + .innerJoin('applicationVariable.application', 'application') + .where('application.workspaceId = :workspaceId', { workspaceId }) + .getMany(); + + const applicationVariableMaps: ApplicationVariableCacheMaps = { + byId: {}, + byApplicationId: {}, + }; + + for (const entity of applicationVariableEntities) { + const flatApplicationVariable = + fromApplicationVariableEntityToFlatApplicationVariable(entity); + + applicationVariableMaps.byId[flatApplicationVariable.id] = + flatApplicationVariable; + + if (!isDefined(flatApplicationVariable.applicationId)) { + continue; + } + if ( + !isDefined( + applicationVariableMaps.byApplicationId[ + flatApplicationVariable.applicationId + ], + ) + ) { + applicationVariableMaps.byApplicationId[ + flatApplicationVariable.applicationId + ] = [flatApplicationVariable]; + continue; + } + + applicationVariableMaps.byApplicationId[ + flatApplicationVariable.applicationId + ]?.push(flatApplicationVariable); + } + + return applicationVariableMaps; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type.ts new file mode 100644 index 00000000000..0a25616135c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type.ts @@ -0,0 +1,6 @@ +import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; + +export type ApplicationVariableCacheMaps = { + byId: Partial>; + byApplicationId: Partial>; +}; diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/types/flat-application-variable.type.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/flat-application-variable.type.ts new file mode 100644 index 00000000000..e01f18d6db1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/flat-application-variable.type.ts @@ -0,0 +1,4 @@ +import { type ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type'; + +export type FlatApplicationVariable = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util.ts new file mode 100644 index 00000000000..ca37dacc646 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util.ts @@ -0,0 +1,15 @@ +import { type ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; + +export const fromApplicationVariableEntityToFlatApplicationVariable = ( + entity: ApplicationVariableEntity, +): FlatApplicationVariable => ({ + id: entity.id, + key: entity.key, + value: entity.value, + description: entity.description, + isSecret: entity.isSecret, + applicationId: entity.applicationId, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), +}); diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts index 9fe554b4505..58525855d44 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts @@ -1,5 +1,6 @@ +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; import { type ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; export type ServerlessExecuteError = { errorType: string; @@ -15,16 +16,17 @@ export type ServerlessExecuteResult = { error?: ServerlessExecuteError; }; -// TODO refactor to be using FlatServerlessFunction export interface ServerlessDriver { - delete(serverlessFunction: ServerlessFunctionEntity): Promise; + delete(flatServerlessFunction: FlatServerlessFunction): Promise; execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts index 15e803f3234..6a1b173b3dc 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts @@ -36,16 +36,15 @@ import { LambdaBuildDirectoryManager, NODE_LAYER_SUBFOLDER, } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; -import { - type ServerlessFunctionEntity, - ServerlessFunctionRuntime, -} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { ServerlessFunctionRuntime } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionException, ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60; const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h @@ -126,11 +125,11 @@ export class LambdaDriver implements ServerlessDriver { } private async waitFunctionUpdates( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunction: FlatServerlessFunction, maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS, ) { const waitParams = { - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, }; await waitUntilFunctionUpdatedV2( @@ -139,14 +138,16 @@ export class LambdaDriver implements ServerlessDriver { ); } - private getLayerName(serverlessFunction: ServerlessFunctionEntity) { - return serverlessFunction.serverlessFunctionLayer.checksum; + private getLayerName( + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + return flatServerlessFunctionLayer.checksum; } private async createLayerIfNotExists( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ): Promise { - const layerName = this.getLayerName(serverlessFunction); + const layerName = this.getLayerName(flatServerlessFunctionLayer); const listLayerParams: ListLayerVersionsCommandInput = { LayerName: layerName, @@ -172,7 +173,10 @@ export class LambdaDriver implements ServerlessDriver { NODE_LAYER_SUBFOLDER, ); - await copyAndBuildDependencies(nodeDependenciesFolder, serverlessFunction); + await copyAndBuildDependencies( + nodeDependenciesFolder, + flatServerlessFunctionLayer, + ); await createZipFile(sourceTemporaryDir, lambdaZipPath); @@ -201,11 +205,11 @@ export class LambdaDriver implements ServerlessDriver { } private async getLambdaExecutor( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunction: FlatServerlessFunction, ) { try { const getFunctionCommand: GetFunctionCommand = new GetFunctionCommand({ - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, }); return await (await this.getLambdaClient()).send(getFunctionCommand); @@ -216,20 +220,23 @@ export class LambdaDriver implements ServerlessDriver { } } - async delete(serverlessFunction: ServerlessFunctionEntity) { - const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction); + async delete(flatServerlessFunction: FlatServerlessFunction) { + const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction); if (isDefined(lambdaExecutor)) { const deleteFunctionCommand = new DeleteFunctionCommand({ - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, }); await (await this.getLambdaClient()).send(deleteFunctionCommand); } } - private async isAlreadyBuilt(serverlessFunction: ServerlessFunctionEntity) { - const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction); + private async isAlreadyBuilt( + flatServerlessFunction: FlatServerlessFunction, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction); if (!isDefined(lambdaExecutor)) { return false; @@ -238,28 +245,38 @@ export class LambdaDriver implements ServerlessDriver { const layers = lambdaExecutor.Configuration?.Layers; if (!isDefined(layers) || layers.length !== 1) { - await this.delete(serverlessFunction); + await this.delete(flatServerlessFunction); return false; } - const layerName = this.getLayerName(serverlessFunction); + const layerName = this.getLayerName(flatServerlessFunctionLayer); if (layers[0].Arn?.includes(layerName)) { return true; } - await this.delete(serverlessFunction); + await this.delete(flatServerlessFunction); return false; } - private async build(serverlessFunction: ServerlessFunctionEntity) { - if (await this.isAlreadyBuilt(serverlessFunction)) { + private async build( + flatServerlessFunction: FlatServerlessFunction, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + if ( + await this.isAlreadyBuilt( + flatServerlessFunction, + flatServerlessFunctionLayer, + ) + ) { return; } - const layerArn = await this.createLayerIfNotExists(serverlessFunction); + const layerArn = await this.createLayerIfNotExists( + flatServerlessFunctionLayer, + ); const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager(); @@ -274,11 +291,11 @@ export class LambdaDriver implements ServerlessDriver { Code: { ZipFile: await fs.readFile(lambdaZipPath), }, - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, Layers: [layerArn], Handler: 'index.handler', Role: this.options.lambdaRole, - Runtime: serverlessFunction.runtime, + Runtime: flatServerlessFunction.runtime, Timeout: 900, // timeout is handled by the serverless function service }; @@ -305,23 +322,25 @@ export class LambdaDriver implements ServerlessDriver { } async execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; }): Promise { - await this.build(serverlessFunction); - await this.waitFunctionUpdates(serverlessFunction); + await this.build(flatServerlessFunction, flatServerlessFunctionLayer); + await this.waitFunctionUpdates(flatServerlessFunction); const startTime = Date.now(); - const folderPath = getServerlessFolder({ - serverlessFunction, + const folderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, version, }); @@ -340,7 +359,7 @@ export class LambdaDriver implements ServerlessDriver { try { builtBundleFilePath = await buildServerlessFunctionInMemory({ sourceTemporaryDir, - handlerPath: serverlessFunction.handlerPath, + handlerPath: flatServerlessFunction.handlerPath, }); } catch (error) { return formatBuildError(error, startTime); @@ -354,11 +373,11 @@ export class LambdaDriver implements ServerlessDriver { params: payload, code: compiledCode, env: env ?? {}, - handlerName: serverlessFunction.handlerName, + handlerName: flatServerlessFunction.handlerName, }; const params: InvokeCommandInput = { - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, Payload: JSON.stringify(executorPayload), LogType: LogType.Tail, }; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts index c333e953c32..1a7887045db 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts @@ -14,9 +14,10 @@ import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/dri import { formatBuildError } from 'src/engine/core-modules/serverless/drivers/utils/format-build-error'; import { ConsoleListener } from 'src/engine/core-modules/serverless/drivers/utils/intercept-console'; import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; export interface LocalDriverOptions { fileStorageService: FileStorageService; @@ -30,53 +31,55 @@ export class LocalDriver implements ServerlessDriver { } private getInMemoryLayerFolderPath = ( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ) => { - return join( - SERVERLESS_TMPDIR_FOLDER, - serverlessFunction.serverlessFunctionLayer.checksum, - ); + return join(SERVERLESS_TMPDIR_FOLDER, flatServerlessFunctionLayer.checksum); }; private async createLayerIfNotExists( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ) { - const inMemoryLayerFolderPath = - this.getInMemoryLayerFolderPath(serverlessFunction); + const inMemoryLayerFolderPath = this.getInMemoryLayerFolderPath( + flatServerlessFunctionLayer, + ); try { await fs.access(inMemoryLayerFolderPath); } catch { await copyAndBuildDependencies( inMemoryLayerFolderPath, - serverlessFunction, + flatServerlessFunctionLayer, ); } } async delete() {} - private async build(serverlessFunction: ServerlessFunctionEntity) { - await this.createLayerIfNotExists(serverlessFunction); + private async build( + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + await this.createLayerIfNotExists(flatServerlessFunctionLayer); } async execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; }): Promise { - await this.build(serverlessFunction); + await this.build(flatServerlessFunctionLayer); const startTime = Date.now(); - const folderPath = getServerlessFolder({ - serverlessFunction, + const folderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, version, }); @@ -95,7 +98,7 @@ export class LocalDriver implements ServerlessDriver { try { builtBundleFilePath = await buildServerlessFunctionInMemory({ sourceTemporaryDir, - handlerPath: serverlessFunction.handlerPath, + handlerPath: flatServerlessFunction.handlerPath, }); } catch (error) { return formatBuildError(error, startTime); @@ -104,7 +107,7 @@ export class LocalDriver implements ServerlessDriver { try { await fs.symlink( join( - this.getInMemoryLayerFolderPath(serverlessFunction), + this.getInMemoryLayerFolderPath(flatServerlessFunctionLayer), 'node_modules', ), join(sourceTemporaryDir, 'node_modules'), @@ -153,7 +156,7 @@ export class LocalDriver implements ServerlessDriver { const runnerPath = await this.writeBootstrapRunner({ dir: sourceTemporaryDir, builtFileAbsPath: builtBundleFilePath, - handlerName: serverlessFunction.handlerName, + handlerName: flatServerlessFunction.handlerName, }); const { ok, result, error, stack, stdout, stderr } = diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts index 09bab79dbd7..f9fb7c3443b 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts @@ -1,12 +1,14 @@ -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; -export const buildEnvVar = (serverlessFunction: ServerlessFunctionEntity) => { - return (serverlessFunction.application?.applicationVariables ?? []).reduce( - (acc, v) => { - acc[v.key] = String(v.value ?? ''); +export const buildEnvVar = ( + flatApplicationVariables: FlatApplicationVariable[], +): Record => { + return flatApplicationVariables.reduce>( + (acc, flatApplicationVariable) => { + acc[flatApplicationVariable.key] = flatApplicationVariable.value; return acc; }, - {} as Record, + {}, ); }; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts index 424bd823810..44ca71522a9 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts @@ -4,21 +4,21 @@ import { join } from 'path'; import { promisify } from 'util'; import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name'; -import type { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; const execFilePromise = promisify(execFile); export const copyAndBuildDependencies = async ( buildDirectory: string, - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ) => { await fs.mkdir(buildDirectory, { recursive: true, }); - const packageJson = serverlessFunction.serverlessFunctionLayer.packageJson; + const packageJson = flatServerlessFunctionLayer.packageJson; - const yarnLock = serverlessFunction.serverlessFunctionLayer.yarnLock; + const yarnLock = flatServerlessFunctionLayer.yarnLock; await fs.writeFile( join(buildDirectory, 'package.json'), diff --git a/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts b/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts index a586c6bc3bc..d1e0ab1355b 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts @@ -6,27 +6,36 @@ import { } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; @Injectable() export class ServerlessService implements ServerlessDriver { constructor(@Inject(SERVERLESS_DRIVER) private driver: ServerlessDriver) {} - async delete(serverlessFunction: ServerlessFunctionEntity): Promise { - return this.driver.delete(serverlessFunction); + async delete(flatServerlessFunction: FlatServerlessFunction): Promise { + return this.driver.delete(flatServerlessFunction); } async execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; }): Promise { - return this.driver.execute({ serverlessFunction, payload, version, env }); + return this.driver.execute({ + flatServerlessFunction, + flatServerlessFunctionLayer, + payload, + version, + env, + }); } } diff --git a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts index 6e78863d171..25243ec8ff3 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts @@ -3,23 +3,25 @@ import { join } from 'path'; import { isDefined } from 'twenty-shared/utils'; import { FileFolder } from 'twenty-shared/types'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionException, ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -export const getServerlessFolder = ({ - serverlessFunction, +export const getServerlessFolderOrThrow = ({ + flatServerlessFunction, version, toDelete = false, }: { - serverlessFunction: ServerlessFunctionEntity | FlatServerlessFunction; + flatServerlessFunction: FlatServerlessFunction; version?: 'draft' | 'latest' | (string & NonNullable); toDelete?: boolean; }) => { - if (version === 'latest' && !isDefined(serverlessFunction.latestVersion)) { + if ( + version === 'latest' && + !isDefined(flatServerlessFunction.latestVersion) + ) { throw new ServerlessFunctionException( "Can't get 'latest' version when serverlessFunction 'latestVersion' is undefined", ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND, @@ -27,14 +29,14 @@ export const getServerlessFolder = ({ } const computedVersion = - version === 'latest' ? serverlessFunction.latestVersion : version; + version === 'latest' ? flatServerlessFunction.latestVersion : version; return join( - 'workspace-' + serverlessFunction.workspaceId, + 'workspace-' + flatServerlessFunction.workspaceId, toDelete ? FileFolder.ServerlessFunctionToDelete : FileFolder.ServerlessFunction, - serverlessFunction.id, + flatServerlessFunction.id, computedVersion || '', ); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts index 93b47b62c61..632256f4fb7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts @@ -1,9 +1,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -15,9 +14,15 @@ import { CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger import { CronTriggerIdInput } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger-id.input'; import { CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto'; import { UpdateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/update-cron-trigger.input'; -import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity'; +import { + CronTriggerException, + CronTriggerExceptionCode, +} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception'; import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service'; import { cronTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util'; +import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; @UseGuards( WorkspaceAuthGuard, @@ -29,37 +34,59 @@ import { cronTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modul export class CronTriggerResolver { constructor( private readonly cronTriggerV2Service: CronTriggerV2Service, - @InjectRepository(CronTriggerEntity) - private readonly cronTriggerRepository: Repository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => CronTriggerDTO) async findOneCronTrigger( @Args('input') { id }: CronTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, + const { flatCronTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatCronTriggerMaps'], + }, + ); + + const flatCronTrigger = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatCronTriggerMaps, }); + + if (!isDefined(flatCronTrigger)) { + throw new CronTriggerException( + `Cron trigger with id ${id} not found`, + CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND, + ); + } + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @Query(() => [CronTriggerDTO]) async findManyCronTriggers( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerRepository.find({ - where: { workspaceId }, - }); + const { flatCronTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatCronTriggerMaps'], + }, + ); + + return Object.values(flatCronTriggerMaps.byId) + .filter(isDefined) + .map(fromFlatCronTriggerToCronTriggerDto); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @@ -67,14 +94,16 @@ export class CronTriggerResolver { async deleteOneCronTrigger( @Args('input') input: CronTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerV2Service.destroyOne({ + const flatCronTrigger = await this.cronTriggerV2Service.destroyOne({ destroyCronTriggerInput: input, workspaceId, }); + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @@ -83,11 +112,16 @@ export class CronTriggerResolver { @Args('input') input: UpdateCronTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerV2Service.updateOne(input, workspaceId); + const flatCronTrigger = await this.cronTriggerV2Service.updateOne( + input, + workspaceId, + ); + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @@ -96,11 +130,16 @@ export class CronTriggerResolver { @Args('input') input: CreateCronTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerV2Service.createOne(input, workspaceId); + const flatCronTrigger = await this.cronTriggerV2Service.createOne( + input, + workspaceId, + ); + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts index 440d82ac21d..a71d57bf0ff 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts @@ -10,7 +10,7 @@ import { CronTriggerException, CronTriggerExceptionCode, } from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception'; -import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; +import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; import { fromCreateCronTriggerInputToFlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/utils/from-create-cron-trigger-input-to-flat-cron-trigger.util'; import { fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/cron-trigger/utils/from-update-cron-trigger-input-to-flat-cron-trigger-to-update-or-throw.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; @@ -34,7 +34,7 @@ export class CronTriggerV2Service { * when interacting with another application than workspace custom one * */ applicationId?: string, - ) { + ): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { @@ -90,7 +90,7 @@ export class CronTriggerV2Service { async updateOne( cronTriggerInput: UpdateCronTriggerInput, workspaceId: string, - ) { + ): Promise { const { flatCronTriggerMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts index 083c3ed37c1..707a7b5c5e3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts @@ -7,7 +7,7 @@ import { CronTriggerExceptionCode, } from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception'; -export const cronTriggerGraphQLApiExceptionHandler = (error: Error): void => { +export const cronTriggerGraphQLApiExceptionHandler = (error: Error): never => { if (error instanceof CronTriggerException) { switch (error.code) { case CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util.ts new file mode 100644 index 00000000000..c98120c3a64 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util.ts @@ -0,0 +1,11 @@ +import { type CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto'; +import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; + +export const fromFlatCronTriggerToCronTriggerDto = ( + flatCronTrigger: FlatCronTrigger, +): CronTriggerDTO => ({ + id: flatCronTrigger.id, + settings: flatCronTrigger.settings, + createdAt: new Date(flatCronTrigger.createdAt), + updatedAt: new Date(flatCronTrigger.updatedAt), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts index 5e0f77096f0..1f216cecd0a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts @@ -1,9 +1,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -15,9 +14,15 @@ import { CreateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/dat import { DatabaseEventTriggerIdInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger-id.input'; import { DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto'; import { UpdateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/update-database-event-trigger.input'; -import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity'; +import { + DatabaseEventTriggerException, + DatabaseEventTriggerExceptionCode, +} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception'; import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service'; import { databaseEventTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils'; +import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; @UseGuards( WorkspaceAuthGuard, @@ -29,37 +34,61 @@ import { databaseEventTriggerGraphQLApiExceptionHandler } from 'src/engine/metad export class DatabaseEventTriggerResolver { constructor( private readonly databaseEventTriggerV2Service: DatabaseEventTriggerV2Service, - @InjectRepository(DatabaseEventTriggerEntity) - private readonly databaseEventTriggerRepository: Repository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => DatabaseEventTriggerDTO) async findOneDatabaseEventTrigger( @Args('input') { id }: DatabaseEventTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, + const { flatDatabaseEventTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatDatabaseEventTriggerMaps'], + }, + ); + + const flatDatabaseEventTrigger = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatDatabaseEventTriggerMaps, }); + + if (!isDefined(flatDatabaseEventTrigger)) { + throw new DatabaseEventTriggerException( + `Database event trigger with id ${id} not found`, + DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND, + ); + } + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, + ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @Query(() => [DatabaseEventTriggerDTO]) async findManyDatabaseEventTriggers( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerRepository.find({ - where: { workspaceId }, - }); + const { flatDatabaseEventTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatDatabaseEventTriggerMaps'], + }, + ); + + return Object.values(flatDatabaseEventTriggerMaps.byId) + .filter(isDefined) + .map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @@ -67,14 +96,19 @@ export class DatabaseEventTriggerResolver { async deleteOneDatabaseEventTrigger( @Args('input') input: DatabaseEventTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerV2Service.destroyOne({ - destroyDatabaseEventTriggerInput: input, - workspaceId, - }); + const flatDatabaseEventTrigger = + await this.databaseEventTriggerV2Service.destroyOne({ + destroyDatabaseEventTriggerInput: input, + workspaceId, + }); + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, + ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @@ -83,14 +117,16 @@ export class DatabaseEventTriggerResolver { @Args('input') input: UpdateDatabaseEventTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerV2Service.updateOne( - input, - workspaceId, + const flatDatabaseEventTrigger = + await this.databaseEventTriggerV2Service.updateOne(input, workspaceId); + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @@ -99,14 +135,16 @@ export class DatabaseEventTriggerResolver { @Args('input') input: CreateDatabaseEventTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerV2Service.createOne( - input, - workspaceId, + const flatDatabaseEventTrigger = + await this.databaseEventTriggerV2Service.createOne(input, workspaceId); + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts index 4be493b08ae..40909c5c8b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts @@ -10,7 +10,7 @@ import { DatabaseEventTriggerException, DatabaseEventTriggerExceptionCode, } from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception'; -import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; +import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; import { fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/utils/from-create-database-event-trigger-input-to-flat-database-event-trigger.util'; import { fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/database-event-trigger/utils/from-update-database-event-trigger-input-to-flat-database-event-trigger-to-update-or-throw.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; @@ -34,7 +34,7 @@ export class DatabaseEventTriggerV2Service { * when interacting with another application than workspace custom one * */ applicationId?: string, - ) { + ): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { @@ -92,7 +92,7 @@ export class DatabaseEventTriggerV2Service { async updateOne( databaseEventTriggerInput: UpdateDatabaseEventTriggerInput, workspaceId: string, - ) { + ): Promise { const { flatDatabaseEventTriggerMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts index 069f0727205..e7d7408c3a5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts @@ -9,7 +9,7 @@ import { export const databaseEventTriggerGraphQLApiExceptionHandler = ( error: Error, -): void => { +): never => { if (error instanceof DatabaseEventTriggerException) { switch (error.code) { case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util.ts new file mode 100644 index 00000000000..446e18037b6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util.ts @@ -0,0 +1,11 @@ +import { type DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto'; +import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; + +export const fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto = ( + flatDatabaseEventTrigger: FlatDatabaseEventTrigger, +): DatabaseEventTriggerDTO => ({ + id: flatDatabaseEventTrigger.id, + settings: flatDatabaseEventTrigger.settings, + createdAt: new Date(flatDatabaseEventTrigger.createdAt), + updatedAt: new Date(flatDatabaseEventTrigger.updatedAt), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts index c5210b5e5ee..6975cbd0ac1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts @@ -3,6 +3,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; import { FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/cron-trigger/constants/flat-cron-trigger-editable-properties.constant'; import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-editable-properties.constant'; import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant'; +import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant'; import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; import { FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant'; @@ -14,7 +15,6 @@ import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-module import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant'; import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate-group/constants/flat-row-level-permission-predicate-group-editable-properties.constant'; import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/flat-row-level-permission-predicate-editable-properties.constant'; -import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant'; import { FLAT_SKILL_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-skill/constants/flat-skill-editable-properties.constant'; import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant'; import { FLAT_VIEW_FILTER_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter-group/constants/flat-view-filter-group-editable-properties.constant'; @@ -81,8 +81,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = { (property) => property !== 'code', ), 'deletedAt', + 'latestVersion', + 'publishedVersions', ], - propertiesToStringify: ['toolInputSchema'], + propertiesToStringify: ['toolInputSchema', 'publishedVersions'], }, cronTrigger: { propertiesToCompare: [...FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES], diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts index 2ea1cf41c64..7868f77ca23 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts @@ -1,9 +1,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -11,13 +10,19 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input'; import { RouteTriggerIdInput } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger-id.input'; import { RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto'; import { UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input'; -import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; +import { + RouteTriggerException, + RouteTriggerExceptionCode, +} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service'; +import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util'; import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils'; @UseGuards( @@ -33,37 +38,59 @@ import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modu export class RouteTriggerResolver { constructor( private readonly routeV2Service: RouteTriggerV2Service, - @InjectRepository(RouteTriggerEntity) - private readonly routeTriggerRepository: Repository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => RouteTriggerDTO) async findOneRouteTrigger( @Args('input') { id }: RouteTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeTriggerRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, + const { flatRouteTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRouteTriggerMaps'], + }, + ); + + const flatRouteTrigger = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatRouteTriggerMaps, }); + + if (!isDefined(flatRouteTrigger)) { + throw new RouteTriggerException( + `Route trigger with id ${id} not found`, + RouteTriggerExceptionCode.ROUTE_NOT_FOUND, + ); + } + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @Query(() => [RouteTriggerDTO]) async findManyRouteTriggers( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeTriggerRepository.find({ - where: { workspaceId }, - }); + const { flatRouteTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRouteTriggerMaps'], + }, + ); + + return Object.values(flatRouteTriggerMaps.byId) + .filter(isDefined) + .map(fromFlatRouteTriggerToRouteTriggerDto); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @@ -71,14 +98,16 @@ export class RouteTriggerResolver { async deleteOneRouteTrigger( @Args('input') input: RouteTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeV2Service.destroyOne({ + const flatRouteTrigger = await this.routeV2Service.destroyOne({ destroyRouteTriggerInput: input, workspaceId, }); + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @@ -87,11 +116,16 @@ export class RouteTriggerResolver { @Args('input') input: UpdateRouteTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeV2Service.updateOne(input, workspaceId); + const flatRouteTrigger = await this.routeV2Service.updateOne( + input, + workspaceId, + ); + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @@ -100,11 +134,16 @@ export class RouteTriggerResolver { @Args('input') input: CreateRouteTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeV2Service.createOne(input, workspaceId); + const flatRouteTrigger = await this.routeV2Service.createOne( + input, + workspaceId, + ); + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts index c4b9852ba5f..17a75c2b6f3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts @@ -12,7 +12,7 @@ import { RouteTriggerException, RouteTriggerExceptionCode, } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; -import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; +import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; import { fromCreateRouteTriggerInputToFlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/utils/from-create-route-trigger-input-to-flat-route-trigger.util'; import { fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/route-trigger/utils/from-update-route-trigger-input-to-flat-route-trigger-to-update-or-throw.util'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; @@ -34,7 +34,7 @@ export class RouteTriggerV2Service { * when interacting with another application than workspace custom one * */ applicationId?: string, - ) { + ): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { @@ -89,7 +89,7 @@ export class RouteTriggerV2Service { async updateOne( routeTriggerInput: UpdateRouteTriggerInput, workspaceId: string, - ) { + ): Promise { const { flatRouteTriggerMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util.ts new file mode 100644 index 00000000000..f925a3d1464 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util.ts @@ -0,0 +1,14 @@ +import { type RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto'; +import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; + +export const fromFlatRouteTriggerToRouteTriggerDto = ( + flatRouteTrigger: FlatRouteTrigger, +): RouteTriggerDTO => ({ + id: flatRouteTrigger.id, + path: flatRouteTrigger.path, + isAuthRequired: flatRouteTrigger.isAuthRequired, + httpMethod: flatRouteTrigger.httpMethod, + forwardedRequestHeaders: flatRouteTrigger.forwardedRequestHeaders, + createdAt: new Date(flatRouteTrigger.createdAt), + updatedAt: new Date(flatRouteTrigger.updatedAt), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts index 078f6dcb788..fd82c2e35bf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts @@ -7,7 +7,7 @@ import { RouteTriggerExceptionCode, } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; -export const routeTriggerGraphQLApiExceptionHandler = (error: Error): void => { +export const routeTriggerGraphQLApiExceptionHandler = (error: Error): never => { if (error instanceof RouteTriggerException) { switch (error.code) { case RouteTriggerExceptionCode.ROUTE_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts index d21f7d058e9..44da77e49dc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts @@ -5,13 +5,23 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; import { ServerlessFunctionLayerResolver } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.resolver'; import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service'; +import { WorkspaceServerlessFunctionLayerMapCacheService } from 'src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ PermissionsModule, TypeOrmModule.forFeature([ServerlessFunctionLayerEntity]), + WorkspaceCacheModule, + ], + providers: [ + ServerlessFunctionLayerService, + ServerlessFunctionLayerResolver, + WorkspaceServerlessFunctionLayerMapCacheService, + ], + exports: [ + ServerlessFunctionLayerService, + WorkspaceServerlessFunctionLayerMapCacheService, ], - providers: [ServerlessFunctionLayerService, ServerlessFunctionLayerResolver], - exports: [ServerlessFunctionLayerService], }) export class ServerlessFunctionLayerModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts index 79eeb8b2f93..67fe6285370 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts @@ -10,12 +10,14 @@ import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serve import { CreateServerlessFunctionLayerInput } from 'src/engine/metadata-modules/serverless-function-layer/dtos/create-serverless-function-layer.input'; import { getLastCommonLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-common-layer-dependencies'; import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class ServerlessFunctionLayerService { constructor( @InjectRepository(ServerlessFunctionLayerEntity) private readonly serverlessFunctionLayerRepository: Repository, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async create( @@ -32,12 +34,21 @@ export class ServerlessFunctionLayerService { workspaceId, }); - return this.serverlessFunctionLayerRepository.save(serverlessFunctionLayer); + const savedLayer = await this.serverlessFunctionLayerRepository.save( + serverlessFunctionLayer, + ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'serverlessFunctionLayerMaps', + ]); + + return savedLayer; } async update( id: string, data: QueryDeepPartialEntity, + workspaceId: string, ) { const checksum = data.yarnLock ? serverlessFunctionCreateHash(data.yarnLock as string) @@ -45,7 +56,16 @@ export class ServerlessFunctionLayerService { const updateData = { ...data, ...(checksum && { checksum }) }; - return this.serverlessFunctionLayerRepository.update(id, updateData); + const result = await this.serverlessFunctionLayerRepository.update( + id, + updateData, + ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'serverlessFunctionLayerMaps', + ]); + + return result; } async createCommonLayerIfNotExist(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service.ts new file mode 100644 index 00000000000..1deeed4421a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; +import { type ServerlessFunctionLayerCacheMaps } from 'src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type'; +import { fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; + +@Injectable() +@WorkspaceCache('serverlessFunctionLayerMaps') +export class WorkspaceServerlessFunctionLayerMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ServerlessFunctionLayerEntity) + private readonly serverlessFunctionLayerRepository: Repository, + ) { + super(); + } + + async computeForCache( + workspaceId: string, + ): Promise { + const serverlessFunctionLayerEntities = + await this.serverlessFunctionLayerRepository.find({ + where: { + workspaceId, + }, + }); + + const serverlessFunctionLayerMaps: ServerlessFunctionLayerCacheMaps = { + byId: {}, + }; + + for (const entity of serverlessFunctionLayerEntities) { + const flatServerlessFunctionLayer = + fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer(entity); + + serverlessFunctionLayerMaps.byId[flatServerlessFunctionLayer.id] = + flatServerlessFunctionLayer; + } + + return serverlessFunctionLayerMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type.ts new file mode 100644 index 00000000000..2cb2b0003c2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type.ts @@ -0,0 +1,5 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type'; +import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; + +export type FlatServerlessFunctionLayer = + FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type.ts new file mode 100644 index 00000000000..f3c5171c8da --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type.ts @@ -0,0 +1,5 @@ +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; + +export type ServerlessFunctionLayerCacheMaps = { + byId: Partial>; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util.ts new file mode 100644 index 00000000000..612cf5d93a2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util.ts @@ -0,0 +1,15 @@ +import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; + +export const fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer = ( + entity: ServerlessFunctionLayerEntity, +): FlatServerlessFunctionLayer => ({ + id: entity.id, + packageJson: entity.packageJson, + yarnLock: entity.yarnLock, + checksum: entity.checksum, + workspaceId: entity.workspaceId, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), + serverlessFunctionIds: entity.serverlessFunctions?.map((sf) => sf.id) ?? [], +}); 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 92b86da9414..77f1f7a735b 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 @@ -24,6 +24,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service'; import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service'; import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ @@ -46,6 +47,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WorkspaceMigrationModule, ServerlessFunctionLayerModule, SubscriptionsModule, + WorkspaceCacheModule, TokenModule, ], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts index 2f9d4772a73..092b7fbe853 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts @@ -1,11 +1,9 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; import graphqlTypeJson from 'graphql-type-json'; -import { Repository } from 'typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -14,19 +12,22 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input'; import { GetServerlessFunctionSourceCodeInput } from 'src/engine/metadata-modules/serverless-function/dtos/get-serverless-function-source-code.input'; import { PublishServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/publish-serverless-function.input'; import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input'; -import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; -import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; -import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; -import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils'; import { ServerlessFunctionLogsDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.dto'; import { ServerlessFunctionLogsInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.input'; +import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; +import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; +import { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util'; +import { fromFlatServerlessFunctionToServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util'; +import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils'; import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum'; import { SubscriptionService } from 'src/engine/subscriptions/subscription.service'; @@ -41,40 +42,91 @@ import { SubscriptionService } from 'src/engine/subscriptions/subscription.servi export class ServerlessFunctionResolver { constructor( private readonly serverlessFunctionService: ServerlessFunctionService, - @InjectRepository(ServerlessFunctionEntity) - private readonly serverlessFunctionRepository: Repository, private readonly subscriptionService: SubscriptionService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => ServerlessFunctionDTO) async findOneServerlessFunction( @Args('input') { id }: ServerlessFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, - relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'], + const { + flatServerlessFunctionMaps, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatServerlessFunctionMaps', + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @Query(() => [ServerlessFunctionDTO]) async findManyServerlessFunctions( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return this.serverlessFunctionRepository.find({ - where: { workspaceId }, - relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'], - }); + const { + flatServerlessFunctionMaps, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatServerlessFunctionMaps', + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return Object.values(flatServerlessFunctionMaps.byId) + .filter( + ( + flatServerlessFunction, + ): flatServerlessFunction is FlatServerlessFunction => + isDefined(flatServerlessFunction) && + !isDefined(flatServerlessFunction.deletedAt), + ) + .map((flatServerlessFunction) => + fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }), + ); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -83,7 +135,7 @@ export class ServerlessFunctionResolver { try { return await this.serverlessFunctionService.getAvailablePackages(id); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -99,7 +151,7 @@ export class ServerlessFunctionResolver { input.version, ); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -108,14 +160,38 @@ export class ServerlessFunctionResolver { async deleteOneServerlessFunction( @Args('input') input: ServerlessFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionService.deleteOneServerlessFunction({ - id: input.id, - workspaceId, + const flatServerlessFunction = + await this.serverlessFunctionService.deleteOneServerlessFunction({ + id: input.id, + workspaceId, + }); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -125,14 +201,38 @@ export class ServerlessFunctionResolver { @Args('input') input: UpdateServerlessFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionService.updateOneServerlessFunction( - input, - workspaceId, - ); + const flatServerlessFunction = + await this.serverlessFunctionService.updateOneServerlessFunction( + input, + workspaceId, + ); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -142,14 +242,38 @@ export class ServerlessFunctionResolver { @Args('input') input: CreateServerlessFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionService.createOneServerlessFunction( - input, - workspaceId, - ); + const flatServerlessFunction = + await this.serverlessFunctionService.createOneServerlessFunction( + input, + workspaceId, + ); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -169,7 +293,7 @@ export class ServerlessFunctionResolver { version, }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -178,16 +302,40 @@ export class ServerlessFunctionResolver { async publishServerlessFunction( @Args('input') input: PublishServerlessFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { const { id } = input; - return await this.serverlessFunctionService.publishOneServerlessFunctionOrFail( - id, - workspaceId, - ); + const flatServerlessFunction = + await this.serverlessFunctionService.publishOneServerlessFunctionOrFail( + id, + workspaceId, + ); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } 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 ac35213d6d5..eb89cf423af 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 @@ -1,32 +1,31 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { join } from 'path'; - import deepEqual from 'deep-equal'; import { DEFAULT_API_KEY_NAME, DEFAULT_API_URL_NAME, } from 'twenty-shared/application'; -import { Sources } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { IsNull, Not, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed'; import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var'; -import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service'; -import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; @@ -34,8 +33,15 @@ import { ServerlessFunctionException, ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; +import { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util'; +import { fromCreateServerlessFunctionInputToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util'; +import { fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util'; import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum'; import { SubscriptionService } from 'src/engine/subscriptions/subscription.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; import { WorkflowVersionStepException, WorkflowVersionStepExceptionCode, @@ -57,33 +63,57 @@ export class ServerlessFunctionService { private readonly auditService: AuditService, private readonly applicationTokenService: ApplicationTokenService, private readonly subscriptionService: SubscriptionService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly applicationService: ApplicationService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} - async hasServerlessFunctionPublishedVersion(serverlessFunctionId: string) { - return await this.serverlessFunctionRepository.exists({ - where: { - id: serverlessFunctionId, - latestVersion: Not(IsNull()), - }, + async hasServerlessFunctionPublishedVersion( + serverlessFunctionId: string, + workspaceId: string, + ) { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + const flatServerlessFunction = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: serverlessFunctionId, + flatEntityMaps: flatServerlessFunctionMaps, }); + + return ( + isDefined(flatServerlessFunction) && + !isDefined(flatServerlessFunction.deletedAt) && + isDefined(flatServerlessFunction.latestVersion) + ); } async getServerlessFunctionSourceCode( workspaceId: string, id: string, version: string, - ): Promise { - const serverlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + ) { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); + + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); try { - const folderPath = getServerlessFolder({ - serverlessFunction, + const folderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, version, }); @@ -109,24 +139,43 @@ export class ServerlessFunctionService { }): Promise { await this.throttleExecution(workspaceId); - const functionToExecute = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, - relations: [ - 'serverlessFunctionLayer', - 'application.applicationVariables', - ], - }); + const { + flatServerlessFunctionMaps, + flatApplicationMaps, + applicationVariableMaps, + serverlessFunctionLayerMaps, + } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatServerlessFunctionMaps', + 'flatApplicationMaps', + 'applicationVariableMaps', + 'serverlessFunctionLayerMaps', + ]); - const applicationAccessToken = isDefined(functionToExecute.applicationId) + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); + + const flatServerlessFunctionLayer = + serverlessFunctionLayerMaps.byId[ + flatServerlessFunction.serverlessFunctionLayerId + ]; + + if (!isDefined(flatServerlessFunctionLayer)) { + throw new ServerlessFunctionException( + `Serverless function layer with id ${flatServerlessFunction.serverlessFunctionLayerId} not found`, + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + const applicationAccessToken = isDefined( + flatServerlessFunction.applicationId, + ) ? await this.applicationTokenService.generateApplicationToken({ workspaceId, - applicationId: functionToExecute.applicationId, + applicationId: flatServerlessFunction.applicationId, expiresInSeconds: Math.max( - functionToExecute.timeoutSeconds, + flatServerlessFunction.timeoutSeconds, MIN_TOKEN_EXPIRATION_IN_SECONDS, ), }) @@ -134,6 +183,14 @@ export class ServerlessFunctionService { const baseUrl = cleanServerUrl(this.twentyConfigService.get('SERVER_URL')); + const flatApplicationVariables = isDefined( + flatServerlessFunction.applicationId, + ) + ? (applicationVariableMaps.byApplicationId[ + flatServerlessFunction.applicationId + ] ?? []) + : []; + const envVariables = { ...(isDefined(baseUrl) ? { @@ -145,18 +202,19 @@ export class ServerlessFunctionService { [DEFAULT_API_KEY_NAME]: applicationAccessToken.token, } : {}), - ...buildEnvVar(functionToExecute), + ...buildEnvVar(flatApplicationVariables), }; const resultServerlessFunction = await this.callWithTimeout({ callback: () => this.serverlessService.execute({ - serverlessFunction: functionToExecute, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env: envVariables, }), - timeoutMs: functionToExecute.timeoutSeconds * 1000, + timeoutMs: flatServerlessFunction.timeoutSeconds * 1000, }); if (this.twentyConfigService.get('SERVERLESS_LOGS_ENABLED')) { @@ -164,18 +222,24 @@ export class ServerlessFunctionService { console.log(resultServerlessFunction.logs); } + const applicationUniversalIdentifier = isDefined( + flatServerlessFunction.applicationId, + ) + ? flatApplicationMaps.byId[flatServerlessFunction.applicationId] + ?.universalIdentifier + : undefined; + await this.subscriptionService.publish({ channel: SubscriptionChannel.SERVERLESS_FUNCTION_LOGS_CHANNEL, workspaceId, payload: { serverlessFunctionLogs: { logs: resultServerlessFunction.logs, - id: functionToExecute.id, - name: functionToExecute.name, - universalIdentifier: functionToExecute.universalIdentifier, - applicationId: functionToExecute.applicationId, - applicationUniversalIdentifier: - functionToExecute.application?.universalIdentifier, + id: flatServerlessFunction.id, + name: flatServerlessFunction.name, + universalIdentifier: flatServerlessFunction.universalIdentifier, + applicationId: flatServerlessFunction.applicationId, + applicationUniversalIdentifier, }, }, }); @@ -190,23 +254,31 @@ export class ServerlessFunctionService { ...(resultServerlessFunction.error && { errorType: resultServerlessFunction.error.errorType, }), - functionId: functionToExecute.id, - functionName: functionToExecute.name, + functionId: flatServerlessFunction.id, + functionName: flatServerlessFunction.name, }); return resultServerlessFunction; } - async publishOneServerlessFunctionOrFail(id: string, workspaceId: string) { - const existingServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + async publishOneServerlessFunctionOrFail( + id: string, + workspaceId: string, + ): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); - if (isDefined(existingServerlessFunction.latestVersion)) { + const existingFlatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); + + if (isDefined(existingFlatServerlessFunction.latestVersion)) { const latestCode = await this.getServerlessFunctionSourceCode( workspaceId, id, @@ -219,21 +291,21 @@ export class ServerlessFunctionService { ); if (deepEqual(latestCode, draftCode)) { - return existingServerlessFunction; + return existingFlatServerlessFunction; } } - const newVersion = existingServerlessFunction.latestVersion - ? `${parseInt(existingServerlessFunction.latestVersion, 10) + 1}` + const newVersion = existingFlatServerlessFunction.latestVersion + ? `${parseInt(existingFlatServerlessFunction.latestVersion, 10) + 1}` : '1'; - const draftFolderPath = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + const draftFolderPath = getServerlessFolderOrThrow({ + flatServerlessFunction: existingFlatServerlessFunction, version: 'draft', }); - const newFolderPath = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + const newFolderPath = getServerlessFolderOrThrow({ + flatServerlessFunction: existingFlatServerlessFunction, version: newVersion, }); @@ -243,38 +315,60 @@ export class ServerlessFunctionService { }); const newPublishedVersions = [ - ...existingServerlessFunction.publishedVersions, + ...existingFlatServerlessFunction.publishedVersions, newVersion, ]; - await this.serverlessFunctionRepository.update( - existingServerlessFunction.id, - { - latestVersion: newVersion, - publishedVersions: newPublishedVersions, - }, - ); + const updatedFlatServerlessFunction: FlatServerlessFunction = { + ...existingFlatServerlessFunction, + latestVersion: newVersion, + publishedVersions: newPublishedVersions, + }; - const publishedServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [updatedFlatServerlessFunction], + }, + }, workspaceId, + isSystemBuild: false, }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while publishing serverless function', + ); + } + + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + const publishedFlatServerlessFunction = + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, }); - // This check should never be thrown, but we encounter some issue with - // publishing serverless function in self hosted instances - // See https://github.com/twentyhq/twenty/issues/13058 - // TODO: remove this check when issue solved - if (!isDefined(publishedServerlessFunction.latestVersion)) { + if (!isDefined(publishedFlatServerlessFunction.latestVersion)) { throw new WorkflowVersionStepException( - `Fail to publish serverlessFunction ${publishedServerlessFunction.id}.Received latest version ${publishedServerlessFunction.latestVersion}`, + `Fail to publish serverlessFunction ${publishedFlatServerlessFunction.id}.Received latest version ${publishedFlatServerlessFunction.latestVersion}`, WorkflowVersionStepExceptionCode.CODE_STEP_FAILURE, ); } - return publishedServerlessFunction; + return publishedFlatServerlessFunction; } async deleteOneServerlessFunction({ @@ -285,73 +379,196 @@ export class ServerlessFunctionService { id: string; workspaceId: string; softDelete?: boolean; - }) { - const existingServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + }): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - withDeleted: true, - }); + ); - if (softDelete) { - await this.serverlessFunctionRepository.softDelete({ id }); - } else { - await this.serverlessFunctionRepository.delete({ id }); - // We don't need to await this - this.fileStorageService.delete({ - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, - }), - }); + const existingFlatServerlessFunction = flatServerlessFunctionMaps.byId[id]; + + if (!isDefined(existingFlatServerlessFunction)) { + throw new ServerlessFunctionException( + 'Serverless function to delete not found', + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); } - // We don't need to await this - this.serverlessService.delete(existingServerlessFunction); + if (softDelete) { + const updatedFlatServerlessFunctionWithDeletedAt: FlatServerlessFunction = + { + ...existingFlatServerlessFunction, + deletedAt: new Date().toISOString(), + }; - return existingServerlessFunction; + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + updatedFlatServerlessFunctionWithDeletedAt, + ], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting serverless function', + ); + } + + return updatedFlatServerlessFunctionWithDeletedAt; + } else { + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [existingFlatServerlessFunction], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while destroying serverless function', + ); + } + } + + return existingFlatServerlessFunction; } - async restoreOneServerlessFunction(id: string) { - await this.serverlessFunctionRepository.restore({ id }); + async restoreOneServerlessFunction( + id: string, + workspaceId: string, + ): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + const existingFlatServerlessFunction = flatServerlessFunctionMaps.byId[id]; + + if (!isDefined(existingFlatServerlessFunction)) { + throw new ServerlessFunctionException( + 'Serverless function to restore not found', + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + const restoredFlatServerlessFunction: FlatServerlessFunction = { + ...existingFlatServerlessFunction, + deletedAt: null, + }; + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [restoredFlatServerlessFunction], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while restoring serverless function', + ); + } + + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, + }); } async updateOneServerlessFunction( serverlessFunctionInput: UpdateServerlessFunctionInput, workspaceId: string, - ) { - const existingServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id: serverlessFunctionInput.id, + ): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, + ); + + const updatedFlatServerlessFunction = + fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow({ + flatServerlessFunctionMaps, + updateServerlessFunctionInput: serverlessFunctionInput, }); - await this.serverlessFunctionRepository.update( - existingServerlessFunction.id, - { - name: serverlessFunctionInput.update.name, - description: serverlessFunctionInput.update.description, - timeoutSeconds: serverlessFunctionInput.update.timeoutSeconds, - toolInputSchema: serverlessFunctionInput.update.toolInputSchema, - isTool: serverlessFunctionInput.update.isTool, - }, - ); + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [updatedFlatServerlessFunction], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); - const fileFolder = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, - version: 'draft', - }); + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while updating serverless function', + ); + } - await this.fileStorageService.writeFolder( - serverlessFunctionInput.update.code, - fileFolder, - ); + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); - return this.serverlessFunctionRepository.findOneBy({ - id: existingServerlessFunction.id, + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: updatedFlatServerlessFunction.id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, }); } @@ -390,7 +607,7 @@ export class ServerlessFunctionService { serverlessFunctionLayerId?: string; }, workspaceId: string, - ) { + ): Promise { let serverlessFunctionToCreateLayerId = serverlessFunctionInput.serverlessFunctionLayerId; @@ -403,40 +620,58 @@ export class ServerlessFunctionService { serverlessFunctionToCreateLayerId = commonServerlessFunctionLayerId; } - const createServerlessFunctionInput: CreateServerlessFunctionInput = { - ...serverlessFunctionInput, - serverlessFunctionLayerId: serverlessFunctionToCreateLayerId, - }; + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); - // If no toolInputSchema is provided, use the default schema - // (because the default template will be used for the code) - const toolInputSchema = isDefined(serverlessFunctionInput.toolInputSchema) - ? serverlessFunctionInput.toolInputSchema - : DEFAULT_TOOL_INPUT_SCHEMA; - - const serverlessFunctionToCreate = this.serverlessFunctionRepository.create( - { ...createServerlessFunctionInput, workspaceId, toolInputSchema }, - ); - - const createdServerlessFunction = - await this.serverlessFunctionRepository.save(serverlessFunctionToCreate); - - const draftFileFolder = getServerlessFolder({ - serverlessFunction: createdServerlessFunction, - version: 'draft', - }); - - for (const file of await getBaseTypescriptProjectFiles) { - await this.fileStorageService.write({ - file: file.content, - name: file.name, - mimeType: undefined, - folder: join(draftFileFolder, file.path), + const flatServerlessFunctionToCreate = + fromCreateServerlessFunctionInputToFlatServerlessFunction({ + createServerlessFunctionInput: { + ...serverlessFunctionInput, + serverlessFunctionLayerId: serverlessFunctionToCreateLayerId, + }, + workspaceId, + workspaceCustomApplicationId: + serverlessFunctionInput.applicationId ?? + workspaceCustomFlatApplication.id, }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [flatServerlessFunctionToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while creating serverless function', + ); } - return this.serverlessFunctionRepository.findOneBy({ - id: createdServerlessFunction.id, + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatServerlessFunctionToCreate.id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, }); } @@ -453,24 +688,29 @@ export class ServerlessFunctionService { return; } - const serverlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); + + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); await this.fileStorageService.copy({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: serverlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction, version, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: serverlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction, version: 'draft', }), }, @@ -485,50 +725,51 @@ export class ServerlessFunctionService { id: string; version: string; workspaceId: string; - }) { - const serverlessFunctionToDuplicate = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + }): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); - const newServerlessFunction = await this.createOneServerlessFunction( + const flatServerlessFunctionToDuplicate = findFlatServerlessFunctionOrThrow( { - name: serverlessFunctionToDuplicate.name, - description: serverlessFunctionToDuplicate.description ?? undefined, - timeoutSeconds: serverlessFunctionToDuplicate.timeoutSeconds, - applicationId: serverlessFunctionToDuplicate.applicationId ?? undefined, + id, + flatServerlessFunctionMaps, + }, + ); + + const newFlatServerlessFunction = await this.createOneServerlessFunction( + { + name: flatServerlessFunctionToDuplicate.name, + description: flatServerlessFunctionToDuplicate.description ?? undefined, + timeoutSeconds: flatServerlessFunctionToDuplicate.timeoutSeconds, + applicationId: + flatServerlessFunctionToDuplicate.applicationId ?? undefined, serverlessFunctionLayerId: - serverlessFunctionToDuplicate.serverlessFunctionLayerId, + flatServerlessFunctionToDuplicate.serverlessFunctionLayerId, }, workspaceId, ); - if (!isDefined(newServerlessFunction)) { - throw new ServerlessFunctionException( - 'Failed to create new serverless function', - ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED, - ); - } - await this.fileStorageService.copy({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: serverlessFunctionToDuplicate, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: flatServerlessFunctionToDuplicate, version, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: newServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: newFlatServerlessFunction, version: 'draft', }), }, }); - return newServerlessFunction; + return newFlatServerlessFunction; } private async throttleExecution(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts index 0f94bffcc27..e1eb992f262 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts @@ -12,7 +12,7 @@ import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/fl import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type'; +import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util.ts new file mode 100644 index 00000000000..5d5f8333c08 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util.ts @@ -0,0 +1,33 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { + ServerlessFunctionException, + ServerlessFunctionExceptionCode, +} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; + +export const findFlatServerlessFunctionOrThrow = ({ + flatServerlessFunctionMaps, + id, +}: { + flatServerlessFunctionMaps: MetadataFlatEntityMaps<'serverlessFunction'>; + id: string; +}) => { + const flatServerlessFunction = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatServerlessFunctionMaps, + }); + + if ( + !isDefined(flatServerlessFunction) || + isDefined(flatServerlessFunction.deletedAt) + ) { + throw new ServerlessFunctionException( + `Serverless function with id ${id} not found`, + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + return flatServerlessFunction; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts index 60414cb32e0..f3dd0a28cf8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts @@ -1,5 +1,5 @@ -import { v4 } from 'uuid'; import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant'; import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; @@ -39,7 +39,7 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({ handlerName: rawCreateServerlessFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME, universalIdentifier: - rawCreateServerlessFunctionInput.universalIdentifier ?? id, + rawCreateServerlessFunctionInput.universalIdentifier ?? v4(), createdAt: currentDate.toISOString(), updatedAt: currentDate.toISOString(), deletedAt: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts new file mode 100644 index 00000000000..81b2c4c1fa7 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts @@ -0,0 +1,59 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; +import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util'; +import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; +import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; +import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util'; +import { type ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; + +export const fromFlatServerlessFunctionToServerlessFunctionDto = ({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, +}: { + flatServerlessFunction: FlatServerlessFunction; + flatCronTriggerMaps: FlatEntityMaps; + flatDatabaseEventTriggerMaps: FlatEntityMaps; + flatRouteTriggerMaps: FlatEntityMaps; +}): ServerlessFunctionDTO => { + const cronTriggers = flatServerlessFunction.cronTriggerIds + .map((id) => flatCronTriggerMaps.byId[id]) + .filter(isDefined) + .map(fromFlatCronTriggerToCronTriggerDto); + + const databaseEventTriggers = flatServerlessFunction.databaseEventTriggerIds + .map((id) => flatDatabaseEventTriggerMaps.byId[id]) + .filter(isDefined) + .map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto); + + const routeTriggers = flatServerlessFunction.routeTriggerIds + .map((id) => flatRouteTriggerMaps.byId[id]) + .filter(isDefined) + .map(fromFlatRouteTriggerToRouteTriggerDto); + + return { + id: flatServerlessFunction.id, + name: flatServerlessFunction.name, + description: flatServerlessFunction.description ?? undefined, + runtime: flatServerlessFunction.runtime, + timeoutSeconds: flatServerlessFunction.timeoutSeconds, + latestVersion: flatServerlessFunction.latestVersion ?? undefined, + handlerPath: flatServerlessFunction.handlerPath, + handlerName: flatServerlessFunction.handlerName, + publishedVersions: flatServerlessFunction.publishedVersions, + toolInputSchema: flatServerlessFunction.toolInputSchema ?? undefined, + isTool: flatServerlessFunction.isTool, + applicationId: flatServerlessFunction.applicationId ?? undefined, + workspaceId: flatServerlessFunction.workspaceId, + createdAt: new Date(flatServerlessFunction.createdAt), + updatedAt: new Date(flatServerlessFunction.updatedAt), + cronTriggers, + databaseEventTriggers, + routeTriggers, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type.ts rename to packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts index 511372aa180..dd8dc1b6440 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts @@ -1,18 +1,13 @@ -import { t } from '@lingui/core/macro'; import { extractAndSanitizeObjectStringFields, - isDefined, trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type'; import { FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant'; import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; -import { - ServerlessFunctionException, - ServerlessFunctionExceptionCode, -} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; +import { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util'; import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils'; import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; @@ -22,7 +17,7 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr flatServerlessFunctionMaps, }: { updateServerlessFunctionInput: UpdateServerlessFunctionInput; - flatServerlessFunctionMaps: FlatEntityMaps; + flatServerlessFunctionMaps: MetadataFlatEntityMaps<'serverlessFunction'>; }): FlatServerlessFunction => { const { id: serverlessFunctionToUpdateId } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( @@ -31,14 +26,10 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr ); const existingFlatServerlessFunctionToUpdate = - flatServerlessFunctionMaps.byId[serverlessFunctionToUpdateId]; - - if (!isDefined(existingFlatServerlessFunctionToUpdate)) { - throw new ServerlessFunctionException( - t`Serverless function to update not found`, - ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, - ); - } + findFlatServerlessFunctionOrThrow({ + id: serverlessFunctionToUpdateId, + flatServerlessFunctionMaps, + }); const updatedEditableFieldProperties = { ...extractAndSanitizeObjectStringFields( { diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts index 1cbf4d7b3ea..b4d8b8c6bcb 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -2,6 +2,7 @@ import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { type EntityMetadata } from 'typeorm'; import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; +import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type'; import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { type FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type'; import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type'; @@ -9,6 +10,7 @@ import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/ import { type UserWorkspaceRoleMap } from 'src/engine/metadata-modules/role-target/services/workspace-user-workspace-role-map-cache.service'; import { type FlatRowLevelPermissionPredicateGroupMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group-maps.type'; import { type FlatRowLevelPermissionPredicateMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-maps.type'; +import { type ServerlessFunctionLayerCacheMaps } from 'src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type'; export const WORKSPACE_CACHE_KEYS_V2 = { flatObjectMetadataMaps: 'flat-maps:object-metadata', @@ -44,6 +46,8 @@ export const WORKSPACE_CACHE_KEYS_V2 = { 'flat-maps:row-level-permission-predicate-group', flatFrontComponentMaps: 'flat-maps:front-component', flatWorkspaceMemberMaps: 'flat-maps:workspace-member', + serverlessFunctionLayerMaps: 'cache:serverless-function-layer', + applicationVariableMaps: 'cache:application-variable', } as const satisfies Record; export type AdditionalCacheDataMaps = { @@ -57,6 +61,8 @@ export type AdditionalCacheDataMaps = { flatRowLevelPermissionPredicateMaps: FlatRowLevelPermissionPredicateMaps; flatRowLevelPermissionPredicateGroupMaps: FlatRowLevelPermissionPredicateGroupMaps; flatWorkspaceMemberMaps: FlatWorkspaceMemberMaps; + serverlessFunctionLayerMaps: ServerlessFunctionLayerCacheMaps; + applicationVariableMaps: ApplicationVariableCacheMaps; }; export type WorkspaceCacheDataMap = AllFlatEntityMaps & AdditionalCacheDataMaps; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts index a0c310845fa..1e866faf6a4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts @@ -8,7 +8,7 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { CreateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; @@ -38,8 +38,8 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra workspaceId, }); - const draftFileFolder = getServerlessFolder({ - serverlessFunction, + const draftFileFolder = getServerlessFolderOrThrow({ + flatServerlessFunction: serverlessFunction, version: 'draft', }); @@ -66,8 +66,8 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra const { action } = context; await this.fileStorageService.delete({ - folderPath: getServerlessFolder({ - serverlessFunction: action.flatEntity, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: action.flatEntity, }), }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts index 61bc655b953..23aa9755b51 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { DeleteServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type'; @@ -43,13 +43,13 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra // TODO: Should implement a cron task or a job to delete the files after a certain period of time await this.fileStorageService.move({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, toDelete: true, }), }, @@ -70,14 +70,14 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra await this.fileStorageService.move({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, toDelete: true, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, toDelete: false, }), }, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts index df15270459f..68236fe55cc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts @@ -7,7 +7,7 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; @@ -43,7 +43,7 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action), ); - const serverlessFunction = findFlatEntityByIdInFlatEntityMapsOrThrow({ + const flatServerlessFunction = findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityId: entityId, flatEntityMaps: context.allFlatEntityMaps.flatServerlessFunctionMaps, }); @@ -51,37 +51,35 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra for (const update of action.updates) { if (update.property === 'checksum' && isDefined(code)) { await this.handleChecksumUpdate({ - serverlessFunction, + flatServerlessFunction, code, }); } if (update.property === 'deletedAt' && isDefined(update.to)) { await this.handleDeletedAtUpdate({ - serverlessFunction, + flatServerlessFunction, }); } } } async handleDeletedAtUpdate({ - serverlessFunction, + flatServerlessFunction, }: { - serverlessFunction: FlatServerlessFunction; + flatServerlessFunction: FlatServerlessFunction; }) { - this.serverlessService.delete( - serverlessFunction as unknown as ServerlessFunctionEntity, - ); + this.serverlessService.delete(flatServerlessFunction); } async handleChecksumUpdate({ - serverlessFunction, + flatServerlessFunction, code, }: { - serverlessFunction: FlatServerlessFunction; + flatServerlessFunction: FlatServerlessFunction; code: Sources; }) { - const fileFolder = getServerlessFolder({ - serverlessFunction, + const fileFolder = getServerlessFolderOrThrow({ + flatServerlessFunction, version: 'draft', }); diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index ed44369c088..82fa308abd2 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -327,6 +327,7 @@ export class WorkflowCommonWorkspaceService { case 'restore': await this.serverlessFunctionService.restoreOneServerlessFunction( step.settings.input.serverlessFunctionId, + workspaceId, ); break; case 'destroy': diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts index ec14c30d533..1c0ac4f3fc1 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts @@ -6,8 +6,9 @@ import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/ag import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { ServerlessFunctionRuntime } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @@ -238,29 +239,33 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { describe('runStepCreationSideEffectsAndBuildStep', () => { it('should create code step with serverless function', async () => { - const mockServerlessFunction = { + const mockFlatServerlessFunction: FlatServerlessFunction = { id: 'new-function-id', name: 'Test Function', description: 'Test Description', latestVersion: 'v1', publishedVersions: [], workspaceId: mockWorkspaceId, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), deletedAt: null, - isActive: true, - isSystem: false, - isCustom: true, - isPublic: false, - runtime: 'nodejs', + runtime: ServerlessFunctionRuntime.NODE22, timeoutSeconds: 30, - layerArn: '', - layerName: '', - layerSize: 0, - } as unknown as ServerlessFunctionEntity; + handlerPath: 'src/index.ts', + handlerName: 'main', + checksum: null, + toolInputSchema: null, + isTool: false, + serverlessFunctionLayerId: 'layer-id', + universalIdentifier: 'universal-id', + applicationId: null, + cronTriggerIds: [], + databaseEventTriggerIds: [], + routeTriggerIds: [], + }; serverlessFunctionService.createOneServerlessFunction.mockResolvedValue( - mockServerlessFunction, + mockFlatServerlessFunction, ); const result = await service.runStepCreationSideEffectsAndBuildStep({ @@ -313,29 +318,33 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { nextStepIds: ['next-step'], } as unknown as WorkflowAction; - const mockNewServerlessFunction = { + const mockNewFlatServerlessFunction: FlatServerlessFunction = { id: 'new-function-id', name: 'Test Function', description: 'Test Description', latestVersion: 'v1', publishedVersions: [], workspaceId: mockWorkspaceId, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), deletedAt: null, - isActive: true, - isSystem: false, - isCustom: true, - isPublic: false, - runtime: 'nodejs', + runtime: ServerlessFunctionRuntime.NODE22, timeoutSeconds: 30, - layerArn: '', - layerName: '', - layerSize: 0, - } as unknown as ServerlessFunctionEntity; + handlerPath: 'src/index.ts', + handlerName: 'main', + checksum: null, + toolInputSchema: null, + isTool: false, + serverlessFunctionLayerId: 'layer-id', + universalIdentifier: 'universal-id', + applicationId: null, + cronTriggerIds: [], + databaseEventTriggerIds: [], + routeTriggerIds: [], + }; serverlessFunctionService.duplicateServerlessFunction.mockResolvedValue( - mockNewServerlessFunction, + mockNewFlatServerlessFunction, ); const clonedStep = await service.cloneStep({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts index f5f49d9fdfe..0748536f2fb 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts @@ -86,6 +86,7 @@ export class WorkflowVersionStepOperationsWorkspaceService { if ( !(await this.serverlessFunctionService.hasServerlessFunctionPublishedVersion( step.settings.input.serverlessFunctionId, + workspaceId, )) ) { await this.serverlessFunctionService.deleteOneServerlessFunction({